Merge branch 'develop' of flaschengeist.dev:Flaschengeist/flaschengeist-frontend into develop
This commit is contained in:
		
						commit
						a38954cf70
					
				| 
						 | 
					@ -1,4 +0,0 @@
 | 
				
			||||||
[submodule "deps/quasar-ui-qcalendar"]
 | 
					 | 
				
			||||||
	path = deps/quasar-ui-qcalendar
 | 
					 | 
				
			||||||
	url = https://github.com/susnux/quasar-ui-qcalendar
 | 
					 | 
				
			||||||
	branch = quasar2
 | 
					 | 
				
			||||||
							
								
								
									
										97
									
								
								README.md
								
								
								
								
							
							
						
						
									
										97
									
								
								README.md
								
								
								
								
							| 
						 | 
					@ -4,59 +4,76 @@ Modular student club administration system, licensed under the MIT license.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Installation
 | 
					## Installation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Requirements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					 "engines": {
 | 
				
			||||||
 | 
					    "node": ">= 12.22.1",
 | 
				
			||||||
 | 
					    "npm": ">= 6.14.12",
 | 
				
			||||||
 | 
					    "yarn": ">= 1.21.1"
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					So on debian (buster and bullseye) you will need to install node.js and yarn beside the debian packages to meet the needed versions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					pushd ~/opt
 | 
				
			||||||
 | 
					wget https://nodejs.org/dist/v16.2.0/node-v16.2.0-linux-x64.tar.xz
 | 
				
			||||||
 | 
					tar -xJf node-v16.2.0-linux-x64.tar.xz
 | 
				
			||||||
 | 
					export PATH="$(pwd)/node-v16.2.0-linux-x64/bin":"$PATH"
 | 
				
			||||||
 | 
					npm i -g yarn
 | 
				
			||||||
 | 
					npm i -g @quasar/cli
 | 
				
			||||||
 | 
					popd
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Install the dependencies
 | 
					### Install the dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					```bash
 | 
				
			||||||
yarn install
 | 
					yarn install
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Be aware npm might not work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Configure Plugins
 | 
					### Configure Plugins
 | 
				
			||||||
 | 
					
 | 
				
			||||||
You can activate and deactive Plugins in `src/boot/plugins.ts`.
 | 
					#### Installing a plugin
 | 
				
			||||||
You have to set the name of the Plugin into `config.loadModules`.
 | 
					
 | 
				
			||||||
 | 
					Simply add it as a dependency and install it, for example installing the `pricelist`-plugin:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					yarn add '@flaschengeist/pricelist'
 | 
				
			||||||
 | 
					yarn install
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Enable / Disable a plugin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					After installing a plugin you will have to enable it,
 | 
				
			||||||
 | 
					this is done by adding it to the `plugin.config.js` file.
 | 
				
			||||||
 | 
					For the example above the file should look like:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```js
 | 
				
			||||||
 | 
					module.exports = [
 | 
				
			||||||
 | 
					  // pricelist plugin:
 | 
				
			||||||
 | 
					  '@flaschengeist/pricelist',
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Remember to rebuild the project
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Configure Backend
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The application is using the API of [the backend](https://flaschengeist.dev/Flaschengeist/flaschengeist)
 | 
				
			||||||
 | 
					This access needs to be configured in `src/config.ts'->config.baseURL
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- either you do have a proxy webserver that maps the '/api' to the backend (http://localhost:5000) or
 | 
				
			||||||
 | 
					- you do directly configure the backend there:`baseURL: 'http://localhost:5000'`. Be aware not committing this configuration.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Build the application
 | 
					### Build the application
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					```sh
 | 
				
			||||||
yarn quasar build
 | 
					yarn quasar build
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Development
 | 
					## Development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Icons used
 | 
					Please refer to out [development wiki](https://flaschengeist.dev/Flaschengeist/flaschengeist/wiki/Development).
 | 
				
			||||||
 | 
					 | 
				
			||||||
We are using the `mdi-v5` icon set, so feel free to use any icon from it.
 | 
					 | 
				
			||||||
A list can be found [here](https://materialdesignicons.com/)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Commands useful for development
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Start the app in development mode
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Provides hot-code reloading, error reporting, etc.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
yarn quasar dev
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### File linting
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```bash
 | 
					 | 
				
			||||||
yarn run lint
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Plugins
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Build a Plugin
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
A Flaschengeist-Frontend-Plugin should be placed in `src/plugins`.
 | 
					 | 
				
			||||||
It needs a `plugin.ts` File which exports a plugin with the following interface:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
name: string;
 | 
					 | 
				
			||||||
mainRoutes?: PluginRouteConfig[];
 | 
					 | 
				
			||||||
outRoutes?: PluginRouteConfig[];
 | 
					 | 
				
			||||||
requiredModules: string[];
 | 
					 | 
				
			||||||
version: string;
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
You have to import `FG_Plugin` from `plugins.d.ts`.
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,7 +40,7 @@
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { computed, defineComponent, PropType } from 'vue';
 | 
					import { computed, defineComponent, PropType } from 'vue';
 | 
				
			||||||
import { date as q_date } from 'quasar';
 | 
					import { date as q_date } from 'quasar';
 | 
				
			||||||
import { stringIsDate, stringIsTime, stringIsDateTime, Validator } from 'src/utils/validators';
 | 
					import { stringIsDate, stringIsTime, stringIsDateTime, Validator } from '..';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
  name: 'IsoDateInput',
 | 
					  name: 'IsoDateInput',
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					import IsoDateInput from './IsoDateInput.vue';
 | 
				
			||||||
 | 
					import PasswordInput from './PasswordInput.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { IsoDateInput, PasswordInput };
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					export { api, pinia } from './src/internal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export * from './src/stores/';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export * from './src/utils/datetime';
 | 
				
			||||||
 | 
					export * from './src/utils/permission';
 | 
				
			||||||
 | 
					export * from './src/utils/validators';
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,39 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "license": "MIT",
 | 
				
			||||||
 | 
					  "version": "1.0.0-alpha.1",
 | 
				
			||||||
 | 
					  "name": "@flaschengeist/api",
 | 
				
			||||||
 | 
					  "author": "Tim Gröger <flaschengeist@wu5.de>",
 | 
				
			||||||
 | 
					  "homepage": "https://flaschengeist.dev/Flaschengeist",
 | 
				
			||||||
 | 
					  "description": "Modular student club administration system",
 | 
				
			||||||
 | 
					  "bugs": {
 | 
				
			||||||
 | 
					    "url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "format": "prettier --config ./package.json  --write '{,!(node_modules)/**/}*.ts'",
 | 
				
			||||||
 | 
					    "lint": "eslint --ext .js,.ts,.vue ./src"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "main": "./src/index.ts",
 | 
				
			||||||
 | 
					  "peerDependencies": {
 | 
				
			||||||
 | 
					    "@quasar/app": "^3.0.0-beta.26",
 | 
				
			||||||
 | 
					    "flaschengeist": "^2.0.0-alpha.1",
 | 
				
			||||||
 | 
					    "pinia": "^2.0.0-alpha.19"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@flaschengeist/types": "^1.0.0-alpha.1",
 | 
				
			||||||
 | 
					    "@types/node": "^12.20.13",
 | 
				
			||||||
 | 
					    "@typescript-eslint/eslint-plugin": "^4.24.0",
 | 
				
			||||||
 | 
					    "@typescript-eslint/parser": "^4.24.0",
 | 
				
			||||||
 | 
					    "eslint": "^7.26.0",
 | 
				
			||||||
 | 
					    "eslint-config-prettier": "^8.3.0",
 | 
				
			||||||
 | 
					    "eslint-plugin-vue": "^7.9.0",
 | 
				
			||||||
 | 
					    "eslint-webpack-plugin": "^2.5.4",
 | 
				
			||||||
 | 
					    "prettier": "^2.3.0",
 | 
				
			||||||
 | 
					    "typescript": "^4.2.4"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "prettier": {
 | 
				
			||||||
 | 
					    "singleQuote": true,
 | 
				
			||||||
 | 
					    "semi": true,
 | 
				
			||||||
 | 
					    "printWidth": 120,
 | 
				
			||||||
 | 
					    "arrowParens": "always"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					//https://github.com/vuejs/vue-next/issues/3130
 | 
				
			||||||
 | 
					declare module '*.vue' {
 | 
				
			||||||
 | 
					  import { ComponentOptions } from 'vue';
 | 
				
			||||||
 | 
					  const component: ComponentOptions;
 | 
				
			||||||
 | 
					  export default component;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					import axios from 'axios';
 | 
				
			||||||
 | 
					import { createPinia } from 'pinia';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const api = axios.create();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const pinia = createPinia();
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					export * from './main';
 | 
				
			||||||
 | 
					export * from './session';
 | 
				
			||||||
 | 
					export * from './user';
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,8 @@
 | 
				
			||||||
import { useUserStore, useSessionStore } from 'src/plugins/user/store';
 | 
					 | 
				
			||||||
import { translateNotification } from 'src/boot/plugins';
 | 
					 | 
				
			||||||
import { LocalStorage, SessionStorage } from 'quasar';
 | 
					import { LocalStorage, SessionStorage } from 'quasar';
 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					import { FG_Plugin } from '@flaschengeist/types';
 | 
				
			||||||
 | 
					import { useSessionStore, useUserStore } from '.';
 | 
				
			||||||
import { AxiosResponse } from 'axios';
 | 
					import { AxiosResponse } from 'axios';
 | 
				
			||||||
import { api } from 'src/boot/axios';
 | 
					import { api } from '../internal';
 | 
				
			||||||
import { defineStore } from 'pinia';
 | 
					import { defineStore } from 'pinia';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function loadCurrentSession() {
 | 
					function loadCurrentSession() {
 | 
				
			||||||
| 
						 | 
					@ -25,18 +24,18 @@ export const useMainStore = defineStore({
 | 
				
			||||||
    session: loadCurrentSession(),
 | 
					    session: loadCurrentSession(),
 | 
				
			||||||
    user: loadUser(),
 | 
					    user: loadUser(),
 | 
				
			||||||
    notifications: [] as Array<FG_Plugin.Notification>,
 | 
					    notifications: [] as Array<FG_Plugin.Notification>,
 | 
				
			||||||
    shortcuts: [] as FG_Plugin.MenuLink[],
 | 
					    shortcuts: [] as Array<FG_Plugin.MenuLink>,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getters: {
 | 
					  getters: {
 | 
				
			||||||
    loggedIn() {
 | 
					    loggedIn(): boolean {
 | 
				
			||||||
      return this.session !== undefined;
 | 
					      return this.session !== undefined;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    currentUser() {
 | 
					    currentUser(): FG.User {
 | 
				
			||||||
      if (this.user === undefined) throw 'Not logged in, this should not be called';
 | 
					      if (this.user === undefined) throw 'Not logged in, this should not be called';
 | 
				
			||||||
      return this.user;
 | 
					      return this.user;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    permissions() {
 | 
					    permissions(): string[] {
 | 
				
			||||||
      return this.user?.permissions || [];
 | 
					      return this.user?.permissions || [];
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -50,7 +49,7 @@ export const useMainStore = defineStore({
 | 
				
			||||||
        const sessionStore = useSessionStore();
 | 
					        const sessionStore = useSessionStore();
 | 
				
			||||||
        const session = await sessionStore.getSession(this.session.token);
 | 
					        const session = await sessionStore.getSession(this.session.token);
 | 
				
			||||||
        if (session) {
 | 
					        if (session) {
 | 
				
			||||||
          this.session = this.session;
 | 
					          this.session = session;
 | 
				
			||||||
          const userStore = useUserStore();
 | 
					          const userStore = useUserStore();
 | 
				
			||||||
          const user = await userStore.getUser(this.session.userid);
 | 
					          const user = await userStore.getUser(this.session.userid);
 | 
				
			||||||
          if (user) {
 | 
					          if (user) {
 | 
				
			||||||
| 
						 | 
					@ -76,19 +75,13 @@ export const useMainStore = defineStore({
 | 
				
			||||||
    async logout() {
 | 
					    async logout() {
 | 
				
			||||||
      if (!this.session || !this.session.token) return false;
 | 
					      if (!this.session || !this.session.token) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      LocalStorage.clear();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const token = this.session.token;
 | 
					        const token = this.session.token;
 | 
				
			||||||
        this.$patch({
 | 
					 | 
				
			||||||
          session: undefined,
 | 
					 | 
				
			||||||
          user: undefined,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        await api.delete(`/auth/${token}`);
 | 
					        await api.delete(`/auth/${token}`);
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
      } finally {
 | 
					      } finally {
 | 
				
			||||||
        SessionStorage.clear();
 | 
					        this.handleLoggedOut( );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -119,10 +112,11 @@ export const useMainStore = defineStore({
 | 
				
			||||||
      data.forEach((n) => {
 | 
					      data.forEach((n) => {
 | 
				
			||||||
        n.time = new Date(n.time);
 | 
					        n.time = new Date(n.time);
 | 
				
			||||||
        notifications.push(
 | 
					        notifications.push(
 | 
				
			||||||
          (
 | 
					          (flaschengeist?.plugins.filter((p) => p.name === n.plugin)[0]?.notification)(
 | 
				
			||||||
            flaschengeist?.plugins.filter((p) => p.name === n.plugin)[0]?.notification ||
 | 
					            /*||
 | 
				
			||||||
            translateNotification
 | 
					            translateNotification*/
 | 
				
			||||||
          )(n)
 | 
					            n
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      this.notifications.push(...notifications);
 | 
					      this.notifications.push(...notifications);
 | 
				
			||||||
| 
						 | 
					@ -151,6 +145,15 @@ export const useMainStore = defineStore({
 | 
				
			||||||
    async setShortcuts() {
 | 
					    async setShortcuts() {
 | 
				
			||||||
      await api.put(`users/${this.currentUser.userid}/shortcuts`, this.shortcuts);
 | 
					      await api.put(`users/${this.currentUser.userid}/shortcuts`, this.shortcuts);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    handleLoggedOut() {
 | 
				
			||||||
 | 
					      LocalStorage.clear();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.$patch({
 | 
				
			||||||
 | 
					        session: undefined,
 | 
				
			||||||
 | 
					        user: undefined,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      SessionStorage.clear();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,69 @@
 | 
				
			||||||
 | 
					import { AxiosError, AxiosResponse } from 'axios';
 | 
				
			||||||
 | 
					import { defineStore } from 'pinia';
 | 
				
			||||||
 | 
					import { api } from '../internal';
 | 
				
			||||||
 | 
					import { useMainStore } from '.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useSessionStore = defineStore({
 | 
				
			||||||
 | 
					  id: 'sessions',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  state: () => ({}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getters: {},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  actions: {
 | 
				
			||||||
 | 
					    async getSession(token: string) {
 | 
				
			||||||
 | 
					      return await api
 | 
				
			||||||
 | 
					        .get(`/auth/${token}`)
 | 
				
			||||||
 | 
					        .then(({ data }: AxiosResponse<FG.Session>) => data)
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getSessions() {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const { data } = await api.get<FG.Session[]>('/auth');
 | 
				
			||||||
 | 
					        data.forEach((session) => {
 | 
				
			||||||
 | 
					          session.expires = new Date(session.expires);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const mainStore = useMainStore();
 | 
				
			||||||
 | 
					        const currentSession = data.find((session) => {
 | 
				
			||||||
 | 
					          return session.token === mainStore.session?.token;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        if (currentSession) {
 | 
				
			||||||
 | 
					          mainStore.session = currentSession;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return data;
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        return [] as FG.Session[];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async deleteSession(token: string) {
 | 
				
			||||||
 | 
					      const mainStore = useMainStore();
 | 
				
			||||||
 | 
					      if (token === mainStore.session?.token) return mainStore.logout();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        await api.delete(`/auth/${token}`);
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        if (!error || !('response' in error) || (<AxiosError>error).response?.status != 401)
 | 
				
			||||||
 | 
					          throw error;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async updateSession(lifetime: number, token: string) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const { data } = await api.put<FG.Session>(`auth/${token}`, { value: lifetime });
 | 
				
			||||||
 | 
					        data.expires = new Date(data.expires);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const mainStore = useMainStore();
 | 
				
			||||||
 | 
					        if (mainStore.session?.token == data.token) mainStore.session = data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
import { defineStore } from 'pinia';
 | 
					import { defineStore } from 'pinia';
 | 
				
			||||||
import { api } from 'src/boot/axios';
 | 
					import { AxiosError } from 'axios';
 | 
				
			||||||
import { AxiosError, AxiosResponse } from 'axios';
 | 
					import { api } from '../internal';
 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					import { useMainStore } from '.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useUserStore = defineStore({
 | 
					export const useUserStore = defineStore({
 | 
				
			||||||
  id: 'users',
 | 
					  id: 'users',
 | 
				
			||||||
| 
						 | 
					@ -110,68 +110,3 @@ export const useUserStore = defineStore({
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const useSessionStore = defineStore({
 | 
					 | 
				
			||||||
  id: 'sessions',
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  state: () => ({}),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  getters: {},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  actions: {
 | 
					 | 
				
			||||||
    async getSession(token: string) {
 | 
					 | 
				
			||||||
      return await api
 | 
					 | 
				
			||||||
        .get(`/auth/${token}`)
 | 
					 | 
				
			||||||
        .then(({ data }: AxiosResponse<FG.Session>) => data)
 | 
					 | 
				
			||||||
        .catch(() => null);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async getSessions() {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const { data } = await api.get<FG.Session[]>('/auth');
 | 
					 | 
				
			||||||
        data.forEach((session) => {
 | 
					 | 
				
			||||||
          session.expires = new Date(session.expires);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const mainStore = useMainStore();
 | 
					 | 
				
			||||||
        const currentSession = data.find((session) => {
 | 
					 | 
				
			||||||
          return session.token === mainStore.session?.token;
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        if (currentSession) {
 | 
					 | 
				
			||||||
          mainStore.session = currentSession;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return data;
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        return [] as FG.Session[];
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async deleteSession(token: string) {
 | 
					 | 
				
			||||||
      const mainStore = useMainStore();
 | 
					 | 
				
			||||||
      if (token === mainStore.session?.token) return mainStore.logout();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        await api.delete(`/auth/${token}`);
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        if (!error || !('response' in error) || (<AxiosError>error).response?.status != 401)
 | 
					 | 
				
			||||||
          throw error;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return false;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async updateSession(lifetime: number, token: string) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const { data } = await api.put<FG.Session>(`auth/${token}`, { value: lifetime });
 | 
					 | 
				
			||||||
        data.expires = new Date(data.expires);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const mainStore = useMainStore();
 | 
					 | 
				
			||||||
        if (mainStore.session?.token == data.token) mainStore.session = data;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					import { useMainStore } from '../stores';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function hasPermission(permission: string) {
 | 
					export function hasPermission(permission: string) {
 | 
				
			||||||
  const store = useMainStore();
 | 
					  const store = useMainStore();
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "extends": "@quasar/app/tsconfig-preset",
 | 
				
			||||||
 | 
					  "target": "esnext",
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "baseUrl": "./",
 | 
				
			||||||
 | 
					    "lib": [
 | 
				
			||||||
 | 
					      "es2020",
 | 
				
			||||||
 | 
					      "dom"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "types": [
 | 
				
			||||||
 | 
					      "@flaschengeist/types",
 | 
				
			||||||
 | 
					      "@quasar/app",
 | 
				
			||||||
 | 
					      "node"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1 +0,0 @@
 | 
				
			||||||
Subproject commit f245cb8b16c855c059d9170611797028c600696a
 | 
					 | 
				
			||||||
							
								
								
									
										51
									
								
								package.json
								
								
								
								
							
							
						
						
									
										51
									
								
								package.json
								
								
								
								
							| 
						 | 
					@ -2,47 +2,48 @@
 | 
				
			||||||
  "private": true,
 | 
					  "private": true,
 | 
				
			||||||
  "license": "MIT",
 | 
					  "license": "MIT",
 | 
				
			||||||
  "version": "2.0.0-alpha.1",
 | 
					  "version": "2.0.0-alpha.1",
 | 
				
			||||||
  "productName": "Flaschengeist",
 | 
					  "productName": "flaschengeist-frontend",
 | 
				
			||||||
  "name": "flaschengeist-frontend",
 | 
					  "name": "flaschengeist",
 | 
				
			||||||
  "author": "Tim Gröger <flaschengeist@wu5.de>",
 | 
					  "author": "Tim Gröger <flaschengeist@wu5.de>",
 | 
				
			||||||
  "homepage": "https://flaschengeist.dev/Flaschengeist",
 | 
					  "homepage": "https://flaschengeist.dev/Flaschengeist",
 | 
				
			||||||
  "description": "Modular student club administration system",
 | 
					  "description": "Modular student club administration system",
 | 
				
			||||||
  "bugs": {
 | 
					  "bugs": {
 | 
				
			||||||
    "url": "https://flaschengeist.dev/Flaschengeist/flaschengeist-frontend/issues"
 | 
					    "url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "format": "prettier --config ./package.json  --write '{,!(node_modules)/**/}*.ts'",
 | 
					    "format": "prettier --config ./package.json  --write '{,!(node_modules)/**/}*.ts'",
 | 
				
			||||||
    "lint": "eslint --ext .js,.ts,.vue ./src"
 | 
					    "lint": "eslint --ext .js,.ts,.vue ./src ./api"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@flaschengeist/api": "file:./api",
 | 
				
			||||||
 | 
					    "@flaschengeist/users": "^1.0.0-alpha.1",
 | 
				
			||||||
    "axios": "^0.21.1",
 | 
					    "axios": "^0.21.1",
 | 
				
			||||||
    "cordova": "^10.0.0",
 | 
					    "cordova": "^10.0.0",
 | 
				
			||||||
    "pinia": "^2.0.0-alpha.10",
 | 
					    "pinia": "^2.0.0-beta.3",
 | 
				
			||||||
    "quasar": "^2.0.0-beta.12",
 | 
					    "quasar": "^2.0.0"
 | 
				
			||||||
    "vuedraggable": "^4.0.1"
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@quasar/app": "^3.0.0-beta.13",
 | 
					    "@flaschengeist/types": "^1.0.0-alpha.1",
 | 
				
			||||||
    "@quasar/extras": "^1.10.2",
 | 
					    "@quasar/app": "^3.0.0",
 | 
				
			||||||
    "@quasar/quasar-app-extension-qcalendar": "file:deps/quasar-ui-qcalendar/app-extension",
 | 
					    "@quasar/extras": "^1.10.7",
 | 
				
			||||||
    "@types/node": "^12.20.7",
 | 
					    "@types/node": "^12.20.15",
 | 
				
			||||||
    "@types/webpack": "^4.41.27",
 | 
					    "@types/webpack": "^5.28.0",
 | 
				
			||||||
    "@types/webpack-env": "^1.16.0",
 | 
					    "@types/webpack-env": "^1.16.0",
 | 
				
			||||||
    "@typescript-eslint/eslint-plugin": "^4.20.0",
 | 
					    "@typescript-eslint/eslint-plugin": "^4.24.0",
 | 
				
			||||||
    "@typescript-eslint/parser": "^4.20.0",
 | 
					    "@typescript-eslint/parser": "^4.24.0",
 | 
				
			||||||
    "electron": "^12.0.4",
 | 
					    "eslint": "^7.26.0",
 | 
				
			||||||
    "electron-packager": "^14.1.1",
 | 
					    "eslint-config-prettier": "^8.3.0",
 | 
				
			||||||
    "eslint": "^7.23.0",
 | 
					    "eslint-plugin-vue": "^7.9.0",
 | 
				
			||||||
    "eslint-config-prettier": "^8.1.0",
 | 
					    "eslint-webpack-plugin": "^2.5.4",
 | 
				
			||||||
    "eslint-plugin-vue": "^7.8.0",
 | 
					    "modify-source-webpack-plugin": "^3.0.0-rc.0",
 | 
				
			||||||
    "eslint-webpack-plugin": "^2.5.3",
 | 
					    "prettier": "^2.3.0",
 | 
				
			||||||
    "prettier": "^2.2.1",
 | 
					    "typescript": "^4.2.4",
 | 
				
			||||||
    "typescript": "^4.2.3"
 | 
					    "vuedraggable": "^4.0.1"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "prettier": {
 | 
					  "prettier": {
 | 
				
			||||||
    "singleQuote": true,
 | 
					    "singleQuote": true,
 | 
				
			||||||
    "semi": true,
 | 
					    "semi": true,
 | 
				
			||||||
    "printWidth": 100,
 | 
					    "printWidth": 120,
 | 
				
			||||||
    "arrowParens": "always"
 | 
					    "arrowParens": "always"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "browserslist": [
 | 
					  "browserslist": [
 | 
				
			||||||
| 
						 | 
					@ -56,8 +57,8 @@
 | 
				
			||||||
    "last 6 iOS versions"
 | 
					    "last 6 iOS versions"
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  "engines": {
 | 
					  "engines": {
 | 
				
			||||||
    "node": ">= 12.0.0",
 | 
					    "node": ">= 12.22.1",
 | 
				
			||||||
    "npm": ">= 6.13.4",
 | 
					    "npm": ">= 6.14.12",
 | 
				
			||||||
    "yarn": ">= 1.21.1"
 | 
					    "yarn": ">= 1.21.1"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					// You can add your plugins here
 | 
				
			||||||
 | 
					module.exports = [
 | 
				
			||||||
 | 
					//  '@flaschengeist/balance',
 | 
				
			||||||
 | 
					//  '@flaschengeist/schedule',
 | 
				
			||||||
 | 
					//  '@flaschengeist/pricelist',
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
| 
						 | 
					@ -9,11 +9,12 @@
 | 
				
			||||||
/* eslint-env node */
 | 
					/* eslint-env node */
 | 
				
			||||||
/* eslint-disable @typescript-eslint/no-var-requires */
 | 
					/* eslint-disable @typescript-eslint/no-var-requires */
 | 
				
			||||||
const ESLintPlugin = require('eslint-webpack-plugin')
 | 
					const ESLintPlugin = require('eslint-webpack-plugin')
 | 
				
			||||||
const { configure } = require('quasar/wrappers');
 | 
					const { ModifySourcePlugin } = require('modify-source-webpack-plugin')
 | 
				
			||||||
 | 
					const { configure } = require('quasar/wrappers')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = configure(function (/* ctx */) {
 | 
					module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    // https://quasar.dev/quasar-cli/supporting-ts
 | 
					 | 
				
			||||||
    // https://quasar.dev/quasar-cli/supporting-ts
 | 
					    // https://quasar.dev/quasar-cli/supporting-ts
 | 
				
			||||||
    supportTS: {
 | 
					    supportTS: {
 | 
				
			||||||
      tsCheckerConfig: {
 | 
					      tsCheckerConfig: {
 | 
				
			||||||
| 
						 | 
					@ -55,15 +56,14 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // transpile: false,
 | 
					      // transpile: false,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Add dependencies for transpiling with Babel (Array of string/regex)
 | 
					      // Add dependencies for transpiling with Babel (Array of string/regex)
 | 
				
			||||||
      // (from node_modules, which are by default not transpiled).
 | 
					      // (from node_modules, which are by default not transpiled).
 | 
				
			||||||
      // Applies only if "transpile" is set to true.
 | 
					      // Applies only if "transpile" is set to true.
 | 
				
			||||||
      // transpileDependencies: [],
 | 
					      // transpileDependencies: [],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // rtl: false, // https://quasar.dev/options/rtl-support
 | 
					      // rtl: false,
 | 
				
			||||||
      // preloadChunks: true,
 | 
					
 | 
				
			||||||
      // showProgress: false,
 | 
					 | 
				
			||||||
      // gzip: true,
 | 
					 | 
				
			||||||
      // analyze: true,
 | 
					      // analyze: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Options below are automatically set depending on the env, set them if you want to override
 | 
					      // Options below are automatically set depending on the env, set them if you want to override
 | 
				
			||||||
| 
						 | 
					@ -77,7 +77,23 @@ module.exports = configure(function (/* ctx */) {
 | 
				
			||||||
            extensions: [ 'ts', 'js', 'vue' ],
 | 
					            extensions: [ 'ts', 'js', 'vue' ],
 | 
				
			||||||
            exclude: 'node_modules'
 | 
					            exclude: 'node_modules'
 | 
				
			||||||
          }])
 | 
					          }])
 | 
				
			||||||
        },
 | 
					        chain.plugin('modify-source-webpack-plugin')
 | 
				
			||||||
 | 
					          .use(ModifySourcePlugin, [{
 | 
				
			||||||
 | 
					            rules: [
 | 
				
			||||||
 | 
					              {
 | 
				
			||||||
 | 
					                test: /plugins\.ts$/,
 | 
				
			||||||
 | 
					                modify: (src, filename) => {
 | 
				
			||||||
 | 
					                  const custom_plgns = require('./plugin.config.js')
 | 
				
			||||||
 | 
					                  const required_plgns = require('./src/vendor-plugin.config.js')
 | 
				
			||||||
 | 
					                  return src.replace(/\/\* *INSERT_PLUGIN_LIST *\*\//, 
 | 
				
			||||||
 | 
					                  [...custom_plgns, ...required_plgns].map(v => `import("${v}").then(v => success(v)).catch(() => failure("${v}"))`)
 | 
				
			||||||
 | 
					                    .join(','))
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					          }])
 | 
				
			||||||
 | 
					        //chain.resolve.alias.set('flaschengeist', '.')
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +0,0 @@
 | 
				
			||||||
{
 | 
					 | 
				
			||||||
  "@quasar/qcalendar": {}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,8 @@
 | 
				
			||||||
import config from 'src/config';
 | 
					import { useMainStore, api } from '@flaschengeist/api';
 | 
				
			||||||
import { boot } from 'quasar/wrappers';
 | 
					 | 
				
			||||||
import { LocalStorage, Notify } from 'quasar';
 | 
					import { LocalStorage, Notify } from 'quasar';
 | 
				
			||||||
import axios, { AxiosError } from 'axios';
 | 
					import { AxiosError } from 'axios';
 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					import { boot } from 'quasar/wrappers';
 | 
				
			||||||
 | 
					import config from 'src/config';
 | 
				
			||||||
const api = axios.create();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default boot(({ router }) => {
 | 
					export default boot(({ router }) => {
 | 
				
			||||||
  api.defaults.baseURL = LocalStorage.getItem<string>('baseURL') || config.baseURL;
 | 
					  api.defaults.baseURL = LocalStorage.getItem<string>('baseURL') || config.baseURL;
 | 
				
			||||||
| 
						 | 
					@ -22,7 +20,7 @@ export default boot(({ router }) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /***
 | 
					  /***
 | 
				
			||||||
   * Intercept responses
 | 
					   * Intercept responses
 | 
				
			||||||
   *   - filter 401 --> logout
 | 
					   *   - filter 401 --> handleLoggedOut
 | 
				
			||||||
   *   - filter timeout or 502-504 --> backendOffline
 | 
					   *   - filter timeout or 502-504 --> backendOffline
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  api.interceptors.response.use(
 | 
					  api.interceptors.response.use(
 | 
				
			||||||
| 
						 | 
					@ -45,7 +43,7 @@ export default boot(({ router }) => {
 | 
				
			||||||
            query: { redirect: next },
 | 
					            query: { redirect: next },
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
        } else if (e.response && e.response.status == 401) {
 | 
					        } else if (e.response && e.response.status == 401) {
 | 
				
			||||||
          void store.logout();
 | 
					          void store.handleLoggedOut();
 | 
				
			||||||
          if (current.name !== 'login') {
 | 
					          if (current.name !== 'login') {
 | 
				
			||||||
            await router.push({
 | 
					            await router.push({
 | 
				
			||||||
              name: 'login',
 | 
					              name: 'login',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,5 @@
 | 
				
			||||||
 | 
					import { useMainStore, hasPermissions } from '@flaschengeist/api';
 | 
				
			||||||
import { boot } from 'quasar/wrappers';
 | 
					import { boot } from 'quasar/wrappers';
 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
import { hasPermissions } from 'src/utils/permission';
 | 
					 | 
				
			||||||
import { RouteRecord } from 'vue-router';
 | 
					import { RouteRecord } from 'vue-router';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default boot(({ router }) => {
 | 
					export default boot(({ router }) => {
 | 
				
			||||||
| 
						 | 
					@ -13,7 +12,7 @@ export default boot(({ router }) => {
 | 
				
			||||||
      // Secured area (LOGIN REQUIRED)
 | 
					      // Secured area (LOGIN REQUIRED)
 | 
				
			||||||
      // Check login is ok
 | 
					      // Check login is ok
 | 
				
			||||||
      if (!store.session || store.session.expires <= new Date()) {
 | 
					      if (!store.session || store.session.expires <= new Date()) {
 | 
				
			||||||
        void store.logout();
 | 
					        void store.handleLoggedOut();
 | 
				
			||||||
        return next({ name: 'login', query: { redirect: to.fullPath } });
 | 
					        return next({ name: 'login', query: { redirect: to.fullPath } });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,46 +1,47 @@
 | 
				
			||||||
import { boot } from 'quasar/wrappers';
 | 
					import { Notify } from 'quasar';
 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					 | 
				
			||||||
import routes from 'src/router/routes';
 | 
					 | 
				
			||||||
import { api } from 'boot/axios';
 | 
					import { api } from 'boot/axios';
 | 
				
			||||||
 | 
					import { boot } from 'quasar/wrappers';
 | 
				
			||||||
 | 
					import routes from 'src/router/routes';
 | 
				
			||||||
import { AxiosResponse } from 'axios';
 | 
					import { AxiosResponse } from 'axios';
 | 
				
			||||||
import { RouteRecordRaw } from 'vue-router';
 | 
					import { RouteRecordRaw } from 'vue-router';
 | 
				
			||||||
import { Notify } from 'quasar';
 | 
					import { FG_Plugin } from '@flaschengeist/types';
 | 
				
			||||||
 | 
					 | 
				
			||||||
const config: { [key: string]: Array<string> } = {
 | 
					 | 
				
			||||||
  // Do not change required Modules !!
 | 
					 | 
				
			||||||
  requiredModules: ['User'],
 | 
					 | 
				
			||||||
  // here you can import plugins.
 | 
					 | 
				
			||||||
  loadModules: ['Balance', 'Schedule', 'Pricelist'],
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* Stop!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// do not change anything here !!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// You can not even read? I said stop!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Really are you stupid? Stop scrolling down here!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Every line you scroll down, an unicorn will die painfully!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Ok you must hate unicorns... But what if I say you I joked... Baby otters will die!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
         .-"""-.
 | 
					 | 
				
			||||||
        /      o\
 | 
					 | 
				
			||||||
       |    o   0).-.
 | 
					 | 
				
			||||||
       |       .-;(_/     .-.
 | 
					 | 
				
			||||||
        \     /  /)).---._|  `\   ,
 | 
					 | 
				
			||||||
         '.  '  /((       `'-./ _/|
 | 
					 | 
				
			||||||
           \  .'  )        .-.;`  /
 | 
					 | 
				
			||||||
            '.             |  `\-'
 | 
					 | 
				
			||||||
              '._        -'    /
 | 
					 | 
				
			||||||
                 ``""--`------`
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/****************************************************
 | 
					/****************************************************
 | 
				
			||||||
 ******** Internal area for some magic **************
 | 
					 ******** Internal area for some magic **************
 | 
				
			||||||
 ****************************************************/
 | 
					 ****************************************************/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare type ImportPlgn = { default: FG_Plugin.Plugin };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function validatePlugin(plugin: FG_Plugin.Plugin) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    typeof plugin.name === 'string' &&
 | 
				
			||||||
 | 
					    typeof plugin.id === 'string' &&
 | 
				
			||||||
 | 
					    plugin.id.length > 0 &&
 | 
				
			||||||
 | 
					    typeof plugin.version === 'string'
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* eslint-disable */
 | 
				
			||||||
 | 
					// This functions are used by webpack magic
 | 
				
			||||||
 | 
					// Called when import promise resolved
 | 
				
			||||||
 | 
					function success(value: ImportPlgn, path: string) {
 | 
				
			||||||
 | 
					  if (validatePlugin(value.default)) PLUGINS.plugins.set(value.default.id, value.default);
 | 
				
			||||||
 | 
					  else failure(path);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					// Called when import promise rejected
 | 
				
			||||||
 | 
					function failure(path = 'unknown') {
 | 
				
			||||||
 | 
					  console.error(`Plugin ${path} could not be found and not imported`);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/* eslint-enable */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Here does some magic happens, WebPack will automatically replace the following comment with the import statements
 | 
				
			||||||
 | 
					const PLUGINS = {
 | 
				
			||||||
 | 
					  context: <Array<() => Promise<ImportPlgn>>>[
 | 
				
			||||||
 | 
					    /*INSERT_PLUGIN_LIST*/
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  plugins: new Map<string, FG_Plugin.Plugin>(),
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface BackendPlugin {
 | 
					interface BackendPlugin {
 | 
				
			||||||
  permissions: string[];
 | 
					  permissions: string[];
 | 
				
			||||||
  version: string;
 | 
					  version: string;
 | 
				
			||||||
| 
						 | 
					@ -221,46 +222,27 @@ function combineShortcuts(target: FG_Plugin.Shortcut[], source: FG_Plugin.MenuRo
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Load a Flaschengeist plugin
 | 
					 * Load a Flaschengeist plugin
 | 
				
			||||||
 * @param loadedPlugins Flaschgeist object
 | 
					 * @param loadedPlugins Flaschgeist object
 | 
				
			||||||
 * @param pluginName Plugin to load
 | 
					 * @param plugin Plugin to load
 | 
				
			||||||
 * @param context RequireContext of plugins
 | 
					 | 
				
			||||||
 * @param router VueRouter instance
 | 
					 * @param router VueRouter instance
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function loadPlugin(
 | 
					function loadPlugin(
 | 
				
			||||||
  loadedPlugins: FG_Plugin.Flaschengeist,
 | 
					  loadedPlugins: FG_Plugin.Flaschengeist,
 | 
				
			||||||
  pluginName: string,
 | 
					  plugin: FG_Plugin.Plugin,
 | 
				
			||||||
  context: __WebpackModuleApi.RequireContext,
 | 
					 | 
				
			||||||
  backend: Backend
 | 
					  backend: Backend
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  // Check if already loaded
 | 
					  // Check if already loaded
 | 
				
			||||||
  if (loadedPlugins.plugins.findIndex((p) => p.name === pluginName) !== -1) return true;
 | 
					  if (loadedPlugins.plugins.findIndex((p) => p.name === plugin.name) !== -1) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Search if plugin is installed
 | 
					  // Check backend dependencies
 | 
				
			||||||
  const available = context.keys();
 | 
					 | 
				
			||||||
  const plugin = available.includes(`./${pluginName.toLowerCase()}/plugin.ts`)
 | 
					 | 
				
			||||||
    ? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
 | 
					 | 
				
			||||||
      <FG_Plugin.Plugin>context(`./${pluginName.toLowerCase()}/plugin.ts`).default
 | 
					 | 
				
			||||||
    : undefined;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!plugin) {
 | 
					 | 
				
			||||||
    // Plugin is not found, results in an error
 | 
					 | 
				
			||||||
    console.exception(`Could not find required Plugin ${pluginName}`);
 | 
					 | 
				
			||||||
    return false;
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    // Plugin found. Check backend dependencies
 | 
					 | 
				
			||||||
  if (
 | 
					  if (
 | 
				
			||||||
      !plugin.requiredBackendModules.every((required) => backend.plugins[required] !== undefined)
 | 
					    !plugin.requiredModules.every(
 | 
				
			||||||
    ) {
 | 
					      (required) =>
 | 
				
			||||||
      console.error(`Plugin ${pluginName}: Backend modules not satisfied`);
 | 
					        backend.plugins[required[0]] !== undefined &&
 | 
				
			||||||
      return false;
 | 
					        (required.length == 1 ||
 | 
				
			||||||
    }
 | 
					          true) /* validate the version, semver440 from python is... tricky on node*/
 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Check frontend dependencies
 | 
					 | 
				
			||||||
    if (
 | 
					 | 
				
			||||||
      !plugin.requiredModules.every((required) =>
 | 
					 | 
				
			||||||
        loadPlugin(loadedPlugins, required, context, backend)
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
      console.error(`Plugin ${pluginName}: Backend modules not satisfied`);
 | 
					    console.error(`Plugin ${plugin.name}: Backend modules not satisfied`);
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -294,8 +276,7 @@ function loadPlugin(
 | 
				
			||||||
    notification: plugin.notification?.bind({}) || translateNotification,
 | 
					    notification: plugin.notification?.bind({}) || translateNotification,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return plugin;
 | 
					  return true;
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
| 
						 | 
					@ -317,8 +298,9 @@ async function getBackend() {
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export default boot(async ({ router, app }) => {
 | 
					export default boot(async ({ router, app }) => {
 | 
				
			||||||
  const backend = await getBackend();
 | 
					  const backend = await getBackend();
 | 
				
			||||||
  if (backend === null) {
 | 
					  if (!backend || typeof backend !== 'object' || !('plugins' in backend)) {
 | 
				
			||||||
    void router.push({ name: 'error' });
 | 
					    console.log('Backend error');
 | 
				
			||||||
 | 
					    router.isReady().finally(() => void router.push({ name: 'offline', params: { refresh: 1 } }));
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -331,39 +313,23 @@ export default boot(async ({ router, app }) => {
 | 
				
			||||||
    widgets: [],
 | 
					    widgets: [],
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // get all plugins
 | 
					  const BreakError = {};
 | 
				
			||||||
  const pluginsContext = require.context('src/plugins', true, /.+\/plugin.ts$/);
 | 
					  try {
 | 
				
			||||||
 | 
					    PLUGINS.plugins.forEach((plugin, name) => {
 | 
				
			||||||
  // Start loading plugins
 | 
					      if (!loadPlugin(loadedPlugins, plugin, backend)) {
 | 
				
			||||||
  // Load required modules, if not found or error when loading this will forward the user to the error page
 | 
					 | 
				
			||||||
  config.requiredModules.forEach((required) => {
 | 
					 | 
				
			||||||
    const plugin = loadPlugin(loadedPlugins, required, pluginsContext, backend);
 | 
					 | 
				
			||||||
    if (!plugin) {
 | 
					 | 
				
			||||||
        void router.push({ name: 'error' });
 | 
					        void router.push({ name: 'error' });
 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Load user defined plugins
 | 
					 | 
				
			||||||
  // If there is an error with loading a plugin, the user will get informed.
 | 
					 | 
				
			||||||
  const failed: string[] = [];
 | 
					 | 
				
			||||||
  config.loadModules.forEach((required) => {
 | 
					 | 
				
			||||||
    const plugin = loadPlugin(loadedPlugins, required, pluginsContext, backend);
 | 
					 | 
				
			||||||
    if (!plugin) {
 | 
					 | 
				
			||||||
      failed.push(required);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  if (failed.length > 0) {
 | 
					 | 
				
			||||||
    // Log failed plugins
 | 
					 | 
				
			||||||
    console.error('Could not load all plugins', failed);
 | 
					 | 
				
			||||||
    // Inform user about error
 | 
					 | 
				
			||||||
        Notify.create({
 | 
					        Notify.create({
 | 
				
			||||||
          type: 'negative',
 | 
					          type: 'negative',
 | 
				
			||||||
      message:
 | 
					          message: `Fehler beim Laden: Bitte wende dich an den Admin (error: PNF-${name}!`,
 | 
				
			||||||
        'Fehler beim Laden: Nicht alle Funktionen stehen zur Verfügung. Bitte wende dich an den Admin!',
 | 
					 | 
				
			||||||
          timeout: 10000,
 | 
					          timeout: 10000,
 | 
				
			||||||
          progress: true,
 | 
					          progress: true,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					        throw BreakError;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    if (e !== BreakError) throw e;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Sort widgets by priority
 | 
					  // Sort widgets by priority
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,8 @@
 | 
				
			||||||
import { createPinia, Pinia } from 'pinia';
 | 
					import { useMainStore, pinia } from '@flaschengeist/api';
 | 
				
			||||||
import { boot } from 'quasar/wrappers';
 | 
					import { boot } from 'quasar/wrappers';
 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
import { ref } from 'vue';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const pinia = ref<Pinia>();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default boot(({ app }) => {
 | 
					export default boot(({ app }) => {
 | 
				
			||||||
  pinia.value = createPinia();
 | 
					  app.use(pinia);
 | 
				
			||||||
  app.use(pinia.value);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const store = useMainStore();
 | 
					  const store = useMainStore();
 | 
				
			||||||
  void store.init();
 | 
					  void store.init();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,8 +34,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { defineComponent, PropType, computed } from 'vue';
 | 
					import { defineComponent, PropType, computed } from 'vue';
 | 
				
			||||||
import { formatDateTime } from 'src/utils/datetime';
 | 
					import { formatDateTime } from '@flaschengeist/api';
 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					import { FG_Plugin } from '@flaschengeist/types';
 | 
				
			||||||
import { useRouter } from 'vue-router';
 | 
					import { useRouter } from 'vue-router';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,17 +1,23 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <q-expansion-item v-if="isGranted(entry)" clickable tag="a" target="self" :label='title' :icon='entry.icon' expand-separator>
 | 
					  <q-expansion-item
 | 
				
			||||||
      <q-list class='q-ml-lg'>
 | 
					    v-if="isGranted(entry)"
 | 
				
			||||||
        <div v-for='child in entry.children' :key='child.link'>
 | 
					    clickable
 | 
				
			||||||
    <q-item v-if='isGranted(child)' clickable :to='{name: child.link}'>
 | 
					    :label="getTitle(entry)"
 | 
				
			||||||
 | 
					    :icon="entry.icon"
 | 
				
			||||||
 | 
					    expand-separator
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <q-list class="q-ml-lg">
 | 
				
			||||||
 | 
					      <div v-for="child in entry.children" :key="child.link">
 | 
				
			||||||
 | 
					        <q-item v-if="isGranted(child)" clickable :to="{ name: child.link }">
 | 
				
			||||||
          <q-menu context-menu>
 | 
					          <q-menu context-menu>
 | 
				
			||||||
        <q-btn v-close-popup label='Verknüpfung erstellen' dense @click='addShortCut(child)'/>
 | 
					            <q-btn v-close-popup label="Verknüpfung erstellen" dense @click="addShortCut(child)" />
 | 
				
			||||||
          </q-menu>
 | 
					          </q-menu>
 | 
				
			||||||
          <q-item-section avatar>
 | 
					          <q-item-section avatar>
 | 
				
			||||||
        <q-icon :name='child.icon' />
 | 
					            <q-icon :name="child.icon" />
 | 
				
			||||||
          </q-item-section>
 | 
					          </q-item-section>
 | 
				
			||||||
          <q-item-section>
 | 
					          <q-item-section>
 | 
				
			||||||
            <q-item-label>
 | 
					            <q-item-label>
 | 
				
			||||||
          {{child.title}}
 | 
					              {{ getTitle(child) }}
 | 
				
			||||||
            </q-item-label>
 | 
					            </q-item-label>
 | 
				
			||||||
          </q-item-section>
 | 
					          </q-item-section>
 | 
				
			||||||
        </q-item>
 | 
					        </q-item>
 | 
				
			||||||
| 
						 | 
					@ -22,8 +28,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { computed, defineComponent, PropType } from 'vue';
 | 
					import { computed, defineComponent, PropType } from 'vue';
 | 
				
			||||||
import { hasPermissions } from 'src/utils/permission';
 | 
					import { hasPermissions } from '@flaschengeist/api';
 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					import { FG_Plugin } from '@flaschengeist/types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
  name: 'EssentialExpansionLink',
 | 
					  name: 'EssentialExpansionLink',
 | 
				
			||||||
| 
						 | 
					@ -38,16 +44,18 @@ export default defineComponent({
 | 
				
			||||||
    addShortCut: (val: FG_Plugin.MenuLink) => val.link,
 | 
					    addShortCut: (val: FG_Plugin.MenuLink) => val.link,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  setup(props, { emit }) {
 | 
					  setup(props, { emit }) {
 | 
				
			||||||
 | 
					    function isGranted(val: FG_Plugin.MenuLink) {
 | 
				
			||||||
    function isGranted(val: FG_Plugin.MenuLink) { return hasPermissions(val.permissions || [])};
 | 
					      return computed(() => hasPermissions(val.permissions || []));
 | 
				
			||||||
    const title = computed(() =>
 | 
					    }
 | 
				
			||||||
      typeof props.entry.title === 'object' ? props.entry.title.value : props.entry.title
 | 
					    function getTitle(entry: FG_Plugin.MenuLink) {
 | 
				
			||||||
    );
 | 
					      return computed(() => (typeof entry.title === 'function' ? entry.title() : entry.title)).value;
 | 
				
			||||||
    function addShortCut(val: FG_Plugin.MenuLink) {
 | 
					 | 
				
			||||||
      emit('addShortCut', val)
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return { isGranted, title, addShortCut};
 | 
					    function addShortCut(val: FG_Plugin.MenuLink) {
 | 
				
			||||||
 | 
					      emit('addShortCut', val);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return { isGranted, getTitle, addShortCut };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,8 +12,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { computed, defineComponent, PropType } from 'vue';
 | 
					import { computed, defineComponent, PropType } from 'vue';
 | 
				
			||||||
import { hasPermissions } from 'src/utils/permission';
 | 
					import { hasPermissions } from '@flaschengeist/api';
 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					import { FG_Plugin } from '@flaschengeist/types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
  name: 'EssentialLink',
 | 
					  name: 'EssentialLink',
 | 
				
			||||||
| 
						 | 
					@ -27,7 +27,7 @@ export default defineComponent({
 | 
				
			||||||
  setup(props) {
 | 
					  setup(props) {
 | 
				
			||||||
    const isGranted = computed(() => hasPermissions(props.entry.permissions || []));
 | 
					    const isGranted = computed(() => hasPermissions(props.entry.permissions || []));
 | 
				
			||||||
    const title = computed(() =>
 | 
					    const title = computed(() =>
 | 
				
			||||||
      typeof props.entry.title === 'object' ? props.entry.title.value : props.entry.title
 | 
					      typeof props.entry.title === 'function' ? props.entry.title() : props.entry.title
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    return { isGranted, title };
 | 
					    return { isGranted, title };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,8 +8,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { computed, defineComponent, PropType } from 'vue';
 | 
					import { computed, defineComponent, PropType } from 'vue';
 | 
				
			||||||
import { hasPermissions } from 'src/utils/permission';
 | 
					import { hasPermissions } from '@flaschengeist/api';
 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					import { FG_Plugin } from '@flaschengeist/types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
  name: 'ShortcutLink',
 | 
					  name: 'ShortcutLink',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,137 +0,0 @@
 | 
				
			||||||
declare namespace FG {
 | 
					 | 
				
			||||||
  interface Notification {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    plugin: string;
 | 
					 | 
				
			||||||
    text: string;
 | 
					 | 
				
			||||||
    data?: unknown;
 | 
					 | 
				
			||||||
    time: Date;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface User {
 | 
					 | 
				
			||||||
    userid: string;
 | 
					 | 
				
			||||||
    display_name: string;
 | 
					 | 
				
			||||||
    firstname: string;
 | 
					 | 
				
			||||||
    lastname: string;
 | 
					 | 
				
			||||||
    mail: string;
 | 
					 | 
				
			||||||
    birthday?: Date;
 | 
					 | 
				
			||||||
    roles: Array<string>;
 | 
					 | 
				
			||||||
    permissions?: Array<string>;
 | 
					 | 
				
			||||||
    avatar_url?: string;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface Session {
 | 
					 | 
				
			||||||
    expires: Date;
 | 
					 | 
				
			||||||
    token: string;
 | 
					 | 
				
			||||||
    lifetime: number;
 | 
					 | 
				
			||||||
    browser: string;
 | 
					 | 
				
			||||||
    platform: string;
 | 
					 | 
				
			||||||
    userid: string;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  type Permission = string;
 | 
					 | 
				
			||||||
  interface Role {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    name: string;
 | 
					 | 
				
			||||||
    permissions: Array<Permission>;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface Transaction {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    time: Date;
 | 
					 | 
				
			||||||
    amount: number;
 | 
					 | 
				
			||||||
    reversal_id?: number;
 | 
					 | 
				
			||||||
    author_id?: string;
 | 
					 | 
				
			||||||
    sender_id?: string;
 | 
					 | 
				
			||||||
    original_id?: number;
 | 
					 | 
				
			||||||
    receiver_id?: string;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface Event {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    start: Date;
 | 
					 | 
				
			||||||
    end?: Date;
 | 
					 | 
				
			||||||
    name?: string;
 | 
					 | 
				
			||||||
    description?: string;
 | 
					 | 
				
			||||||
    type: EventType | number;
 | 
					 | 
				
			||||||
    is_template: boolean;
 | 
					 | 
				
			||||||
    jobs: Array<Job>;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface EventType {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    name: string;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface Invite {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    job_id: number;
 | 
					 | 
				
			||||||
    invitee_id: string;
 | 
					 | 
				
			||||||
    sender_id: string;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface Job {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    start: Date;
 | 
					 | 
				
			||||||
    end?: Date;
 | 
					 | 
				
			||||||
    type: JobType | number;
 | 
					 | 
				
			||||||
    comment?: string;
 | 
					 | 
				
			||||||
    services: Array<Service>;
 | 
					 | 
				
			||||||
    required_services: number;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface JobType {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    name: string;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface Service {
 | 
					 | 
				
			||||||
    userid: string;
 | 
					 | 
				
			||||||
    is_backup: boolean;
 | 
					 | 
				
			||||||
    value: number;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface Drink {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    article_id?: string;
 | 
					 | 
				
			||||||
    package_size?: number;
 | 
					 | 
				
			||||||
    name: string;
 | 
					 | 
				
			||||||
    volume?: number;
 | 
					 | 
				
			||||||
    cost_per_volume?: number;
 | 
					 | 
				
			||||||
    cost_per_package?: number;
 | 
					 | 
				
			||||||
    tags?: Array<Tag>;
 | 
					 | 
				
			||||||
    type?: DrinkType;
 | 
					 | 
				
			||||||
    volumes: Array<DrinkPriceVolume>;
 | 
					 | 
				
			||||||
    uuid: string;
 | 
					 | 
				
			||||||
    receipt?: Array<string>;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface DrinkIngredient {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    volume: number;
 | 
					 | 
				
			||||||
    ingredient_id: number;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface DrinkPrice {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    price: number;
 | 
					 | 
				
			||||||
    public: boolean;
 | 
					 | 
				
			||||||
    description?: string;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface DrinkPriceVolume {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    volume: number;
 | 
					 | 
				
			||||||
    min_prices: Array<MinPrices>;
 | 
					 | 
				
			||||||
    prices: Array<DrinkPrice>;
 | 
					 | 
				
			||||||
    ingredients: Array<Ingredient>;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface DrinkType {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    name: string;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface ExtraIngredient {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    name: string;
 | 
					 | 
				
			||||||
    price: number;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface Ingredient {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    drink_ingredient?: DrinkIngredient;
 | 
					 | 
				
			||||||
    extra_ingredient?: ExtraIngredient;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface MinPrices {
 | 
					 | 
				
			||||||
    percentage: number;
 | 
					 | 
				
			||||||
    price: number;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  interface Tag {
 | 
					 | 
				
			||||||
    id: number;
 | 
					 | 
				
			||||||
    name: string;
 | 
					 | 
				
			||||||
    color: string;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -52,14 +52,12 @@
 | 
				
			||||||
      @click.capture="openMenu"
 | 
					      @click.capture="openMenu"
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <!-- Plugins -->
 | 
					      <!-- Plugins -->
 | 
				
			||||||
      <q-list>
 | 
					 | 
				
			||||||
      <essential-expansion-link
 | 
					      <essential-expansion-link
 | 
				
			||||||
        v-for="(entry, index) in mainLinks"
 | 
					        v-for="(entry, index) in mainLinks"
 | 
				
			||||||
        :key="'plugin' + index"
 | 
					        :key="'plugin' + index"
 | 
				
			||||||
        :entry="entry"
 | 
					        :entry="entry"
 | 
				
			||||||
        @add-short-cut="addShortcut"
 | 
					        @add-short-cut="addShortcut"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      </q-list>
 | 
					 | 
				
			||||||
      <q-separator />
 | 
					      <q-separator />
 | 
				
			||||||
      <essential-link
 | 
					      <essential-link
 | 
				
			||||||
        v-for="(entry, index) in essentials"
 | 
					        v-for="(entry, index) in essentials"
 | 
				
			||||||
| 
						 | 
					@ -74,6 +72,7 @@
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import EssentialExpansionLink from 'components/navigation/EssentialExpansionLink.vue';
 | 
				
			||||||
import EssentialLink from 'src/components/navigation/EssentialLink.vue';
 | 
					import EssentialLink from 'src/components/navigation/EssentialLink.vue';
 | 
				
			||||||
import ShortcutLink from 'src/components/navigation/ShortcutLink.vue';
 | 
					import ShortcutLink from 'src/components/navigation/ShortcutLink.vue';
 | 
				
			||||||
import Notification from 'src/components/Notification.vue';
 | 
					import Notification from 'src/components/Notification.vue';
 | 
				
			||||||
| 
						 | 
					@ -86,14 +85,13 @@ import {
 | 
				
			||||||
  onBeforeUnmount,
 | 
					  onBeforeUnmount,
 | 
				
			||||||
  ComponentPublicInstance,
 | 
					  ComponentPublicInstance,
 | 
				
			||||||
} from 'vue';
 | 
					} from 'vue';
 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					 | 
				
			||||||
import { useRouter } from 'vue-router';
 | 
					 | 
				
			||||||
import { Screen } from 'quasar';
 | 
					import { Screen } from 'quasar';
 | 
				
			||||||
import config from 'src/config';
 | 
					import config from 'src/config';
 | 
				
			||||||
import EssentialExpansionLink from 'components/navigation/EssentialExpansionLink.vue';
 | 
					import { useRouter } from 'vue-router';
 | 
				
			||||||
import draggable from 'vuedraggable';
 | 
					import { useMainStore } from '@flaschengeist/api';
 | 
				
			||||||
const drag: ComponentPublicInstance = <ComponentPublicInstance>draggable;
 | 
					import { FG_Plugin } from '@flaschengeist/types';
 | 
				
			||||||
 | 
					import drag from 'vuedraggable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const essentials: FG_Plugin.MenuLink[] = [
 | 
					const essentials: FG_Plugin.MenuLink[] = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    title: 'Über Flaschengeist',
 | 
					    title: 'Über Flaschengeist',
 | 
				
			||||||
| 
						 | 
					@ -104,7 +102,13 @@ const essentials: FG_Plugin.MenuLink[] = [
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
  name: 'MainLayout',
 | 
					  name: 'MainLayout',
 | 
				
			||||||
  components: { EssentialExpansionLink, EssentialLink, ShortcutLink, Notification, drag },
 | 
					  components: {
 | 
				
			||||||
 | 
					    EssentialExpansionLink,
 | 
				
			||||||
 | 
					    EssentialLink,
 | 
				
			||||||
 | 
					    ShortcutLink,
 | 
				
			||||||
 | 
					    Notification,
 | 
				
			||||||
 | 
					    drag: <ComponentPublicInstance>drag,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  setup() {
 | 
					  setup() {
 | 
				
			||||||
    const router = useRouter();
 | 
					    const router = useRouter();
 | 
				
			||||||
    const mainStore = useMainStore();
 | 
					    const mainStore = useMainStore();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,8 +25,8 @@
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					 | 
				
			||||||
import { defineComponent, inject } from 'vue';
 | 
					import { defineComponent, inject } from 'vue';
 | 
				
			||||||
 | 
					import { FG_Plugin } from '@flaschengeist/types';
 | 
				
			||||||
import ShortcutLink from 'components/navigation/ShortcutLink.vue';
 | 
					import ShortcutLink from 'components/navigation/ShortcutLink.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,8 +12,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { computed, defineComponent, inject } from 'vue';
 | 
					import { computed, defineComponent, inject } from 'vue';
 | 
				
			||||||
import { hasPermissions } from 'src/utils/permission';
 | 
					import { hasPermissions } from '@flaschengeist/api';
 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					import { FG_Plugin } from '@flaschengeist/types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
  name: 'Dashboard',
 | 
					  name: 'Dashboard',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,14 +44,14 @@
 | 
				
			||||||
      </q-card-section>
 | 
					      </q-card-section>
 | 
				
			||||||
      <div class="row justify-end">
 | 
					      <div class="row justify-end">
 | 
				
			||||||
        <q-btn
 | 
					        <q-btn
 | 
				
			||||||
          v-if="$q.platform.is.cordova || $q.platform.is.electron"
 | 
					          v-if="quasar.platform.is.cordova || quasar.platform.is.electron"
 | 
				
			||||||
          flat
 | 
					          flat
 | 
				
			||||||
          round
 | 
					          round
 | 
				
			||||||
          icon="mdi-menu-down"
 | 
					          icon="mdi-menu-down"
 | 
				
			||||||
          @click="openServerSettings"
 | 
					          @click="openServerSettings"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <q-slide-transition v-if="$q.platform.is.cordova || $q.platform.is.electron">
 | 
					      <q-slide-transition v-if="quasar.platform.is.cordova || quasar.platform.is.electron">
 | 
				
			||||||
        <div v-show="visible">
 | 
					        <div v-show="visible">
 | 
				
			||||||
          <q-separator />
 | 
					          <q-separator />
 | 
				
			||||||
          <q-card-section>
 | 
					          <q-card-section>
 | 
				
			||||||
| 
						 | 
					@ -70,12 +70,10 @@
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { useRouter } from 'vue-router';
 | 
					import { useRouter } from 'vue-router';
 | 
				
			||||||
import { Loading, Notify } from 'quasar';
 | 
					import { Loading, Notify } from 'quasar';
 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					import { notEmpty, useMainStore, useUserStore } from '@flaschengeist/api';
 | 
				
			||||||
 | 
					import { PasswordInput } from '@flaschengeist/api/components';
 | 
				
			||||||
import { defineComponent, ref } from 'vue';
 | 
					import { defineComponent, ref } from 'vue';
 | 
				
			||||||
import { setBaseURL, api } from 'boot/axios';
 | 
					import { setBaseURL, api } from 'boot/axios';
 | 
				
			||||||
import { notEmpty } from 'src/utils/validators';
 | 
					 | 
				
			||||||
import { useUserStore } from 'src/plugins/user/store';
 | 
					 | 
				
			||||||
import PasswordInput from 'src/components/utils/PasswordInput.vue';
 | 
					 | 
				
			||||||
import { useQuasar } from 'quasar';
 | 
					import { useQuasar } from 'quasar';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
| 
						 | 
					@ -83,6 +81,7 @@ export default defineComponent({
 | 
				
			||||||
  components: { PasswordInput },
 | 
					  components: { PasswordInput },
 | 
				
			||||||
  setup() {
 | 
					  setup() {
 | 
				
			||||||
    const mainStore = useMainStore();
 | 
					    const mainStore = useMainStore();
 | 
				
			||||||
 | 
					    const userStore = useUserStore();
 | 
				
			||||||
    const mainRoute = { name: 'dashboard' };
 | 
					    const mainRoute = { name: 'dashboard' };
 | 
				
			||||||
    const router = useRouter();
 | 
					    const router = useRouter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -91,7 +90,7 @@ export default defineComponent({
 | 
				
			||||||
    const password = ref('');
 | 
					    const password = ref('');
 | 
				
			||||||
    const server = ref<string | undefined>(api.defaults.baseURL);
 | 
					    const server = ref<string | undefined>(api.defaults.baseURL);
 | 
				
			||||||
    const visible = ref(false);
 | 
					    const visible = ref(false);
 | 
				
			||||||
    const $q = useQuasar();
 | 
					    const quasar = useQuasar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function openServerSettings() {
 | 
					    function openServerSettings() {
 | 
				
			||||||
      visible.value = !visible.value;
 | 
					      visible.value = !visible.value;
 | 
				
			||||||
| 
						 | 
					@ -108,7 +107,7 @@ export default defineComponent({
 | 
				
			||||||
      const status = await mainStore.login(userid.value, password.value);
 | 
					      const status = await mainStore.login(userid.value, password.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (status === true) {
 | 
					      if (status === true) {
 | 
				
			||||||
        mainStore.user = (await useUserStore().getUser(userid.value, true)) || undefined;
 | 
					        mainStore.user = (await userStore.getUser(userid.value, true)) || undefined;
 | 
				
			||||||
        const x = router.currentRoute.value.query['redirect'];
 | 
					        const x = router.currentRoute.value.query['redirect'];
 | 
				
			||||||
        void router.push(typeof x === 'string' ? { path: x } : mainRoute);
 | 
					        void router.push(typeof x === 'string' ? { path: x } : mainRoute);
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
| 
						 | 
					@ -165,7 +164,7 @@ export default defineComponent({
 | 
				
			||||||
      server,
 | 
					      server,
 | 
				
			||||||
      userid,
 | 
					      userid,
 | 
				
			||||||
      visible,
 | 
					      visible,
 | 
				
			||||||
      $q,
 | 
					      quasar,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,9 +46,9 @@ export default defineComponent({
 | 
				
			||||||
    const ival = setInterval(() => {
 | 
					    const ival = setInterval(() => {
 | 
				
			||||||
      reload.value -= 1;
 | 
					      reload.value -= 1;
 | 
				
			||||||
      if (reload.value <= 0) {
 | 
					      if (reload.value <= 0) {
 | 
				
			||||||
 | 
					        if (router.currentRoute.value.params && 'refresh' in router.currentRoute.value.params)
 | 
				
			||||||
 | 
					          router.go(0);
 | 
				
			||||||
        const path = router.currentRoute.value.query.redirect;
 | 
					        const path = router.currentRoute.value.query.redirect;
 | 
				
			||||||
        console.log('Offline: ');
 | 
					 | 
				
			||||||
        console.log(path);
 | 
					 | 
				
			||||||
        void router.replace(path ? { path: <string>path } : { name: 'login' });
 | 
					        void router.replace(path ? { path: <string>path } : { name: 'login' });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }, 1000);
 | 
					    }, 1000);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,10 +34,10 @@
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import { useMainStore } from '@flaschengeist/api';
 | 
				
			||||||
import { useRouter } from 'vue-router';
 | 
					import { useRouter } from 'vue-router';
 | 
				
			||||||
import { Loading, Notify } from 'quasar';
 | 
					import { Loading, Notify } from 'quasar';
 | 
				
			||||||
import { defineComponent, ref } from 'vue';
 | 
					import { defineComponent, ref } from 'vue';
 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
  // name: 'PageName'
 | 
					  // name: 'PageName'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -60,8 +60,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { defineComponent, inject } from 'vue';
 | 
					import { defineComponent, inject } from 'vue';
 | 
				
			||||||
 | 
					import { FG_Plugin } from '@flaschengeist/types';
 | 
				
			||||||
import Developer from 'components/about/Developer.vue';
 | 
					import Developer from 'components/about/Developer.vue';
 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const developers = [
 | 
					const developers = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
| 
						 | 
					@ -69,8 +69,7 @@ const developers = [
 | 
				
			||||||
    lastname: 'Gröger',
 | 
					    lastname: 'Gröger',
 | 
				
			||||||
    club: 'Studentenclub Wu5 e.V.',
 | 
					    club: 'Studentenclub Wu5 e.V.',
 | 
				
			||||||
    job: 'Gründer von Flaschengeist; Maintainer',
 | 
					    job: 'Gründer von Flaschengeist; Maintainer',
 | 
				
			||||||
    pic:
 | 
					    pic: 'https://scontent-frt3-2.xx.fbcdn.net/v/t1.0-9/31768724_1663023210401956_3834323197281435648_n.jpg?_nc_cat=109&_nc_sid=09cbfe&_nc_ohc=jWvUfn_xJ9YAX_oJ3CE&_nc_ht=scontent-frt3-2.xx&oh=15249378051f1e27f8b15122effb5c4a&oe=5FAC6A17',
 | 
				
			||||||
      'https://scontent-frt3-2.xx.fbcdn.net/v/t1.0-9/31768724_1663023210401956_3834323197281435648_n.jpg?_nc_cat=109&_nc_sid=09cbfe&_nc_ohc=jWvUfn_xJ9YAX_oJ3CE&_nc_ht=scontent-frt3-2.xx&oh=15249378051f1e27f8b15122effb5c4a&oe=5FAC6A17',
 | 
					 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      'Eigentlich wöllte ich jetzt hier echt viel hinschreiben. Aber ich habe keinen Plan was. Früher war ich einfach nur Tim G. und habe für andere den Kaffe geholt. Unter anderen für Ferdinand Thiessen.',
 | 
					      'Eigentlich wöllte ich jetzt hier echt viel hinschreiben. Aber ich habe keinen Plan was. Früher war ich einfach nur Tim G. und habe für andere den Kaffe geholt. Unter anderen für Ferdinand Thiessen.',
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -78,8 +77,7 @@ const developers = [
 | 
				
			||||||
    firstname: 'Ferdinand',
 | 
					    firstname: 'Ferdinand',
 | 
				
			||||||
    lastname: 'Thiessen',
 | 
					    lastname: 'Thiessen',
 | 
				
			||||||
    club: 'Club Aquarium e.V.',
 | 
					    club: 'Club Aquarium e.V.',
 | 
				
			||||||
    pic:
 | 
					    pic: 'https://scontent-frx5-1.xx.fbcdn.net/v/t1.0-9/17022243_1418942461493397_9069541318944803902_n.jpg?_nc_cat=110&_nc_sid=174925&_nc_ohc=HjkSm8vcRW8AX8bTnJ8&_nc_ht=scontent-frx5-1.xx&oh=f09bd36525f3c6e55feaafb3b05b43d2&oe=5FAD432A',
 | 
				
			||||||
      'https://scontent-frx5-1.xx.fbcdn.net/v/t1.0-9/17022243_1418942461493397_9069541318944803902_n.jpg?_nc_cat=110&_nc_sid=174925&_nc_ohc=HjkSm8vcRW8AX8bTnJ8&_nc_ht=scontent-frx5-1.xx&oh=f09bd36525f3c6e55feaafb3b05b43d2&oe=5FAD432A',
 | 
					 | 
				
			||||||
    job: 'Backend-Developer; Co-Maintainer',
 | 
					    job: 'Backend-Developer; Co-Maintainer',
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      'Geiler Typ. Einfach mal so alles Aufgeräumt. Aufeinmal könnte man aus dem Code eine Dokumentation zaubern!',
 | 
					      'Geiler Typ. Einfach mal so alles Aufgeräumt. Aufeinmal könnte man aus dem Code eine Dokumentation zaubern!',
 | 
				
			||||||
| 
						 | 
					@ -90,8 +88,7 @@ const developers = [
 | 
				
			||||||
    club: 'Studentenclub Wu5 e.V.',
 | 
					    club: 'Studentenclub Wu5 e.V.',
 | 
				
			||||||
    job: 'Eigentlich Frontend-Developer',
 | 
					    job: 'Eigentlich Frontend-Developer',
 | 
				
			||||||
    description: 'Er findet sich langsam rein.',
 | 
					    description: 'Er findet sich langsam rein.',
 | 
				
			||||||
    pic:
 | 
					    pic: 'https://scontent-frt3-1.xx.fbcdn.net/v/t31.0-8/10363433_647611335326483_3447118968375865826_o.jpg?_nc_cat=104&_nc_sid=09cbfe&_nc_ohc=nWMgo-6Ih74AX_NiGUz&_nc_ht=scontent-frt3-1.xx&oh=f16d2edfe86f68d54900099087edb9c9&oe=5FAACFD4',
 | 
				
			||||||
      'https://scontent-frt3-1.xx.fbcdn.net/v/t31.0-8/10363433_647611335326483_3447118968375865826_o.jpg?_nc_cat=104&_nc_sid=09cbfe&_nc_ohc=nWMgo-6Ih74AX_NiGUz&_nc_ht=scontent-frt3-1.xx&oh=f16d2edfe86f68d54900099087edb9c9&oe=5FAACFD4',
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,117 +0,0 @@
 | 
				
			||||||
import { RouteLocationRaw, RouteRecordRaw, RouteRecordName } from 'vue-router';
 | 
					 | 
				
			||||||
import { Component, ComputedRef } from 'vue';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
declare namespace FG_Plugin {
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Interface defining a Flaschengeist plugin
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  interface Plugin {
 | 
					 | 
				
			||||||
    name: string;
 | 
					 | 
				
			||||||
    version: string;
 | 
					 | 
				
			||||||
    widgets: Widget[];
 | 
					 | 
				
			||||||
    /** Pther frontend modules needed for this plugin to work correctly */
 | 
					 | 
				
			||||||
    requiredModules: string[];
 | 
					 | 
				
			||||||
    /** Backend modules needed for this plugin to work correctly */
 | 
					 | 
				
			||||||
    requiredBackendModules: string[];
 | 
					 | 
				
			||||||
    /** Menu entries for authenticated users */
 | 
					 | 
				
			||||||
    innerRoutes?: MenuRoute[];
 | 
					 | 
				
			||||||
    /** Public menu entries (without authentification) */
 | 
					 | 
				
			||||||
    outerRoutes?: MenuRoute[];
 | 
					 | 
				
			||||||
    /** Routes without menu links, for internal usage */
 | 
					 | 
				
			||||||
    internalRoutes?: NamedRouteRecordRaw[];
 | 
					 | 
				
			||||||
    /** Handle notifications, defaults to boot/plugins.ts:translateNotification() */
 | 
					 | 
				
			||||||
    notification?(msg: FG.Notification): FG_Plugin.Notification;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Defines the loaded state of the Flaschengeist
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  interface Flaschengeist {
 | 
					 | 
				
			||||||
    /** All loaded plugins */
 | 
					 | 
				
			||||||
    plugins: LoadedPlugin[];
 | 
					 | 
				
			||||||
    /** All routes, combined from all plugins */
 | 
					 | 
				
			||||||
    routes: RouteRecordRaw[];
 | 
					 | 
				
			||||||
    /** All menu entries */
 | 
					 | 
				
			||||||
    menuLinks: MenuLink[];
 | 
					 | 
				
			||||||
    /** All inner shortcuts */
 | 
					 | 
				
			||||||
    shortcuts: Shortcut[];
 | 
					 | 
				
			||||||
    /** All outer shortcuts */
 | 
					 | 
				
			||||||
    outerShortcuts: Shortcut[];
 | 
					 | 
				
			||||||
    /** All widgets */
 | 
					 | 
				
			||||||
    widgets: Widget[];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Interface for a frontend notification
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  interface Notification extends FG.Notification {
 | 
					 | 
				
			||||||
    /** If set a button for accepting will be shown, this function will get called before deleting the notification */
 | 
					 | 
				
			||||||
    accept?(): Promise<void>;
 | 
					 | 
				
			||||||
    /** If set this function is called before the notification gets deleted */
 | 
					 | 
				
			||||||
    reject?(): Promise<void>;
 | 
					 | 
				
			||||||
    /** If set the notification text is interpreted as a link to this location */
 | 
					 | 
				
			||||||
    link?: RouteLocationRaw;
 | 
					 | 
				
			||||||
    /** If set this icon is used */
 | 
					 | 
				
			||||||
    icon?: string;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Loaded Flaschengeist plugin
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  interface LoadedPlugin {
 | 
					 | 
				
			||||||
    name: string;
 | 
					 | 
				
			||||||
    version: string;
 | 
					 | 
				
			||||||
    notification(msg: FG.Notification): FG_Plugin.Notification;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Defines a shortcut link
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  interface Shortcut {
 | 
					 | 
				
			||||||
    link: RouteRecordName;
 | 
					 | 
				
			||||||
    icon: string;
 | 
					 | 
				
			||||||
    permissions?: string[];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Defines a main menu entry along with the route
 | 
					 | 
				
			||||||
   * Used when defining a plugin
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  interface MenuRoute extends MenuEntry {
 | 
					 | 
				
			||||||
    route: NamedRouteRecordRaw;
 | 
					 | 
				
			||||||
    shortcut?: boolean;
 | 
					 | 
				
			||||||
    children?: this[];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  type NamedRouteRecordRaw = RouteRecordRaw & {
 | 
					 | 
				
			||||||
    name: RouteRecordName;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Defines a menu entry in the main menu
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  interface MenuLink extends MenuEntry {
 | 
					 | 
				
			||||||
    /** Name of the target route */
 | 
					 | 
				
			||||||
    link: RouteRecordName;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Base interface for internal use
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  interface MenuEntry {
 | 
					 | 
				
			||||||
    title: string | ComputedRef<string>;
 | 
					 | 
				
			||||||
    icon: string;
 | 
					 | 
				
			||||||
    permissions?: string[];
 | 
					 | 
				
			||||||
    children?: this[];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Widget object for the dashboard
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  interface Widget {
 | 
					 | 
				
			||||||
    name: string;
 | 
					 | 
				
			||||||
    priority: number;
 | 
					 | 
				
			||||||
    permissions: FG.Permission[];
 | 
					 | 
				
			||||||
    widget: Component;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,115 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-card>
 | 
					 | 
				
			||||||
    <BalanceHeader v-model="user" :show-selector="showSelector" @open-history="openHistory" />
 | 
					 | 
				
			||||||
    <q-separator />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-card-section v-if="shortCuts" class="row q-col-gutter-md">
 | 
					 | 
				
			||||||
      <div v-for="(shortcut, index) in shortCuts" :key="index" class="col-4">
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          v-if="shortcut"
 | 
					 | 
				
			||||||
          push
 | 
					 | 
				
			||||||
          color="primary"
 | 
					 | 
				
			||||||
          style="width: 100%"
 | 
					 | 
				
			||||||
          :label="shortcut.toFixed(2).toString() + ' €'"
 | 
					 | 
				
			||||||
          @click="changeBalance(shortcut)"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <q-popup-proxy context-menu>
 | 
					 | 
				
			||||||
            <q-btn label="Entfernen" @click="removeShortcut(shortcut)" />
 | 
					 | 
				
			||||||
          </q-popup-proxy>
 | 
					 | 
				
			||||||
          <q-tooltip>Rechtsklick um Verknüpfung zu entfernen</q-tooltip>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </div></q-card-section
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
    <q-card-section class="row q-col-gutter-md items-center">
 | 
					 | 
				
			||||||
      <div class="col-sm-4 col-xs-12">
 | 
					 | 
				
			||||||
        <q-input
 | 
					 | 
				
			||||||
          v-model.number="amount"
 | 
					 | 
				
			||||||
          type="number"
 | 
					 | 
				
			||||||
          filled
 | 
					 | 
				
			||||||
          label="Eigener Betrag"
 | 
					 | 
				
			||||||
          step="0.1"
 | 
					 | 
				
			||||||
          min="0"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <div class="col-sm-4 col-xs-6">
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          style="width: 100%"
 | 
					 | 
				
			||||||
          color="primary"
 | 
					 | 
				
			||||||
          label="Anschreiben"
 | 
					 | 
				
			||||||
          @click="changeBalance(amount * -1)"
 | 
					 | 
				
			||||||
          ><q-tooltip>Rechtsklick um Betrag als Verknüpfung hinzuzufügen</q-tooltip>
 | 
					 | 
				
			||||||
          <q-popup-proxy v-model="showAddShortcut" context-menu>
 | 
					 | 
				
			||||||
            <q-btn label="neue Verknüpfung" @click="addShortcut"></q-btn>
 | 
					 | 
				
			||||||
          </q-popup-proxy>
 | 
					 | 
				
			||||||
        </q-btn>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <div class="col-sm-4 col-xs-6">
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          v-if="canAddCredit"
 | 
					 | 
				
			||||||
          style="width: 100%"
 | 
					 | 
				
			||||||
          color="secondary"
 | 
					 | 
				
			||||||
          label="Gutschreiben"
 | 
					 | 
				
			||||||
          @click="changeBalance(amount)"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </q-card-section>
 | 
					 | 
				
			||||||
  </q-card>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { computed, ref, defineComponent, onBeforeMount } from 'vue';
 | 
					 | 
				
			||||||
import { hasPermission } from 'src/utils/permission';
 | 
					 | 
				
			||||||
import BalanceHeader from '../components/BalanceHeader.vue';
 | 
					 | 
				
			||||||
import PERMISSIONS from '../permissions';
 | 
					 | 
				
			||||||
import { useBalanceStore } from '../store';
 | 
					 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'BalanceAdd',
 | 
					 | 
				
			||||||
  components: { BalanceHeader },
 | 
					 | 
				
			||||||
  emits: { 'open-history': () => true },
 | 
					 | 
				
			||||||
  setup(_, { emit }) {
 | 
					 | 
				
			||||||
    const store = useBalanceStore();
 | 
					 | 
				
			||||||
    const mainStore = useMainStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onBeforeMount(() => {
 | 
					 | 
				
			||||||
      void store.getShortcuts();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const amount = ref<number>(0);
 | 
					 | 
				
			||||||
    const showAddShortcut = ref(false);
 | 
					 | 
				
			||||||
    const user = ref(mainStore.currentUser);
 | 
					 | 
				
			||||||
    const shortCuts = computed(() => store.shortcuts);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const canAddCredit = hasPermission(PERMISSIONS.CREDIT);
 | 
					 | 
				
			||||||
    const showSelector = hasPermission(PERMISSIONS.DEBIT) || hasPermission(PERMISSIONS.CREDIT);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function addShortcut() {
 | 
					 | 
				
			||||||
      if (amount.value != 0) void store.createShortcut(amount.value * -1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    function removeShortcut(shortcut: number) {
 | 
					 | 
				
			||||||
      void store.removeShortcut(shortcut);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    async function changeBalance(amount: number) {
 | 
					 | 
				
			||||||
      await store.changeBalance(amount, user.value);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function openHistory() {
 | 
					 | 
				
			||||||
      emit('open-history');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      user,
 | 
					 | 
				
			||||||
      addShortcut,
 | 
					 | 
				
			||||||
      canAddCredit,
 | 
					 | 
				
			||||||
      removeShortcut,
 | 
					 | 
				
			||||||
      showAddShortcut,
 | 
					 | 
				
			||||||
      changeBalance,
 | 
					 | 
				
			||||||
      amount,
 | 
					 | 
				
			||||||
      showSelector,
 | 
					 | 
				
			||||||
      shortCuts,
 | 
					 | 
				
			||||||
      openHistory,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,66 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-card-section class="fit row justify-left content-center items-center q-col-gutter-sm">
 | 
					 | 
				
			||||||
    <div class="col-5">
 | 
					 | 
				
			||||||
      <div v-if="balance" class="text-h6">
 | 
					 | 
				
			||||||
        Aktueller Stand: {{ balance.balance.toFixed(2) }} €
 | 
					 | 
				
			||||||
        <q-badge v-if="isLocked" color="negative" align="top"> gesperrt </q-badge>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <q-spinner v-else color="primary" size="3em" />
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    <div v-if="showSelector" class="col-6">
 | 
					 | 
				
			||||||
      <UserSelector v-model="user" />
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    <div class="col-1 justify-end">
 | 
					 | 
				
			||||||
      <q-btn round flat icon="mdi-format-list-checks" @click="openHistory" />
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </q-card-section>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { computed, defineComponent, onBeforeMount, PropType } from 'vue';
 | 
					 | 
				
			||||||
import UserSelector from 'src/plugins/user/components/UserSelector.vue';
 | 
					 | 
				
			||||||
import { useBalanceStore } from '../store';
 | 
					 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'BalanceHeader',
 | 
					 | 
				
			||||||
  components: { UserSelector },
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    showSelector: Boolean,
 | 
					 | 
				
			||||||
    modelValue: {
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
      type: Object as PropType<FG.User>,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  emits: { 'update:modelValue': (u: FG.User) => !!u, 'open-history': () => true },
 | 
					 | 
				
			||||||
  setup(props, { emit }) {
 | 
					 | 
				
			||||||
    const store = useBalanceStore();
 | 
					 | 
				
			||||||
    const mainStore = useMainStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onBeforeMount(() => void store.getBalance(mainStore.currentUser));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const balance = computed(() =>
 | 
					 | 
				
			||||||
      store.balances.find((x) => x.userid === props.modelValue.userid)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const isLocked = computed(
 | 
					 | 
				
			||||||
      () =>
 | 
					 | 
				
			||||||
        balance.value === undefined ||
 | 
					 | 
				
			||||||
        (balance.value.limit !== undefined && balance.value.balance <= balance.value.limit)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    const user = computed({
 | 
					 | 
				
			||||||
      get: () => props.modelValue,
 | 
					 | 
				
			||||||
      set: (x: FG.User) => {
 | 
					 | 
				
			||||||
        void store.getBalance(x);
 | 
					 | 
				
			||||||
        emit('update:modelValue', x);
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function openHistory() {
 | 
					 | 
				
			||||||
      emit('open-history');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return { user, balance, isLocked, openHistory };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,75 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-card>
 | 
					 | 
				
			||||||
    <BalanceHeader v-model="sender" :show-selector="showSelector" @open-history="openHistory" />
 | 
					 | 
				
			||||||
    <q-separator />
 | 
					 | 
				
			||||||
    <q-card-section class="row q-col-gutter-md items-center">
 | 
					 | 
				
			||||||
      <div class="col-sm-4 col-xs-12">
 | 
					 | 
				
			||||||
        <q-input v-model.number="amount" type="number" filled label="Betrag" step="0.1" min="0" />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <div class="col-sm-4 col-xs-6">
 | 
					 | 
				
			||||||
        <UserSelector v-model="receiver" label="Empfänger" />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <div class="col-sm-4 col-xs-6">
 | 
					 | 
				
			||||||
        <q-btn
 | 
					 | 
				
			||||||
          style="width: 100%"
 | 
					 | 
				
			||||||
          color="primary"
 | 
					 | 
				
			||||||
          :disable="sendDisabled"
 | 
					 | 
				
			||||||
          label="Senden"
 | 
					 | 
				
			||||||
          @click="sendAmount"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </q-card-section>
 | 
					 | 
				
			||||||
  </q-card>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { computed, ref, defineComponent } from 'vue';
 | 
					 | 
				
			||||||
import { hasPermission } from 'src/utils/permission';
 | 
					 | 
				
			||||||
import UserSelector from 'src/plugins/user/components/UserSelector.vue';
 | 
					 | 
				
			||||||
import BalanceHeader from '../components/BalanceHeader.vue';
 | 
					 | 
				
			||||||
import PERMISSIONS from '../permissions';
 | 
					 | 
				
			||||||
import { useBalanceStore } from '../store';
 | 
					 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'BalanceTransfer',
 | 
					 | 
				
			||||||
  components: { BalanceHeader, UserSelector },
 | 
					 | 
				
			||||||
  emits: { 'open-history': () => true },
 | 
					 | 
				
			||||||
  setup(_, { emit }) {
 | 
					 | 
				
			||||||
    const store = useBalanceStore();
 | 
					 | 
				
			||||||
    const mainStore = useMainStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const showSelector = computed(() => hasPermission(PERMISSIONS.SEND_OTHER));
 | 
					 | 
				
			||||||
    const sender = ref<FG.User | undefined>(mainStore.currentUser);
 | 
					 | 
				
			||||||
    const receiver = ref<FG.User | undefined>(undefined);
 | 
					 | 
				
			||||||
    const amount = ref<number>(0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const sendDisabled = computed(() => {
 | 
					 | 
				
			||||||
      return !(
 | 
					 | 
				
			||||||
        receiver.value &&
 | 
					 | 
				
			||||||
        sender.value &&
 | 
					 | 
				
			||||||
        sender.value.userid != receiver.value.userid &&
 | 
					 | 
				
			||||||
        amount.value > 0
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function sendAmount() {
 | 
					 | 
				
			||||||
      if (receiver.value) await store.changeBalance(amount.value, receiver.value, sender.value);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function openHistory() {
 | 
					 | 
				
			||||||
      emit('open-history');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      sender,
 | 
					 | 
				
			||||||
      receiver,
 | 
					 | 
				
			||||||
      amount,
 | 
					 | 
				
			||||||
      sendAmount,
 | 
					 | 
				
			||||||
      showSelector,
 | 
					 | 
				
			||||||
      sendDisabled,
 | 
					 | 
				
			||||||
      openHistory,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,107 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <q-card flat square>
 | 
					 | 
				
			||||||
      <q-card-section class="row items-center justify-between" horizontal>
 | 
					 | 
				
			||||||
        <div class="col-5 text-left q-px-sm">
 | 
					 | 
				
			||||||
          <div :class="{ 'text-negative': isNegative() }" class="text-weight-bold text-h6">
 | 
					 | 
				
			||||||
            <span v-if="isNegative()">-</span>{{ transaction.amount.toFixed(2) }} €
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <div class="text-caption">{{ text }}</div>
 | 
					 | 
				
			||||||
          <div class="text-caption">{{ timeStr }}</div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div class="col-5 q-px-sm text-center">
 | 
					 | 
				
			||||||
          <div v-if="isReversed" class="text-subtitle1">Storniert</div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div class="col-2 q-pr-sm" style="text-align: right">
 | 
					 | 
				
			||||||
          <q-btn
 | 
					 | 
				
			||||||
            :color="isReversed ? 'positive' : 'negative'"
 | 
					 | 
				
			||||||
            aria-label="Reverse transaction"
 | 
					 | 
				
			||||||
            :icon="!isReversed ? 'mdi-trash-can' : 'mdi-check-bold'"
 | 
					 | 
				
			||||||
            size="sm"
 | 
					 | 
				
			||||||
            round
 | 
					 | 
				
			||||||
            :disable="!canReverse"
 | 
					 | 
				
			||||||
            @click="reverse"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
    <q-separator />
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { ref, computed, defineComponent, onUnmounted, onMounted, PropType } from 'vue';
 | 
					 | 
				
			||||||
import { hasPermission } from 'src/utils/permission';
 | 
					 | 
				
			||||||
import { formatDateTime } from 'src/utils/datetime';
 | 
					 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
import { useUserStore } from 'src/plugins/user/store';
 | 
					 | 
				
			||||||
import { useBalanceStore } from '../store';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'Transaction',
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    transaction: {
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
      type: Object as PropType<FG.Transaction>,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  emits: { 'update:transaction': (t: FG.Transaction) => !!t },
 | 
					 | 
				
			||||||
  setup(props, { emit }) {
 | 
					 | 
				
			||||||
    const mainStore = useMainStore();
 | 
					 | 
				
			||||||
    const userStore = useUserStore();
 | 
					 | 
				
			||||||
    const balanceStore = useBalanceStore();
 | 
					 | 
				
			||||||
    const now = ref(Date.now());
 | 
					 | 
				
			||||||
    const ival = setInterval(() => (now.value = Date.now()), 1000);
 | 
					 | 
				
			||||||
    const text = ref('');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onUnmounted(() => clearInterval(ival));
 | 
					 | 
				
			||||||
    onMounted(() => refreshText());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const isNegative = () => props.transaction.sender_id === mainStore.currentUser.userid;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const refreshText = async () => {
 | 
					 | 
				
			||||||
      if (isNegative()) {
 | 
					 | 
				
			||||||
        text.value = 'Anschreiben';
 | 
					 | 
				
			||||||
        if (props.transaction.receiver_id) {
 | 
					 | 
				
			||||||
          const user = <FG.User>await userStore.getUser(props.transaction.receiver_id);
 | 
					 | 
				
			||||||
          text.value = `Gesendet an ${user.display_name}`;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        text.value = 'Gutschrift';
 | 
					 | 
				
			||||||
        if (props.transaction.sender_id) {
 | 
					 | 
				
			||||||
          const user = <FG.User>await userStore.getUser(props.transaction.sender_id);
 | 
					 | 
				
			||||||
          text.value = `Bekommen von ${user.display_name}`;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const isReversed = computed(
 | 
					 | 
				
			||||||
      () => props.transaction.reversal_id != undefined || props.transaction.original_id != undefined
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const canReverse = computed(
 | 
					 | 
				
			||||||
      () =>
 | 
					 | 
				
			||||||
        !isReversed.value &&
 | 
					 | 
				
			||||||
        (hasPermission('balance_reversal') ||
 | 
					 | 
				
			||||||
          (props.transaction.sender_id === mainStore.currentUser.userid &&
 | 
					 | 
				
			||||||
            now.value - props.transaction.time.getTime() < 10000))
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function reverse() {
 | 
					 | 
				
			||||||
      if (canReverse.value)
 | 
					 | 
				
			||||||
        void balanceStore.revert(props.transaction).then(() => {
 | 
					 | 
				
			||||||
          emit('update:transaction', props.transaction);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const timeStr = computed(() => {
 | 
					 | 
				
			||||||
      const elapsed = (now.value - props.transaction.time.getTime()) / 1000;
 | 
					 | 
				
			||||||
      if (elapsed < 60) return `Vor ${elapsed.toFixed()} s`;
 | 
					 | 
				
			||||||
      return formatDateTime(props.transaction.time, elapsed > 12 * 60 * 60, true, true) + ' Uhr';
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return { timeStr, reverse, isNegative, text, refreshText, canReverse, isReversed };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  watch: { transaction: 'refreshText' },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,29 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-card style="text-align: center">
 | 
					 | 
				
			||||||
    <q-card-section>
 | 
					 | 
				
			||||||
      <div class="text-h6">Gerücht: {{ balance.toFixed(2) }} €</div>
 | 
					 | 
				
			||||||
    </q-card-section>
 | 
					 | 
				
			||||||
  </q-card>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
import { useBalanceStore } from '../store';
 | 
					 | 
				
			||||||
import { computed, defineComponent, onBeforeMount } from 'vue';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'BalanceWidget',
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    const store = useBalanceStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onBeforeMount(() => {
 | 
					 | 
				
			||||||
      const mainStore = useMainStore();
 | 
					 | 
				
			||||||
      void store.getBalance(mainStore.currentUser);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const balance = computed(() => store.balance?.balance || NaN);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return { balance };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,59 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <q-page padding>
 | 
					 | 
				
			||||||
      <q-card>
 | 
					 | 
				
			||||||
        <q-card-section>
 | 
					 | 
				
			||||||
          <q-table :rows="rows" row-key="userid" :columns="columns" />
 | 
					 | 
				
			||||||
        </q-card-section>
 | 
					 | 
				
			||||||
      </q-card>
 | 
					 | 
				
			||||||
    </q-page>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
// TODO: Fill usefull data
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { ref, defineComponent, onMounted } from 'vue';
 | 
					 | 
				
			||||||
import { useBalanceStore } from '../store';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  // name: 'PageName'
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    const store = useBalanceStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onMounted(() => void store.getBalances().then((balances) => rows.value.push(...balances)));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const rows = ref(store.balances);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const columns = [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name: 'userid',
 | 
					 | 
				
			||||||
        label: 'Benutzer ID',
 | 
					 | 
				
			||||||
        field: 'userid',
 | 
					 | 
				
			||||||
        required: true,
 | 
					 | 
				
			||||||
        align: 'left',
 | 
					 | 
				
			||||||
        sortable: true
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name: 'credit',
 | 
					 | 
				
			||||||
        label: 'Haben',
 | 
					 | 
				
			||||||
        field: 'credit',
 | 
					 | 
				
			||||||
        format: (val: number) => val.toFixed(2)
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name: 'debit',
 | 
					 | 
				
			||||||
        label: 'Soll',
 | 
					 | 
				
			||||||
        field: 'debit',
 | 
					 | 
				
			||||||
        format: (val: number) => val.toFixed(2)
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name: 'balance',
 | 
					 | 
				
			||||||
        label: 'Kontostand',
 | 
					 | 
				
			||||||
        format: (_: undefined, row: { debit: number; credit: number }) =>
 | 
					 | 
				
			||||||
          (row.credit - row.debit).toFixed(2)
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
    return { rows, columns };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,135 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
 | 
					 | 
				
			||||||
    <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;
 | 
					 | 
				
			||||||
          show = false;
 | 
					 | 
				
			||||||
        "
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    <q-drawer v-model="showDrawer" side="right" behavior="mobile" @click="showDrawer = !showDrawer">
 | 
					 | 
				
			||||||
      <q-list v-if="!$q.screen.gt.sm && !show" 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-list v-if="show">
 | 
					 | 
				
			||||||
        <div v-for="(transaction, index) in transactions" :key="index" class="col-sm-12">
 | 
					 | 
				
			||||||
          <Transaction v-model:transaction="transactions[index]" />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </q-list>
 | 
					 | 
				
			||||||
    </q-drawer>
 | 
					 | 
				
			||||||
    <q-tab-panels
 | 
					 | 
				
			||||||
      v-model="tab"
 | 
					 | 
				
			||||||
      style="background-color: transparent"
 | 
					 | 
				
			||||||
      class="q-pa-none col-12"
 | 
					 | 
				
			||||||
      animated
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <q-tab-panel name="add" class="q-px-xs">
 | 
					 | 
				
			||||||
        <BalanceAdd
 | 
					 | 
				
			||||||
          @open-history="
 | 
					 | 
				
			||||||
            showDrawer = !showDrawer;
 | 
					 | 
				
			||||||
            show = true;
 | 
					 | 
				
			||||||
          "
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-tab-panel>
 | 
					 | 
				
			||||||
      <q-tab-panel name="transfer" class="q-px-xs">
 | 
					 | 
				
			||||||
        <BalanceTransfer
 | 
					 | 
				
			||||||
          @open-history="
 | 
					 | 
				
			||||||
            showDrawer = !showDrawer;
 | 
					 | 
				
			||||||
            show = true;
 | 
					 | 
				
			||||||
          "
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-tab-panel>
 | 
					 | 
				
			||||||
    </q-tab-panels>
 | 
					 | 
				
			||||||
  </q-page>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { computed, defineComponent, ref, onMounted } from 'vue';
 | 
					 | 
				
			||||||
import { hasSomePermissions } from 'src/utils/permission';
 | 
					 | 
				
			||||||
import PERMISSIONS from '../permissions';
 | 
					 | 
				
			||||||
import BalanceAdd from '../components/BalanceAdd.vue';
 | 
					 | 
				
			||||||
import BalanceTransfer from '../components/BalanceTransfer.vue';
 | 
					 | 
				
			||||||
import Transaction from '../components/Transaction.vue';
 | 
					 | 
				
			||||||
import { useBalanceStore } from '../store';
 | 
					 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'BalanceManage',
 | 
					 | 
				
			||||||
  components: { BalanceAdd, BalanceTransfer, Transaction },
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    const balanceStore = useBalanceStore();
 | 
					 | 
				
			||||||
    const mainStore = useMainStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const now = new Date();
 | 
					 | 
				
			||||||
    onMounted(() => {
 | 
					 | 
				
			||||||
      void balanceStore.getTransactions(mainStore.currentUser, {
 | 
					 | 
				
			||||||
        from: new Date(now.getFullYear(), now.getMonth(), now.getDate()),
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const transactions = computed(() => {
 | 
					 | 
				
			||||||
      return balanceStore.transactions
 | 
					 | 
				
			||||||
        .filter((t) => t.original_id == undefined)
 | 
					 | 
				
			||||||
        .filter((t) => t.time > new Date(now.getFullYear(), now.getMonth(), now.getDate()))
 | 
					 | 
				
			||||||
        .sort((a, b) => (a.time >= b.time ? -1 : 1));
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const canAdd = () =>
 | 
					 | 
				
			||||||
      hasSomePermissions([PERMISSIONS.DEBIT, PERMISSIONS.CREDIT, PERMISSIONS.DEBIT_OWN]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    interface Tab {
 | 
					 | 
				
			||||||
      name: string;
 | 
					 | 
				
			||||||
      label: string;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const tabs: Tab[] = [
 | 
					 | 
				
			||||||
      ...(canAdd() ? [{ name: 'add', label: 'Anschreiben' }] : []),
 | 
					 | 
				
			||||||
      ...(hasSomePermissions([PERMISSIONS.SEND, PERMISSIONS.SEND_OTHER])
 | 
					 | 
				
			||||||
        ? [{ name: 'transfer', label: 'Übertragen' }]
 | 
					 | 
				
			||||||
        : []),
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    //const drawer = ref<boolean>(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /*    const showDrawer = computed({
 | 
					 | 
				
			||||||
      get: () => {
 | 
					 | 
				
			||||||
        return !Screen.gt.sm && drawer.value;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      set: (val: boolean) => {
 | 
					 | 
				
			||||||
        drawer.value = val;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
    const showDrawer = ref<boolean>(false);
 | 
					 | 
				
			||||||
    const tab = ref<string>(canAdd() ? 'add' : 'transfer');
 | 
					 | 
				
			||||||
    const show = ref<boolean>(false);
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      showDrawer,
 | 
					 | 
				
			||||||
      tab,
 | 
					 | 
				
			||||||
      tabs,
 | 
					 | 
				
			||||||
      transactions,
 | 
					 | 
				
			||||||
      show,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,166 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-page padding>
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <q-card-section class="text-center">
 | 
					 | 
				
			||||||
        <div class="text-h4">Aktueller Stand</div>
 | 
					 | 
				
			||||||
        <div style="font-size: 2em">{{ balance.toFixed(2) }} €</div>
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-separator />
 | 
					 | 
				
			||||||
      <q-card-section>
 | 
					 | 
				
			||||||
        <q-table
 | 
					 | 
				
			||||||
          v-model:pagination="pagination"
 | 
					 | 
				
			||||||
          title="Buchungen"
 | 
					 | 
				
			||||||
          :rows="data"
 | 
					 | 
				
			||||||
          :columns="columns"
 | 
					 | 
				
			||||||
          row-key="id"
 | 
					 | 
				
			||||||
          :loading="loading"
 | 
					 | 
				
			||||||
          binary-state-sort
 | 
					 | 
				
			||||||
          @request="onRequest"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <template #top>
 | 
					 | 
				
			||||||
            <q-toggle v-model="showCancelled" label="Stornierte einblenden" />
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
          <template #body-cell="props">
 | 
					 | 
				
			||||||
            <q-td :props="props" :class="{ 'bg-grey': props.row.reversal_id != null }">
 | 
					 | 
				
			||||||
              {{ props.value }}
 | 
					 | 
				
			||||||
            </q-td>
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
        </q-table>
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-page>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { computed, defineComponent, onMounted, ref } from 'vue';
 | 
					 | 
				
			||||||
import { formatDateTime } from 'src/utils/datetime';
 | 
					 | 
				
			||||||
import { useBalanceStore } from '../store';
 | 
					 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
import { useUserStore } from 'src/plugins/user/store';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  // name: 'PageName'
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    const store = useBalanceStore();
 | 
					 | 
				
			||||||
    const mainStore = useMainStore();
 | 
					 | 
				
			||||||
    const userStore = useUserStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onMounted(() => {
 | 
					 | 
				
			||||||
      void store.getBalance(mainStore.currentUser);
 | 
					 | 
				
			||||||
      void userStore.getUsers().then(() =>
 | 
					 | 
				
			||||||
        onRequest({
 | 
					 | 
				
			||||||
          pagination: pagination.value,
 | 
					 | 
				
			||||||
          filter: undefined
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const showCancelled = ref(false);
 | 
					 | 
				
			||||||
    const data = ref<FG.Transaction[]>([]);
 | 
					 | 
				
			||||||
    const loading = ref(false);
 | 
					 | 
				
			||||||
    const pagination = ref({
 | 
					 | 
				
			||||||
      sortBy: 'time',
 | 
					 | 
				
			||||||
      descending: false,
 | 
					 | 
				
			||||||
      page: 1,
 | 
					 | 
				
			||||||
      rowsPerPage: 3,
 | 
					 | 
				
			||||||
      rowsNumber: 10
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    interface PaginationInterface {
 | 
					 | 
				
			||||||
      sortBy: string;
 | 
					 | 
				
			||||||
      descending: boolean;
 | 
					 | 
				
			||||||
      page: number;
 | 
					 | 
				
			||||||
      rowsPerPage: number;
 | 
					 | 
				
			||||||
      rowsNumber: number;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function onRequest(props: { pagination: PaginationInterface; filter?: string }) {
 | 
					 | 
				
			||||||
      const { page, rowsPerPage, sortBy, descending } = props.pagination;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      loading.value = true;
 | 
					 | 
				
			||||||
      // get all rows if "All" (0) is selected
 | 
					 | 
				
			||||||
      const fetchCount = rowsPerPage === 0 ? pagination.value.rowsNumber : rowsPerPage;
 | 
					 | 
				
			||||||
      // calculate starting row of data
 | 
					 | 
				
			||||||
      const startRow = (page - 1) * rowsPerPage;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const result = await store.getTransactions(mainStore.currentUser, {
 | 
					 | 
				
			||||||
          offset: startRow,
 | 
					 | 
				
			||||||
          limit: fetchCount,
 | 
					 | 
				
			||||||
          showCancelled: showCancelled.value,
 | 
					 | 
				
			||||||
          showReversals: false
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        // clear out existing data and add new
 | 
					 | 
				
			||||||
        data.value.splice(0, data.value.length, ...result.transactions);
 | 
					 | 
				
			||||||
        // don't forget to update local pagination object
 | 
					 | 
				
			||||||
        pagination.value.page = page;
 | 
					 | 
				
			||||||
        pagination.value.rowsPerPage = rowsPerPage;
 | 
					 | 
				
			||||||
        pagination.value.sortBy = sortBy;
 | 
					 | 
				
			||||||
        pagination.value.descending = descending;
 | 
					 | 
				
			||||||
        if (result.count) pagination.value.rowsNumber = result.count;
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        // ...
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      loading.value = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const balance = computed(() => store.balance?.balance || NaN);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const columns = [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name: 'time',
 | 
					 | 
				
			||||||
        label: 'Datum',
 | 
					 | 
				
			||||||
        field: 'time',
 | 
					 | 
				
			||||||
        required: true,
 | 
					 | 
				
			||||||
        sortable: true,
 | 
					 | 
				
			||||||
        format: (val: Date) => formatDateTime(new Date(val), true, true, true)
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name: 'type',
 | 
					 | 
				
			||||||
        label: 'Type',
 | 
					 | 
				
			||||||
        format: (_: undefined, row: FG.Transaction) => {
 | 
					 | 
				
			||||||
          if (row.sender_id == null) return 'Gutschrift';
 | 
					 | 
				
			||||||
          else {
 | 
					 | 
				
			||||||
            if (row.receiver_id == null) return 'Angeschrieben';
 | 
					 | 
				
			||||||
            else {
 | 
					 | 
				
			||||||
              if (row.receiver_id === mainStore.currentUser.userid) return 'Bekommen von X';
 | 
					 | 
				
			||||||
              else return 'Gesendet an X';
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name: 'text',
 | 
					 | 
				
			||||||
        label: 'Text',
 | 
					 | 
				
			||||||
        format: (_: undefined, row: FG.Transaction) => {
 | 
					 | 
				
			||||||
          if (row.sender_id == null) return 'Gutschrift';
 | 
					 | 
				
			||||||
          else {
 | 
					 | 
				
			||||||
            if (row.receiver_id == null) return 'Angeschrieben';
 | 
					 | 
				
			||||||
            else {
 | 
					 | 
				
			||||||
              if (row.receiver_id === mainStore.currentUser.userid) return 'Bekommen von X';
 | 
					 | 
				
			||||||
              else return 'Gesendet an X';
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name: 'amount',
 | 
					 | 
				
			||||||
        label: 'Betrag',
 | 
					 | 
				
			||||||
        field: 'amount',
 | 
					 | 
				
			||||||
        format: (val: number) => `${val.toFixed(2)}€`
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name: 'author_id',
 | 
					 | 
				
			||||||
        label: 'Benutzer',
 | 
					 | 
				
			||||||
        field: 'author_id',
 | 
					 | 
				
			||||||
        format: (val: string) => {
 | 
					 | 
				
			||||||
          const user = userStore.users.filter((x) => x.userid == val);
 | 
					 | 
				
			||||||
          if (user.length > 0) return user[0].display_name;
 | 
					 | 
				
			||||||
          else return val;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return { data, pagination, onRequest, loading, balance, columns, showCancelled };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,21 +0,0 @@
 | 
				
			||||||
const PERMISSIONS = {
 | 
					 | 
				
			||||||
  // Show own and others balance
 | 
					 | 
				
			||||||
  SHOW: 'balance_show',
 | 
					 | 
				
			||||||
  SHOW_OTHER: 'balance_show_others',
 | 
					 | 
				
			||||||
  // Credit balance (give)
 | 
					 | 
				
			||||||
  CREDIT: 'balance_credit',
 | 
					 | 
				
			||||||
  // Debit balance (take)
 | 
					 | 
				
			||||||
  DEBIT: 'balance_debit',
 | 
					 | 
				
			||||||
  // Debit own balance only
 | 
					 | 
				
			||||||
  DEBIT_OWN: 'balance_debit_own',
 | 
					 | 
				
			||||||
  // Send from to other
 | 
					 | 
				
			||||||
  SEND: 'balance_send',
 | 
					 | 
				
			||||||
  // Send from other to another
 | 
					 | 
				
			||||||
  SEND_OTHER: 'balance_send_others',
 | 
					 | 
				
			||||||
  // Can set limit for users
 | 
					 | 
				
			||||||
  SET_LIMIT: 'balance_set_limit',
 | 
					 | 
				
			||||||
  //Allow sending / sub while exceeding the set limit
 | 
					 | 
				
			||||||
  EXCEED_LIMIT: 'balance_exceed_limit',
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default PERMISSIONS;
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,21 +0,0 @@
 | 
				
			||||||
import routes from './routes';
 | 
					 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					 | 
				
			||||||
import { defineAsyncComponent } from 'vue';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const plugin: FG_Plugin.Plugin = {
 | 
					 | 
				
			||||||
  name: 'Balance',
 | 
					 | 
				
			||||||
  innerRoutes: routes,
 | 
					 | 
				
			||||||
  requiredModules: ['User'],
 | 
					 | 
				
			||||||
  requiredBackendModules: ['balance'],
 | 
					 | 
				
			||||||
  version: '0.0.2',
 | 
					 | 
				
			||||||
  widgets: [
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      priority: 0,
 | 
					 | 
				
			||||||
      name: 'current',
 | 
					 | 
				
			||||||
      permissions: ['balance_show'],
 | 
					 | 
				
			||||||
      widget: defineAsyncComponent(() => import('./components/Widget.vue')),
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  ],
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default plugin;
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,50 +0,0 @@
 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					 | 
				
			||||||
import permissions from '../permissions';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const mainRoutes: FG_Plugin.MenuRoute[] = [
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    title: 'Gerücht',
 | 
					 | 
				
			||||||
    icon: 'mdi-cash-100',
 | 
					 | 
				
			||||||
    permissions: ['user'],
 | 
					 | 
				
			||||||
    route: {
 | 
					 | 
				
			||||||
      path: 'balance',
 | 
					 | 
				
			||||||
      name: 'balance',
 | 
					 | 
				
			||||||
      redirect: { name: 'balance-view' },
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    children: [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        title: 'Übersicht',
 | 
					 | 
				
			||||||
        icon: 'mdi-cash-check',
 | 
					 | 
				
			||||||
        permissions: [permissions.SHOW],
 | 
					 | 
				
			||||||
        route: {
 | 
					 | 
				
			||||||
          path: 'overview',
 | 
					 | 
				
			||||||
          name: 'balance-view',
 | 
					 | 
				
			||||||
          component: () => import('../pages/Overview.vue'),
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        title: 'Buchen',
 | 
					 | 
				
			||||||
        icon: 'mdi-cash-plus',
 | 
					 | 
				
			||||||
        shortcut: true,
 | 
					 | 
				
			||||||
        permissions: [permissions.DEBIT_OWN, permissions.SHOW],
 | 
					 | 
				
			||||||
        route: {
 | 
					 | 
				
			||||||
          path: 'change',
 | 
					 | 
				
			||||||
          name: 'balance-change',
 | 
					 | 
				
			||||||
          component: () => import('../pages/MainPage.vue'),
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        title: 'Verwaltung',
 | 
					 | 
				
			||||||
        icon: 'mdi-account-cash',
 | 
					 | 
				
			||||||
        permissions: [permissions.SET_LIMIT, permissions.SHOW_OTHER],
 | 
					 | 
				
			||||||
        route: {
 | 
					 | 
				
			||||||
          path: 'admin',
 | 
					 | 
				
			||||||
          name: 'balance-admin',
 | 
					 | 
				
			||||||
          component: () => import('../pages/Admin.vue'),
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default mainRoutes;
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,166 +0,0 @@
 | 
				
			||||||
import { api } from 'src/boot/axios';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface BalanceResponse {
 | 
					 | 
				
			||||||
  balance: number;
 | 
					 | 
				
			||||||
  credit: number;
 | 
					 | 
				
			||||||
  debit: number;
 | 
					 | 
				
			||||||
  limit?: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface BalancesResponse extends BalanceResponse {
 | 
					 | 
				
			||||||
  userid: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface TransactionsResponse {
 | 
					 | 
				
			||||||
  transactions: Array<FG.Transaction>;
 | 
					 | 
				
			||||||
  count?: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { defineStore } from 'pinia';
 | 
					 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
import { AxiosResponse } from 'axios';
 | 
					 | 
				
			||||||
import { Notify } from 'quasar';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function fixTransaction(t: FG.Transaction) {
 | 
					 | 
				
			||||||
  t.time = new Date(t.time);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const useBalanceStore = defineStore({
 | 
					 | 
				
			||||||
  id: 'balance',
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  state: () => ({
 | 
					 | 
				
			||||||
    balances: [] as BalancesResponse[],
 | 
					 | 
				
			||||||
    shortcuts: [] as number[],
 | 
					 | 
				
			||||||
    transactions: [] as FG.Transaction[],
 | 
					 | 
				
			||||||
    _balances_dirty: 0,
 | 
					 | 
				
			||||||
  }),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  getters: {
 | 
					 | 
				
			||||||
    balance() {
 | 
					 | 
				
			||||||
      const mainStore = useMainStore();
 | 
					 | 
				
			||||||
      return this.balances.find((v) => v.userid === mainStore.user?.userid);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  actions: {
 | 
					 | 
				
			||||||
    async createShortcut(shortcut: number) {
 | 
					 | 
				
			||||||
      const mainStore = useMainStore();
 | 
					 | 
				
			||||||
      this.shortcuts.push(shortcut);
 | 
					 | 
				
			||||||
      this.shortcuts.sort((a, b) => a - b);
 | 
					 | 
				
			||||||
      await api.put(`/users/${mainStore.currentUser.userid}/balance/shortcuts`, this.shortcuts);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async removeShortcut(shortcut: number) {
 | 
					 | 
				
			||||||
      const mainStore = useMainStore();
 | 
					 | 
				
			||||||
      this.shortcuts = this.shortcuts.filter((value: number) => value !== shortcut);
 | 
					 | 
				
			||||||
      this.shortcuts.sort((a, b) => a - b);
 | 
					 | 
				
			||||||
      await api.put(`/users/${mainStore.currentUser.userid}/balance/shortcuts`, this.shortcuts);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async getShortcuts(force = false) {
 | 
					 | 
				
			||||||
      if (force || this.shortcuts.length == 0) {
 | 
					 | 
				
			||||||
        const mainStore = useMainStore();
 | 
					 | 
				
			||||||
        const { data } = await api.get<number[]>(
 | 
					 | 
				
			||||||
          `/users/${mainStore.currentUser.userid}/balance/shortcuts`
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        this.shortcuts = data;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async getBalance(user: FG.User) {
 | 
					 | 
				
			||||||
      const { data } = await api.get<BalanceResponse>(`/users/${user.userid}/balance`);
 | 
					 | 
				
			||||||
      const idx = this.balances.findIndex((x) => x.userid === user.userid);
 | 
					 | 
				
			||||||
      if (idx == -1) this.balances.push(Object.assign(data, { userid: user.userid }));
 | 
					 | 
				
			||||||
      else this.balances[idx] = Object.assign(data, { userid: user.userid });
 | 
					 | 
				
			||||||
      return data;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async getBalances(force = false) {
 | 
					 | 
				
			||||||
      if (
 | 
					 | 
				
			||||||
        force ||
 | 
					 | 
				
			||||||
        this.balances.length == 0 ||
 | 
					 | 
				
			||||||
        new Date().getTime() - this._balances_dirty > 60000
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        const { data } = await api.get<BalancesResponse[]>('/balance');
 | 
					 | 
				
			||||||
        this.balances = data;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return this.balances;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async changeBalance(amount: number, user: FG.User, sender: FG.User | undefined = undefined) {
 | 
					 | 
				
			||||||
      const mainStore = useMainStore();
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const { data } = await api.put<FG.Transaction>(`/users/${user.userid}/balance`, {
 | 
					 | 
				
			||||||
          amount,
 | 
					 | 
				
			||||||
          user: user.userid,
 | 
					 | 
				
			||||||
          sender: sender?.userid,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        fixTransaction(data);
 | 
					 | 
				
			||||||
        if (
 | 
					 | 
				
			||||||
          user.userid === mainStore.currentUser.userid ||
 | 
					 | 
				
			||||||
          sender?.userid === mainStore.currentUser.userid
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
          this.transactions.push(data);
 | 
					 | 
				
			||||||
        const f = this.balances.find((x) => x.userid === user.userid);
 | 
					 | 
				
			||||||
        if (f) f.balance += amount;
 | 
					 | 
				
			||||||
        if (sender) {
 | 
					 | 
				
			||||||
          const f = this.balances.find((x) => x.userid === sender.userid);
 | 
					 | 
				
			||||||
          if (f) f.balance += -1 * amount;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this._balances_dirty = 0;
 | 
					 | 
				
			||||||
        return data;
 | 
					 | 
				
			||||||
      } catch ({ response }) {
 | 
					 | 
				
			||||||
        // Maybe Balance changed
 | 
					 | 
				
			||||||
        if (response && (<AxiosResponse>response).status == 409) {
 | 
					 | 
				
			||||||
          Notify.create({
 | 
					 | 
				
			||||||
            type: 'negative',
 | 
					 | 
				
			||||||
            group: false,
 | 
					 | 
				
			||||||
            message: 'Das Limit wurde überschritten!',
 | 
					 | 
				
			||||||
            timeout: 10000,
 | 
					 | 
				
			||||||
            progress: true,
 | 
					 | 
				
			||||||
            actions: [{ icon: 'mdi-close', color: 'white' }],
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
          //void this.getTransactions(true);
 | 
					 | 
				
			||||||
          void this.getBalance(sender ? sender : user);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async getTransactions(
 | 
					 | 
				
			||||||
      user: FG.User,
 | 
					 | 
				
			||||||
      filter:
 | 
					 | 
				
			||||||
        | {
 | 
					 | 
				
			||||||
            limit?: number;
 | 
					 | 
				
			||||||
            offset?: number;
 | 
					 | 
				
			||||||
            from?: Date;
 | 
					 | 
				
			||||||
            to?: Date;
 | 
					 | 
				
			||||||
            showReversals?: boolean;
 | 
					 | 
				
			||||||
            showCancelled?: boolean;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        | undefined = undefined
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      if (!filter) filter = { limit: 10 };
 | 
					 | 
				
			||||||
      const { data } = await api.get<TransactionsResponse>(
 | 
					 | 
				
			||||||
        `/users/${user.userid}/balance/transactions`,
 | 
					 | 
				
			||||||
        { params: filter }
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      data.transactions.forEach((t) => fixTransaction(t));
 | 
					 | 
				
			||||||
      if (data.transactions) this.transactions.push(...data.transactions);
 | 
					 | 
				
			||||||
      return data;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async revert(transaction: FG.Transaction) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const { data } = await api.delete<FG.Transaction>(`/balance/${transaction.id}`);
 | 
					 | 
				
			||||||
        fixTransaction(data);
 | 
					 | 
				
			||||||
        const f = this.transactions.find((x) => x.id === transaction.id);
 | 
					 | 
				
			||||||
        if (f) f.reversal_id = data.id;
 | 
					 | 
				
			||||||
        this.transactions.push(data);
 | 
					 | 
				
			||||||
        console.log(data);
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        // ...
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this._balances_dirty = 0;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,62 +0,0 @@
 | 
				
			||||||
<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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,65 +0,0 @@
 | 
				
			||||||
<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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,512 +0,0 @@
 | 
				
			||||||
<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 { defineComponent, onBeforeMount, ComputedRef, computed, ref } from 'vue';
 | 
					 | 
				
			||||||
import { Drink, usePricelistStore, DrinkPriceVolume } from 'src/plugins/pricelist/store';
 | 
					 | 
				
			||||||
import MinPriceSetting from 'src/plugins/pricelist/components/MinPriceSetting.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';
 | 
					 | 
				
			||||||
import { DeleteObjects } from 'src/plugins/pricelist/utils/utils';
 | 
					 | 
				
			||||||
import { hasPermission } from 'src/utils/permission';
 | 
					 | 
				
			||||||
import { PERMISSIONS } from 'src/plugins/pricelist/permissions';
 | 
					 | 
				
			||||||
import { baseURL } from 'src/config';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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 `${baseURL.value}/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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,60 +0,0 @@
 | 
				
			||||||
<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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,340 +0,0 @@
 | 
				
			||||||
<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 { DrinkPriceVolume } from 'src/plugins/pricelist/store';
 | 
					 | 
				
			||||||
import Ingredients from 'src/plugins/pricelist/components/CalculationTable/Ingredients.vue';
 | 
					 | 
				
			||||||
import NewPrice from 'src/plugins/pricelist/components/CalculationTable/NewPrice.vue';
 | 
					 | 
				
			||||||
import { calc_volume, clone } from '../../utils/utils';
 | 
					 | 
				
			||||||
import { hasPermission } from 'src/utils/permission';
 | 
					 | 
				
			||||||
import { PERMISSIONS } from '../../permissions';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,271 +0,0 @@
 | 
				
			||||||
<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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,59 +0,0 @@
 | 
				
			||||||
<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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,354 +0,0 @@
 | 
				
			||||||
<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 { defineComponent, PropType, ref, onBeforeMount, computed } from 'vue';
 | 
					 | 
				
			||||||
import { Drink, DrinkPriceVolume, usePricelistStore } from '../store';
 | 
					 | 
				
			||||||
import DrinkPriceVolumes from './CalculationTable/DrinkPriceVolumes.vue';
 | 
					 | 
				
			||||||
import { clone, calc_min_prices, DeleteObjects, calc_cost_per_volume } from '../utils/utils';
 | 
					 | 
				
			||||||
import BuildManual from 'src/plugins/pricelist/components/CalculationTable/BuildManual.vue';
 | 
					 | 
				
			||||||
import { baseURL } from 'src/config';
 | 
					 | 
				
			||||||
import { hasPermission } from 'src/utils/permission';
 | 
					 | 
				
			||||||
import { PERMISSIONS } from 'src/plugins/pricelist/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 `${baseURL.value}/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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,113 +0,0 @@
 | 
				
			||||||
<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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,154 +0,0 @@
 | 
				
			||||||
<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 'src/plugins/pricelist/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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,55 +0,0 @@
 | 
				
			||||||
<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 'src/plugins/pricelist/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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,327 +0,0 @@
 | 
				
			||||||
<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 { usePricelistStore, Order } from '../store';
 | 
					 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
import { Search, filter } from 'src/plugins/pricelist/utils/filter';
 | 
					 | 
				
			||||||
import SearchInput from 'src/plugins/pricelist/components/SearchInput.vue';
 | 
					 | 
				
			||||||
import draggable from 'vuedraggable';
 | 
					 | 
				
			||||||
const drag: ComponentPublicInstance = <ComponentPublicInstance>draggable;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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 },
 | 
					 | 
				
			||||||
  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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,89 +0,0 @@
 | 
				
			||||||
<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
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,181 +0,0 @@
 | 
				
			||||||
<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-heigh: 28px;
 | 
					 | 
				
			||||||
  max-width: 28px;
 | 
					 | 
				
			||||||
  max-height: 28px;
 | 
					 | 
				
			||||||
  border-width: 1px;
 | 
					 | 
				
			||||||
  border-color: black;
 | 
					 | 
				
			||||||
  border-radius: 5px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,65 +0,0 @@
 | 
				
			||||||
<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 'src/plugins/pricelist/components/CalculationTable/Ingredients.vue';
 | 
					 | 
				
			||||||
import { DrinkPriceVolume, usePricelistStore } from 'src/plugins/pricelist/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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,38 +0,0 @@
 | 
				
			||||||
<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 'src/plugins/pricelist/components/Pricelist.vue';
 | 
					 | 
				
			||||||
import { usePricelistStore } from 'src/plugins/pricelist/store';
 | 
					 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,36 +0,0 @@
 | 
				
			||||||
<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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,175 +0,0 @@
 | 
				
			||||||
<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 { usePricelistStore } from 'src/plugins/pricelist/store';
 | 
					 | 
				
			||||||
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 { sort } from '../utils/sort';
 | 
					 | 
				
			||||||
import { baseURL } from 'src/config';
 | 
					 | 
				
			||||||
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 `${baseURL.value}/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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,136 +0,0 @@
 | 
				
			||||||
<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 { computed, defineComponent, onBeforeMount, ref } from 'vue';
 | 
					 | 
				
			||||||
import { Screen } from 'quasar';
 | 
					 | 
				
			||||||
import DrinkTypes from 'src/plugins/pricelist/components/DrinkTypes.vue';
 | 
					 | 
				
			||||||
import CalculationTable from 'src/plugins/pricelist/components/CalculationTable.vue';
 | 
					 | 
				
			||||||
import ExtraIngredients from 'src/plugins/pricelist/components/ExtraIngredients.vue';
 | 
					 | 
				
			||||||
import Tags from '../components/Tags.vue';
 | 
					 | 
				
			||||||
import { usePricelistStore } from 'src/plugins/pricelist/store';
 | 
					 | 
				
			||||||
import { hasPermissions } from 'src/utils/permission';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,33 +0,0 @@
 | 
				
			||||||
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',
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,14 +0,0 @@
 | 
				
			||||||
import { innerRoutes, outerRoutes } from './routes';
 | 
					 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const plugin: FG_Plugin.Plugin = {
 | 
					 | 
				
			||||||
  name: 'Pricelist',
 | 
					 | 
				
			||||||
  innerRoutes,
 | 
					 | 
				
			||||||
  outerRoutes,
 | 
					 | 
				
			||||||
  requiredModules: [],
 | 
					 | 
				
			||||||
  requiredBackendModules: ['pricelist'],
 | 
					 | 
				
			||||||
  version: '0.0.1',
 | 
					 | 
				
			||||||
  widgets: [],
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default plugin;
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,73 +0,0 @@
 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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'),
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,313 +0,0 @@
 | 
				
			||||||
import { api } from 'src/boot/axios';
 | 
					 | 
				
			||||||
import { defineStore } from 'pinia';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  calc_volume,
 | 
					 | 
				
			||||||
  calc_cost_per_volume,
 | 
					 | 
				
			||||||
  calc_all_min_prices,
 | 
					 | 
				
			||||||
} from 'src/plugins/pricelist/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 };
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,39 +0,0 @@
 | 
				
			||||||
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 };
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,7 +0,0 @@
 | 
				
			||||||
function sort(a: string | number, b: string | number) {
 | 
					 | 
				
			||||||
  if (a > b) return 1;
 | 
					 | 
				
			||||||
  if (b > a) return -1;
 | 
					 | 
				
			||||||
  return 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export { sort };
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,84 +0,0 @@
 | 
				
			||||||
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 = [];
 | 
					 | 
				
			||||||
  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 };
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,25 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-card class="row justify-center content-center" style="text-align: center">
 | 
					 | 
				
			||||||
    <q-card-section>
 | 
					 | 
				
			||||||
      <div class="text-h6 col-12">Dienste diesen Monat: {{ jobs }}</div>
 | 
					 | 
				
			||||||
      <!--TODO: Filters are deprecated! -->
 | 
					 | 
				
			||||||
      <!--<div class="text-h6 col-12">Nächster Dienst: {{ nextJob | date }}</div>-->
 | 
					 | 
				
			||||||
    </q-card-section>
 | 
					 | 
				
			||||||
  </q-card>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'DummyWidget',
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    function randomNumber(start: number, end: number) {
 | 
					 | 
				
			||||||
      return start + Math.floor(Math.random() * Math.floor(end));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    const jobs = randomNumber(0, 5);
 | 
					 | 
				
			||||||
    const nextJob = new Date(2021, randomNumber(1, 12), randomNumber(1, 31));
 | 
					 | 
				
			||||||
    return { jobs, nextJob };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,245 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-card>
 | 
					 | 
				
			||||||
    <q-form @submit="save()" @reset="reset">
 | 
					 | 
				
			||||||
      <q-card-section class="fit row justify-start content-center items-center">
 | 
					 | 
				
			||||||
        <div class="text-h6 col-xs-12 col-sm-6 q-pa-sm">
 | 
					 | 
				
			||||||
          Veranstaltung <template v-if="modelValue">bearbeiten</template
 | 
					 | 
				
			||||||
          ><template v-else>erstellen</template>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <q-select
 | 
					 | 
				
			||||||
          :model-value="template"
 | 
					 | 
				
			||||||
          filled
 | 
					 | 
				
			||||||
          label="Vorlage"
 | 
					 | 
				
			||||||
          class="col-xs-12 col-sm-6 q-pa-sm"
 | 
					 | 
				
			||||||
          :options="templates"
 | 
					 | 
				
			||||||
          option-label="name"
 | 
					 | 
				
			||||||
          map-options
 | 
					 | 
				
			||||||
          clearable
 | 
					 | 
				
			||||||
          :disable="templates.length == 0"
 | 
					 | 
				
			||||||
          @update:modelValue="fromTemplate"
 | 
					 | 
				
			||||||
          @clear="reset()"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <q-input
 | 
					 | 
				
			||||||
          v-model="event.name"
 | 
					 | 
				
			||||||
          class="col-xs-12 col-sm-6 q-pa-sm"
 | 
					 | 
				
			||||||
          label="Name"
 | 
					 | 
				
			||||||
          type="text"
 | 
					 | 
				
			||||||
          filled
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <q-select
 | 
					 | 
				
			||||||
          v-model="event.type"
 | 
					 | 
				
			||||||
          filled
 | 
					 | 
				
			||||||
          use-input
 | 
					 | 
				
			||||||
          label="Veranstaltungstyp"
 | 
					 | 
				
			||||||
          input-debounce="0"
 | 
					 | 
				
			||||||
          class="col-xs-12 col-sm-6 q-pa-sm"
 | 
					 | 
				
			||||||
          :options="eventtypes"
 | 
					 | 
				
			||||||
          option-label="name"
 | 
					 | 
				
			||||||
          option-value="id"
 | 
					 | 
				
			||||||
          emit-value
 | 
					 | 
				
			||||||
          map-options
 | 
					 | 
				
			||||||
          clearable
 | 
					 | 
				
			||||||
          :rules="[notEmpty]"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <IsoDateInput
 | 
					 | 
				
			||||||
          v-model="event.start"
 | 
					 | 
				
			||||||
          class="col-xs-12 col-sm-6 q-pa-sm"
 | 
					 | 
				
			||||||
          label="Veranstaltungsbeginn"
 | 
					 | 
				
			||||||
          :rules="[notEmpty]"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <IsoDateInput
 | 
					 | 
				
			||||||
          v-model="event.end"
 | 
					 | 
				
			||||||
          class="col-xs-12 col-sm-6 q-pa-sm"
 | 
					 | 
				
			||||||
          label="Veranstaltungsende"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <q-input
 | 
					 | 
				
			||||||
          v-model="event.description"
 | 
					 | 
				
			||||||
          class="col-12 q-pa-sm"
 | 
					 | 
				
			||||||
          label="Beschreibung"
 | 
					 | 
				
			||||||
          type="textarea"
 | 
					 | 
				
			||||||
          filled
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-section v-if="event.template_id === undefined && modelValue === undefined">
 | 
					 | 
				
			||||||
        <q-btn-toggle
 | 
					 | 
				
			||||||
          v-model="recurrent"
 | 
					 | 
				
			||||||
          spread
 | 
					 | 
				
			||||||
          no-caps
 | 
					 | 
				
			||||||
          :options="[
 | 
					 | 
				
			||||||
            { label: 'Einmalig', value: false },
 | 
					 | 
				
			||||||
            { label: 'Wiederkehrend', value: true },
 | 
					 | 
				
			||||||
          ]"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
        <RecurrenceRule v-if="!!recurrent" v-model="recurrenceRule" />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-separator />
 | 
					 | 
				
			||||||
      <q-card-section>
 | 
					 | 
				
			||||||
        <q-btn color="primary" label="Schicht hinzufügen" @click="addJob()" />
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-section v-for="(job, index) in event.jobs" :key="index">
 | 
					 | 
				
			||||||
        <q-card class="q-my-auto">
 | 
					 | 
				
			||||||
          <job
 | 
					 | 
				
			||||||
            v-model="event.jobs[index]"
 | 
					 | 
				
			||||||
            :job-can-delete="jobDeleteDisabled"
 | 
					 | 
				
			||||||
            @remove-job="removeJob(index)"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </q-card>
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-actions align="around">
 | 
					 | 
				
			||||||
        <q-card-actions align="left">
 | 
					 | 
				
			||||||
          <q-btn v-if="!template" color="secondary" label="Neue Vorlage" @click="save(true)" />
 | 
					 | 
				
			||||||
          <q-btn v-else color="negative" label="Vorlage löschen" @click="removeTemplate" />
 | 
					 | 
				
			||||||
        </q-card-actions>
 | 
					 | 
				
			||||||
        <q-card-actions align="right">
 | 
					 | 
				
			||||||
          <q-btn label="Zurücksetzen" type="reset" />
 | 
					 | 
				
			||||||
          <q-btn color="primary" type="submit" label="Speichern" />
 | 
					 | 
				
			||||||
        </q-card-actions>
 | 
					 | 
				
			||||||
      </q-card-actions>
 | 
					 | 
				
			||||||
    </q-form>
 | 
					 | 
				
			||||||
  </q-card>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { computed, defineComponent, PropType, ref, onBeforeMount } from 'vue';
 | 
					 | 
				
			||||||
import { date, ModifyDateOptions } from 'quasar';
 | 
					 | 
				
			||||||
import { useScheduleStore } from '../../store';
 | 
					 | 
				
			||||||
import { notEmpty } from 'src/utils/validators';
 | 
					 | 
				
			||||||
import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
 | 
					 | 
				
			||||||
import Job from './Job.vue';
 | 
					 | 
				
			||||||
import RecurrenceRule from './RecurrenceRule.vue';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'EditEvent',
 | 
					 | 
				
			||||||
  components: { IsoDateInput, Job, RecurrenceRule },
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    modelValue: {
 | 
					 | 
				
			||||||
      required: false,
 | 
					 | 
				
			||||||
      default: () => undefined,
 | 
					 | 
				
			||||||
      type: Object as PropType<FG.Event | undefined>,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  emits: {
 | 
					 | 
				
			||||||
    done: (val: boolean) => typeof val === 'boolean',
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  setup(props, { emit }) {
 | 
					 | 
				
			||||||
    const store = useScheduleStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const emptyJob = {
 | 
					 | 
				
			||||||
      id: NaN,
 | 
					 | 
				
			||||||
      start: new Date(),
 | 
					 | 
				
			||||||
      end: date.addToDate(new Date(), { hours: 1 }),
 | 
					 | 
				
			||||||
      services: [],
 | 
					 | 
				
			||||||
      required_services: 2,
 | 
					 | 
				
			||||||
      type: store.jobTypes[0],
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const emptyEvent = {
 | 
					 | 
				
			||||||
      id: NaN,
 | 
					 | 
				
			||||||
      start: new Date(),
 | 
					 | 
				
			||||||
      jobs: [Object.assign({}, emptyJob)],
 | 
					 | 
				
			||||||
      type: store.eventTypes[0],
 | 
					 | 
				
			||||||
      is_template: false,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const templates = computed(() => store.templates);
 | 
					 | 
				
			||||||
    const template = ref<FG.Event | undefined>(undefined);
 | 
					 | 
				
			||||||
    const event = ref<FG.Event>(props.modelValue || Object.assign({}, emptyEvent));
 | 
					 | 
				
			||||||
    const eventtypes = computed(() => store.eventTypes);
 | 
					 | 
				
			||||||
    const jobDeleteDisabled = computed(() => event.value.jobs.length < 2);
 | 
					 | 
				
			||||||
    const recurrent = ref(false);
 | 
					 | 
				
			||||||
    const recurrenceRule = ref<FG.RecurrenceRule>({ frequency: 'daily', interval: 1 });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onBeforeMount(() => {
 | 
					 | 
				
			||||||
      void store.getEventTypes();
 | 
					 | 
				
			||||||
      void store.getJobTypes();
 | 
					 | 
				
			||||||
      void store.getTemplates();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function addJob() {
 | 
					 | 
				
			||||||
      event.value.jobs.push(Object.assign({}, emptyJob));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function removeJob(index: number) {
 | 
					 | 
				
			||||||
      event.value.jobs.splice(index, 1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function fromTemplate(tpl: FG.Event) {
 | 
					 | 
				
			||||||
      template.value = tpl;
 | 
					 | 
				
			||||||
      event.value = Object.assign({}, tpl);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function save(template = false) {
 | 
					 | 
				
			||||||
      event.value.is_template = template;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        await store.addEvent(event.value);
 | 
					 | 
				
			||||||
        if (props.modelValue === undefined && recurrent.value && !event.value.is_template) {
 | 
					 | 
				
			||||||
          let count = 0;
 | 
					 | 
				
			||||||
          const options: ModifyDateOptions = {};
 | 
					 | 
				
			||||||
          switch (recurrenceRule.value.frequency) {
 | 
					 | 
				
			||||||
            case 'daily':
 | 
					 | 
				
			||||||
              options['days'] = 1 * recurrenceRule.value.interval;
 | 
					 | 
				
			||||||
              break;
 | 
					 | 
				
			||||||
            case 'weekly':
 | 
					 | 
				
			||||||
              options['days'] = 7 * recurrenceRule.value.interval;
 | 
					 | 
				
			||||||
              break;
 | 
					 | 
				
			||||||
            case 'monthly':
 | 
					 | 
				
			||||||
              options['months'] = 1 * recurrenceRule.value.interval;
 | 
					 | 
				
			||||||
              break;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          while (true) {
 | 
					 | 
				
			||||||
            event.value.start = date.addToDate(event.value.start, options);
 | 
					 | 
				
			||||||
            if (event.value.end) event.value.end = date.addToDate(event.value.end, options);
 | 
					 | 
				
			||||||
            event.value.jobs.forEach((job) => {
 | 
					 | 
				
			||||||
              job.start = date.addToDate(job.start, options);
 | 
					 | 
				
			||||||
              if (job.end) job.end = date.addToDate(job.end, options);
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            count++;
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
              count <= 120 &&
 | 
					 | 
				
			||||||
              (!recurrenceRule.value.count || count <= recurrenceRule.value.count) &&
 | 
					 | 
				
			||||||
              (!recurrenceRule.value.until || event.value.start < recurrenceRule.value.until)
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
              await store.addEvent(event.value);
 | 
					 | 
				
			||||||
            else break;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        reset();
 | 
					 | 
				
			||||||
        emit('done', true);
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        console.error(error);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function removeTemplate() {
 | 
					 | 
				
			||||||
      if (template.value !== undefined) {
 | 
					 | 
				
			||||||
        await store.removeEvent(template.value.id);
 | 
					 | 
				
			||||||
        template.value = undefined;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function reset() {
 | 
					 | 
				
			||||||
      event.value = Object.assign({}, props.modelValue || emptyEvent);
 | 
					 | 
				
			||||||
      template.value = undefined;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      jobDeleteDisabled,
 | 
					 | 
				
			||||||
      addJob,
 | 
					 | 
				
			||||||
      eventtypes,
 | 
					 | 
				
			||||||
      templates,
 | 
					 | 
				
			||||||
      removeJob,
 | 
					 | 
				
			||||||
      notEmpty,
 | 
					 | 
				
			||||||
      save,
 | 
					 | 
				
			||||||
      reset,
 | 
					 | 
				
			||||||
      recurrent,
 | 
					 | 
				
			||||||
      fromTemplate,
 | 
					 | 
				
			||||||
      removeTemplate,
 | 
					 | 
				
			||||||
      template,
 | 
					 | 
				
			||||||
      recurrenceRule,
 | 
					 | 
				
			||||||
      event,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style></style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,126 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <q-dialog v-model="edittype">
 | 
					 | 
				
			||||||
      <q-card>
 | 
					 | 
				
			||||||
        <q-card-section>
 | 
					 | 
				
			||||||
          <div class="text-h6">Editere Diensttyp {{ actualEvent.name }}</div>
 | 
					 | 
				
			||||||
        </q-card-section>
 | 
					 | 
				
			||||||
        <q-card-section>
 | 
					 | 
				
			||||||
          <q-input v-model="newEventName" dense label="name" filled />
 | 
					 | 
				
			||||||
        </q-card-section>
 | 
					 | 
				
			||||||
        <q-card-actions>
 | 
					 | 
				
			||||||
          <q-btn flat color="danger" label="Abbrechen" @click="discardChanges()" />
 | 
					 | 
				
			||||||
          <q-btn flat color="primary" label="Speichern" @click="saveChanges()" />
 | 
					 | 
				
			||||||
        </q-card-actions>
 | 
					 | 
				
			||||||
      </q-card>
 | 
					 | 
				
			||||||
    </q-dialog>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <q-card-section>
 | 
					 | 
				
			||||||
        <q-table title="Veranstaltungstypen" :rows="rows" row-key="jobid" :columns="columns">
 | 
					 | 
				
			||||||
          <template #top-right>
 | 
					 | 
				
			||||||
            <q-input v-model="newEventType" dense placeholder="Neuer Typ" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <div></div>
 | 
					 | 
				
			||||||
            <q-btn color="primary" icon="mdi-plus" label="Hinzufügen" @click="addType" />
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
          <template #body-cell-actions="props">
 | 
					 | 
				
			||||||
            <!-- <q-btn :label="item"> -->
 | 
					 | 
				
			||||||
            <!-- {{ item.row.name }} -->
 | 
					 | 
				
			||||||
            <q-td :props="props" align="right" :auto-width="true">
 | 
					 | 
				
			||||||
              <q-btn
 | 
					 | 
				
			||||||
                round
 | 
					 | 
				
			||||||
                icon="mdi-pencil"
 | 
					 | 
				
			||||||
                @click="editType({ id: props.row.id, name: props.row.name })"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              <q-btn round icon="mdi-delete" @click="deleteType(props.row.id)" />
 | 
					 | 
				
			||||||
            </q-td>
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
        </q-table>
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { defineComponent, ref, computed, onBeforeMount } from 'vue';
 | 
					 | 
				
			||||||
import { useScheduleStore } from '../../store';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'EventTypes',
 | 
					 | 
				
			||||||
  components: {},
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    const store = useScheduleStore();
 | 
					 | 
				
			||||||
    const newEventType = ref('');
 | 
					 | 
				
			||||||
    const edittype = ref(false);
 | 
					 | 
				
			||||||
    const emptyEvent: FG.EventType = { id: -1, name: '' };
 | 
					 | 
				
			||||||
    const actualEvent = ref(emptyEvent);
 | 
					 | 
				
			||||||
    const newEventName = ref('');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onBeforeMount(async () => await store.getEventTypes());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const rows = computed(() => store.eventTypes);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const columns = [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name: 'name',
 | 
					 | 
				
			||||||
        label: 'Veranstaltungstyp',
 | 
					 | 
				
			||||||
        field: 'name',
 | 
					 | 
				
			||||||
        align: 'left',
 | 
					 | 
				
			||||||
        sortable: true,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name: 'actions',
 | 
					 | 
				
			||||||
        label: 'Aktionen',
 | 
					 | 
				
			||||||
        field: 'actions',
 | 
					 | 
				
			||||||
        align: 'right',
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function addType() {
 | 
					 | 
				
			||||||
      await store.addEventType(newEventType.value);
 | 
					 | 
				
			||||||
      // if null then conflict with name
 | 
					 | 
				
			||||||
      newEventType.value = '';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function editType(event: FG.EventType) {
 | 
					 | 
				
			||||||
      edittype.value = true;
 | 
					 | 
				
			||||||
      actualEvent.value = event;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function saveChanges() {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        await store.renameEventType(actualEvent.value.id, newEventName.value);
 | 
					 | 
				
			||||||
      } finally {
 | 
					 | 
				
			||||||
        discardChanges();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function discardChanges() {
 | 
					 | 
				
			||||||
      actualEvent.value = emptyEvent;
 | 
					 | 
				
			||||||
      newEventName.value = '';
 | 
					 | 
				
			||||||
      edittype.value = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function deleteType(id: number) {
 | 
					 | 
				
			||||||
      await store.removeEventType(id);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      columns,
 | 
					 | 
				
			||||||
      rows,
 | 
					 | 
				
			||||||
      addType,
 | 
					 | 
				
			||||||
      newEventType,
 | 
					 | 
				
			||||||
      deleteType,
 | 
					 | 
				
			||||||
      edittype,
 | 
					 | 
				
			||||||
      editType,
 | 
					 | 
				
			||||||
      actualEvent,
 | 
					 | 
				
			||||||
      newEventName,
 | 
					 | 
				
			||||||
      discardChanges,
 | 
					 | 
				
			||||||
      saveChanges,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style scoped></style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,113 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-card-section class="fit row justify-start content-center items-center">
 | 
					 | 
				
			||||||
    <q-card-section class="fit row justify-start content-center items-center">
 | 
					 | 
				
			||||||
      <IsoDateInput
 | 
					 | 
				
			||||||
        v-model="job.start"
 | 
					 | 
				
			||||||
        class="col-xs-12 col-sm-6 q-pa-sm"
 | 
					 | 
				
			||||||
        label="Beginn"
 | 
					 | 
				
			||||||
        type="datetime"
 | 
					 | 
				
			||||||
        :rules="[notEmpty]"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <IsoDateInput
 | 
					 | 
				
			||||||
        v-model="job.end"
 | 
					 | 
				
			||||||
        class="col-xs-12 col-sm-6 q-pa-sm"
 | 
					 | 
				
			||||||
        label="Ende"
 | 
					 | 
				
			||||||
        type="datetime"
 | 
					 | 
				
			||||||
        :rules="[notEmpty, isAfterDate]"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <q-select
 | 
					 | 
				
			||||||
        v-model="job.type"
 | 
					 | 
				
			||||||
        filled
 | 
					 | 
				
			||||||
        use-input
 | 
					 | 
				
			||||||
        label="Dienstart"
 | 
					 | 
				
			||||||
        input-debounce="0"
 | 
					 | 
				
			||||||
        class="col-xs-12 col-sm-6 q-pa-sm"
 | 
					 | 
				
			||||||
        :options="jobtypes"
 | 
					 | 
				
			||||||
        option-label="name"
 | 
					 | 
				
			||||||
        option-value="id"
 | 
					 | 
				
			||||||
        map-options
 | 
					 | 
				
			||||||
        clearable
 | 
					 | 
				
			||||||
        :rules="[notEmpty]"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <q-input
 | 
					 | 
				
			||||||
        v-model="job.required_services"
 | 
					 | 
				
			||||||
        filled
 | 
					 | 
				
			||||||
        class="col-xs-12 col-sm-6 q-pa-sm"
 | 
					 | 
				
			||||||
        label="Dienstanzahl"
 | 
					 | 
				
			||||||
        type="number"
 | 
					 | 
				
			||||||
        :rules="[notEmpty]"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <q-input
 | 
					 | 
				
			||||||
        v-model="job.comment"
 | 
					 | 
				
			||||||
        class="col-12 q-pa-sm"
 | 
					 | 
				
			||||||
        label="Beschreibung"
 | 
					 | 
				
			||||||
        type="textarea"
 | 
					 | 
				
			||||||
        filled
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </q-card-section>
 | 
					 | 
				
			||||||
    <q-btn label="Schicht löschen" color="negative" :disabled="jobCanDelete" @click="removeJob" />
 | 
					 | 
				
			||||||
  </q-card-section>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { defineComponent, computed, onBeforeMount, PropType } from 'vue';
 | 
					 | 
				
			||||||
import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
 | 
					 | 
				
			||||||
import { notEmpty } from 'src/utils/validators';
 | 
					 | 
				
			||||||
import { useScheduleStore } from '../../store';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'Job',
 | 
					 | 
				
			||||||
  components: { IsoDateInput },
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    modelValue: {
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
      type: Object as PropType<FG.Job>,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    jobCanDelete: Boolean,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  emits: {
 | 
					 | 
				
			||||||
    'remove-job': () => true,
 | 
					 | 
				
			||||||
    'update:modelValue': (job: FG.Job) => !!job,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  setup(props, { emit }) {
 | 
					 | 
				
			||||||
    const store = useScheduleStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onBeforeMount(() => store.getJobTypes());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const jobtypes = computed(() => store.jobTypes);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const job = new Proxy(props.modelValue, {
 | 
					 | 
				
			||||||
      get(target, prop) {
 | 
					 | 
				
			||||||
        if (typeof prop === 'string') {
 | 
					 | 
				
			||||||
          return ((props.modelValue as unknown) as Record<string, unknown>)[prop];
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      set(obj, prop, value) {
 | 
					 | 
				
			||||||
        if (typeof prop === 'string') {
 | 
					 | 
				
			||||||
          // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
 | 
					 | 
				
			||||||
          emit('update:modelValue', Object.assign({}, props.modelValue, { [prop]: value }));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function removeJob() {
 | 
					 | 
				
			||||||
      emit('remove-job');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function isAfterDate(val: string) {
 | 
					 | 
				
			||||||
      return props.modelValue.start < new Date(val) || 'Ende muss hinter dem Start liegen';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      job,
 | 
					 | 
				
			||||||
      jobtypes,
 | 
					 | 
				
			||||||
      removeJob,
 | 
					 | 
				
			||||||
      notEmpty,
 | 
					 | 
				
			||||||
      isAfterDate,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style></style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,125 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <q-dialog v-model="edittype">
 | 
					 | 
				
			||||||
      <q-card>
 | 
					 | 
				
			||||||
        <q-card-section>
 | 
					 | 
				
			||||||
          <div class="text-h6">Editere Diensttyp {{ actualJob.name }}</div>
 | 
					 | 
				
			||||||
        </q-card-section>
 | 
					 | 
				
			||||||
        <q-card-section>
 | 
					 | 
				
			||||||
          <q-input v-model="newJobName" dense label="name" filled />
 | 
					 | 
				
			||||||
        </q-card-section>
 | 
					 | 
				
			||||||
        <q-card-actions>
 | 
					 | 
				
			||||||
          <q-btn flat color="danger" label="Abbrechen" @click="discardChanges()" />
 | 
					 | 
				
			||||||
          <q-btn flat color="primary" label="Speichern" @click="saveChanges()" />
 | 
					 | 
				
			||||||
        </q-card-actions>
 | 
					 | 
				
			||||||
      </q-card>
 | 
					 | 
				
			||||||
    </q-dialog>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <q-card-section>
 | 
					 | 
				
			||||||
        <q-table title="Diensttypen" :rows="rows" row-key="jobid" :columns="columns">
 | 
					 | 
				
			||||||
          <template #top-right>
 | 
					 | 
				
			||||||
            <q-input v-model="newJob" dense placeholder="Neuer Typ" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <div></div>
 | 
					 | 
				
			||||||
            <q-btn color="primary" icon="mdi-plus" label="Hinzufügen" @click="addType" />
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
          <template #body-cell-actions="props">
 | 
					 | 
				
			||||||
            <!-- <q-btn :label="item"> -->
 | 
					 | 
				
			||||||
            <!-- {{ item.row.name }} -->
 | 
					 | 
				
			||||||
            <q-td :props="props" align="right" :auto-width="true">
 | 
					 | 
				
			||||||
              <q-btn
 | 
					 | 
				
			||||||
                round
 | 
					 | 
				
			||||||
                icon="mdi-pencil"
 | 
					 | 
				
			||||||
                @click="editType({ id: props.row.id, name: props.row.name })"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
              <q-btn round icon="mdi-delete" @click="deleteType(props.row.id)" />
 | 
					 | 
				
			||||||
            </q-td>
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
        </q-table>
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { defineComponent, ref, computed, onBeforeMount } from 'vue';
 | 
					 | 
				
			||||||
import { useScheduleStore } from '../../store';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'JobTypes',
 | 
					 | 
				
			||||||
  components: {},
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    const store = useScheduleStore();
 | 
					 | 
				
			||||||
    const newJob = ref('');
 | 
					 | 
				
			||||||
    const edittype = ref(false);
 | 
					 | 
				
			||||||
    const emptyJob: FG.JobType = { id: -1, name: '' };
 | 
					 | 
				
			||||||
    const actualJob = ref(emptyJob);
 | 
					 | 
				
			||||||
    const newJobName = ref('');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onBeforeMount(() => store.getJobTypes());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const rows = computed(() => store.jobTypes);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const columns = [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name: 'jobname',
 | 
					 | 
				
			||||||
        label: 'Name',
 | 
					 | 
				
			||||||
        field: 'name',
 | 
					 | 
				
			||||||
        align: 'left',
 | 
					 | 
				
			||||||
        sortable: true,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name: 'actions',
 | 
					 | 
				
			||||||
        label: 'Aktionen',
 | 
					 | 
				
			||||||
        field: 'actions',
 | 
					 | 
				
			||||||
        align: 'right',
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function addType() {
 | 
					 | 
				
			||||||
      await store.addJobType(newJob.value);
 | 
					 | 
				
			||||||
      newJob.value = '';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function editType(job: FG.JobType) {
 | 
					 | 
				
			||||||
      edittype.value = true;
 | 
					 | 
				
			||||||
      actualJob.value = job;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function saveChanges() {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        await store.renameJobType(actualJob.value.id, newJobName.value);
 | 
					 | 
				
			||||||
      } finally {
 | 
					 | 
				
			||||||
        discardChanges();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function discardChanges() {
 | 
					 | 
				
			||||||
      actualJob.value = emptyJob;
 | 
					 | 
				
			||||||
      newJobName.value = '';
 | 
					 | 
				
			||||||
      edittype.value = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function deleteType(id: number) {
 | 
					 | 
				
			||||||
      void store.removeJobType(id);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      columns,
 | 
					 | 
				
			||||||
      rows,
 | 
					 | 
				
			||||||
      addType,
 | 
					 | 
				
			||||||
      newJob,
 | 
					 | 
				
			||||||
      deleteType,
 | 
					 | 
				
			||||||
      edittype,
 | 
					 | 
				
			||||||
      editType,
 | 
					 | 
				
			||||||
      actualJob,
 | 
					 | 
				
			||||||
      newJobName,
 | 
					 | 
				
			||||||
      discardChanges,
 | 
					 | 
				
			||||||
      saveChanges,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style scoped></style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,96 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-card class="fit row justify-start content-center items-center">
 | 
					 | 
				
			||||||
    <q-input
 | 
					 | 
				
			||||||
      v-model="rule.interval"
 | 
					 | 
				
			||||||
      filled
 | 
					 | 
				
			||||||
      class="col-xs-12 col-sm-6 q-pa-sm"
 | 
					 | 
				
			||||||
      label="Interval"
 | 
					 | 
				
			||||||
      type="number"
 | 
					 | 
				
			||||||
      :rules="[notEmpty]"
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
    <q-select
 | 
					 | 
				
			||||||
      v-model="rule.frequency"
 | 
					 | 
				
			||||||
      filled
 | 
					 | 
				
			||||||
      label="Wiederholung"
 | 
					 | 
				
			||||||
      input-debounce="200"
 | 
					 | 
				
			||||||
      class="col-xs-12 col-sm-6 q-pa-sm"
 | 
					 | 
				
			||||||
      :options="freqTypes"
 | 
					 | 
				
			||||||
      emit-value
 | 
					 | 
				
			||||||
      map-options
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
    <q-input
 | 
					 | 
				
			||||||
      v-model="rule.count"
 | 
					 | 
				
			||||||
      filled
 | 
					 | 
				
			||||||
      class="col-xs-12 col-sm-6 q-pa-sm"
 | 
					 | 
				
			||||||
      label="Anzahl Wiederholungen"
 | 
					 | 
				
			||||||
      type="number"
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
    <IsoDateInput
 | 
					 | 
				
			||||||
      v-model="rule.until"
 | 
					 | 
				
			||||||
      class="col-xs-12 col-sm-6 q-pa-sm"
 | 
					 | 
				
			||||||
      label="Wiederholen bis"
 | 
					 | 
				
			||||||
      type="date"
 | 
					 | 
				
			||||||
    />
 | 
					 | 
				
			||||||
  </q-card>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
 | 
					 | 
				
			||||||
import { defineComponent, PropType } from 'vue';
 | 
					 | 
				
			||||||
import { notEmpty } from 'src/utils/validators';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'RecurrenceRule',
 | 
					 | 
				
			||||||
  components: { IsoDateInput },
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    modelValue: {
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
      type: Object as PropType<FG.RecurrenceRule>,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  emits: {
 | 
					 | 
				
			||||||
    'update:modelValue': (rule: FG.RecurrenceRule) => !!rule,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  setup(props, { emit }) {
 | 
					 | 
				
			||||||
    const freqTypes = [
 | 
					 | 
				
			||||||
      { label: 'Täglich', value: 'daily' },
 | 
					 | 
				
			||||||
      { label: 'Wöchentlich', value: 'weekly' },
 | 
					 | 
				
			||||||
      { label: 'Monatlich', value: 'monthly' },
 | 
					 | 
				
			||||||
      { label: 'Jährlich', value: 'yearly' },
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const rule = new Proxy(props.modelValue, {
 | 
					 | 
				
			||||||
      get(target, prop) {
 | 
					 | 
				
			||||||
        if (typeof prop === 'string') {
 | 
					 | 
				
			||||||
          return ((props.modelValue as unknown) as Record<string, unknown>)[prop];
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      set(target, prop, value) {
 | 
					 | 
				
			||||||
        if (typeof prop === 'string') {
 | 
					 | 
				
			||||||
          const obj = Object.assign({}, props.modelValue);
 | 
					 | 
				
			||||||
          if (prop == 'frequency' && typeof value === 'string') obj.frequency = value;
 | 
					 | 
				
			||||||
          else if (prop == 'interval') {
 | 
					 | 
				
			||||||
            obj.interval = typeof value === 'string' ? parseInt(value) : <number>value;
 | 
					 | 
				
			||||||
          } else if (prop == 'count') {
 | 
					 | 
				
			||||||
            obj.until = undefined;
 | 
					 | 
				
			||||||
            obj.count = typeof value === 'string' ? parseInt(value) : <number>value;
 | 
					 | 
				
			||||||
          } else if (prop == 'until' && (value instanceof Date || value === undefined)) {
 | 
					 | 
				
			||||||
            obj.count = undefined;
 | 
					 | 
				
			||||||
            obj.until = <Date | undefined>value;
 | 
					 | 
				
			||||||
          } else return false;
 | 
					 | 
				
			||||||
          emit('update:modelValue', obj);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      rule,
 | 
					 | 
				
			||||||
      notEmpty,
 | 
					 | 
				
			||||||
      freqTypes,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style></style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,241 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-dialog
 | 
					 | 
				
			||||||
    :model-value="editor !== undefined"
 | 
					 | 
				
			||||||
    persistent
 | 
					 | 
				
			||||||
    transition-show="scale"
 | 
					 | 
				
			||||||
    transition-hide="scale"
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <div class="column">
 | 
					 | 
				
			||||||
        <div class="col" align="right" style="position: sticky; top: 0; z-index: 999">
 | 
					 | 
				
			||||||
          <q-btn round color="negative" icon="close" dense rounded @click="editDone(false)" />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div class="col" style="margin: 0; padding: 0; margin-top: -2.4em">
 | 
					 | 
				
			||||||
          <edit-event v-model="editor" @done="editDone" />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-dialog>
 | 
					 | 
				
			||||||
  <q-page padding>
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <div style="max-width: 1800px; width: 100%">
 | 
					 | 
				
			||||||
        <q-toolbar class="bg-primary text-white q-my-md shadow-2 items-center row justify-center">
 | 
					 | 
				
			||||||
          <div class="row justify-center items-center">
 | 
					 | 
				
			||||||
            <q-btn flat dense label="Prev" @click="calendarPrev" />
 | 
					 | 
				
			||||||
            <q-separator vertical />
 | 
					 | 
				
			||||||
            <q-btn flat dense
 | 
					 | 
				
			||||||
              >{{ asMonth(selectedDate) }} {{ asYear(selectedDate) }}
 | 
					 | 
				
			||||||
              <q-popup-proxy
 | 
					 | 
				
			||||||
                transition-show="scale"
 | 
					 | 
				
			||||||
                transition-hide="scale"
 | 
					 | 
				
			||||||
                @before-show="updateProxy"
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <q-date v-model="proxyDate">
 | 
					 | 
				
			||||||
                  <div class="row items-center justify-end q-gutter-sm">
 | 
					 | 
				
			||||||
                    <q-btn v-close-popup label="Cancel" color="primary" flat />
 | 
					 | 
				
			||||||
                    <q-btn
 | 
					 | 
				
			||||||
                      v-close-popup
 | 
					 | 
				
			||||||
                      label="OK"
 | 
					 | 
				
			||||||
                      color="primary"
 | 
					 | 
				
			||||||
                      flat
 | 
					 | 
				
			||||||
                      @click="saveNewSelectedDate(proxyDate)"
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                </q-date>
 | 
					 | 
				
			||||||
              </q-popup-proxy>
 | 
					 | 
				
			||||||
            </q-btn>
 | 
					 | 
				
			||||||
            <q-separator vertical />
 | 
					 | 
				
			||||||
            <q-btn flat dense label="Next" @click="calendarNext" />
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <!-- <q-space /> -->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <q-btn-toggle
 | 
					 | 
				
			||||||
            v-model="calendarView"
 | 
					 | 
				
			||||||
            class="row absolute-right"
 | 
					 | 
				
			||||||
            flat
 | 
					 | 
				
			||||||
            stretch
 | 
					 | 
				
			||||||
            toggle-color=""
 | 
					 | 
				
			||||||
            :options="[
 | 
					 | 
				
			||||||
              { label: 'Tag', value: 'day' },
 | 
					 | 
				
			||||||
              { label: 'Woche', value: 'week' },
 | 
					 | 
				
			||||||
            ]"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </q-toolbar>
 | 
					 | 
				
			||||||
        <q-calendar-agenda
 | 
					 | 
				
			||||||
          v-model="selectedDate"
 | 
					 | 
				
			||||||
          :view="calendarRealView"
 | 
					 | 
				
			||||||
          :max-days="calendarDays"
 | 
					 | 
				
			||||||
          :weekdays="[1, 2, 3, 4, 5, 6, 0]"
 | 
					 | 
				
			||||||
          locale="de-de"
 | 
					 | 
				
			||||||
          style="height: 100%; min-height: 400px"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <template #day="{ scope: { timestamp } }">
 | 
					 | 
				
			||||||
            <div itemref="" class="q-pb-sm" style="min-height: 200px">
 | 
					 | 
				
			||||||
              <eventslot
 | 
					 | 
				
			||||||
                v-for="(agenda, index) in events[timestamp.weekday]"
 | 
					 | 
				
			||||||
                :key="index"
 | 
					 | 
				
			||||||
                v-model="events[timestamp.weekday][index]"
 | 
					 | 
				
			||||||
                @removeEvent="remove"
 | 
					 | 
				
			||||||
                @editEvent="edit"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </template>
 | 
					 | 
				
			||||||
        </q-calendar-agenda>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-page>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { computed, defineComponent, onBeforeMount, ref } from 'vue';
 | 
					 | 
				
			||||||
import { useScheduleStore } from '../../store';
 | 
					 | 
				
			||||||
import Eventslot from './slots/EventSlot.vue';
 | 
					 | 
				
			||||||
import { date } from 'quasar';
 | 
					 | 
				
			||||||
import { startOfWeek } from 'src/utils/datetime';
 | 
					 | 
				
			||||||
import EditEvent from '../management/EditEvent.vue';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'AgendaView',
 | 
					 | 
				
			||||||
  components: { Eventslot, EditEvent },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    const store = useScheduleStore();
 | 
					 | 
				
			||||||
    const windowWidth = ref(window.innerWidth);
 | 
					 | 
				
			||||||
    const selectedDate = ref(date.formatDate(new Date(), 'YYYY-MM-DD'));
 | 
					 | 
				
			||||||
    const proxyDate = ref('');
 | 
					 | 
				
			||||||
    const calendarView = ref('week');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const calendarRealView = computed(() => (calendarDays.value != 7 ? 'day' : 'week'));
 | 
					 | 
				
			||||||
    const calendarDays = computed(() =>
 | 
					 | 
				
			||||||
      // <= 1023 is the breakpoint for sm to md
 | 
					 | 
				
			||||||
      calendarView.value == 'day' ? 1 : windowWidth.value <= 1023 ? 3 : 7
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    const events = ref<Agendas>({});
 | 
					 | 
				
			||||||
    const editor = ref<FG.Event | undefined>(undefined);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    interface Agendas {
 | 
					 | 
				
			||||||
      [index: number]: FG.Event[];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onBeforeMount(async () => {
 | 
					 | 
				
			||||||
      window.addEventListener('resize', () => {
 | 
					 | 
				
			||||||
        windowWidth.value = window.innerWidth;
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await loadAgendas();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function edit(id: number) {
 | 
					 | 
				
			||||||
      editor.value = await store.getEvent(id);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    function editDone(changed: boolean) {
 | 
					 | 
				
			||||||
      if (changed) void loadAgendas();
 | 
					 | 
				
			||||||
      editor.value = undefined;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function remove(id: number) {
 | 
					 | 
				
			||||||
      if (await store.removeEvent(id)) {
 | 
					 | 
				
			||||||
        // Successfull removed
 | 
					 | 
				
			||||||
        for (const idx in events.value) {
 | 
					 | 
				
			||||||
          const i = events.value[idx].findIndex((event) => event.id === id);
 | 
					 | 
				
			||||||
          if (i !== -1) {
 | 
					 | 
				
			||||||
            events.value[idx].splice(i, 1);
 | 
					 | 
				
			||||||
            break;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        // Not found, this means our eventa are outdated
 | 
					 | 
				
			||||||
        await loadAgendas();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function loadAgendas() {
 | 
					 | 
				
			||||||
      const selected = new Date(selectedDate.value);
 | 
					 | 
				
			||||||
      console.log(selected);
 | 
					 | 
				
			||||||
      const start = calendarRealView.value === 'day' ? selected : startOfWeek(selected);
 | 
					 | 
				
			||||||
      const end = date.addToDate(start, { days: calendarDays.value });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      events.value = {};
 | 
					 | 
				
			||||||
      const list = await store.getEvents({ from: start, to: end });
 | 
					 | 
				
			||||||
      list.forEach((event) => {
 | 
					 | 
				
			||||||
        const day = event.start.getDay();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!events.value[day]) {
 | 
					 | 
				
			||||||
          events.value[day] = [];
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        events.value[day].push(event);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function calendarNext() {
 | 
					 | 
				
			||||||
      selectedDate.value = date.formatDate(
 | 
					 | 
				
			||||||
        date.addToDate(selectedDate.value, { days: calendarDays.value }),
 | 
					 | 
				
			||||||
        'YYYY-MM-DD'
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      void loadAgendas();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function calendarPrev() {
 | 
					 | 
				
			||||||
      selectedDate.value = date.formatDate(
 | 
					 | 
				
			||||||
        date.subtractFromDate(selectedDate.value, { days: calendarDays.value }),
 | 
					 | 
				
			||||||
        'YYYY-MM-DD'
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      void loadAgendas();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function updateProxy() {
 | 
					 | 
				
			||||||
      proxyDate.value = selectedDate.value;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    function saveNewSelectedDate() {
 | 
					 | 
				
			||||||
      proxyDate.value = date.formatDate(proxyDate.value, 'YYYY-MM-DD');
 | 
					 | 
				
			||||||
      selectedDate.value = proxyDate.value;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    function asMonth(value: string) {
 | 
					 | 
				
			||||||
      if (value) {
 | 
					 | 
				
			||||||
        return date.formatDate(new Date(value), 'MMMM', {
 | 
					 | 
				
			||||||
          months: [
 | 
					 | 
				
			||||||
            'Januar',
 | 
					 | 
				
			||||||
            'Februar',
 | 
					 | 
				
			||||||
            'März',
 | 
					 | 
				
			||||||
            'April',
 | 
					 | 
				
			||||||
            'Mai',
 | 
					 | 
				
			||||||
            'Juni',
 | 
					 | 
				
			||||||
            'Juli',
 | 
					 | 
				
			||||||
            'August',
 | 
					 | 
				
			||||||
            'September',
 | 
					 | 
				
			||||||
            'Oktober',
 | 
					 | 
				
			||||||
            'November',
 | 
					 | 
				
			||||||
            'Dezember',
 | 
					 | 
				
			||||||
          ],
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    function asYear(value: string) {
 | 
					 | 
				
			||||||
      if (value) {
 | 
					 | 
				
			||||||
        return date.formatDate(new Date(value), 'YYYY');
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      asYear,
 | 
					 | 
				
			||||||
      asMonth,
 | 
					 | 
				
			||||||
      selectedDate,
 | 
					 | 
				
			||||||
      edit,
 | 
					 | 
				
			||||||
      editor,
 | 
					 | 
				
			||||||
      editDone,
 | 
					 | 
				
			||||||
      events,
 | 
					 | 
				
			||||||
      calendarNext,
 | 
					 | 
				
			||||||
      calendarPrev,
 | 
					 | 
				
			||||||
      updateProxy,
 | 
					 | 
				
			||||||
      saveNewSelectedDate,
 | 
					 | 
				
			||||||
      proxyDate,
 | 
					 | 
				
			||||||
      remove,
 | 
					 | 
				
			||||||
      calendarDays,
 | 
					 | 
				
			||||||
      calendarView,
 | 
					 | 
				
			||||||
      calendarRealView,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style></style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,99 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-card
 | 
					 | 
				
			||||||
    class="q-mx-xs q-mt-sm justify-start content-center items-center rounded-borders shadow-5"
 | 
					 | 
				
			||||||
    bordered
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <q-card-section class="text-primary q-pa-xs">
 | 
					 | 
				
			||||||
      <div class="text-weight-bolder text-center" style="font-size: 1.5vw">
 | 
					 | 
				
			||||||
        {{ event.type.name }}
 | 
					 | 
				
			||||||
        <template v-if="event.name"
 | 
					 | 
				
			||||||
          >: <span style="font-size: 1.2vw">{{ event.name }}</span>
 | 
					 | 
				
			||||||
        </template>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <div v-if="event.description" class="text-weight-medium" style="font-size: 1vw">
 | 
					 | 
				
			||||||
        {{ event.description }}
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </q-card-section>
 | 
					 | 
				
			||||||
    <q-separator />
 | 
					 | 
				
			||||||
    <q-card-section class="q-pa-xs">
 | 
					 | 
				
			||||||
      <!-- Jobs -->
 | 
					 | 
				
			||||||
      <JobSlot
 | 
					 | 
				
			||||||
        v-for="(job, index) in event.jobs"
 | 
					 | 
				
			||||||
        :key="index"
 | 
					 | 
				
			||||||
        v-model="event.jobs[index]"
 | 
					 | 
				
			||||||
        class="col q-my-xs"
 | 
					 | 
				
			||||||
        :event-id="event.id"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </q-card-section>
 | 
					 | 
				
			||||||
    <q-card-actions v-if="canEdit || canDelete" vertical align="center">
 | 
					 | 
				
			||||||
      <q-btn
 | 
					 | 
				
			||||||
        v-if="canEdit"
 | 
					 | 
				
			||||||
        color="secondary"
 | 
					 | 
				
			||||||
        flat
 | 
					 | 
				
			||||||
        label="Bearbeiten"
 | 
					 | 
				
			||||||
        style="min-width: 95%"
 | 
					 | 
				
			||||||
        @click="edit"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <q-btn
 | 
					 | 
				
			||||||
        v-if="canDelete"
 | 
					 | 
				
			||||||
        color="negative"
 | 
					 | 
				
			||||||
        flat
 | 
					 | 
				
			||||||
        label="Löschen"
 | 
					 | 
				
			||||||
        style="min-width: 95%"
 | 
					 | 
				
			||||||
        @click="remove"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </q-card-actions>
 | 
					 | 
				
			||||||
  </q-card>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { defineComponent, computed, PropType } from 'vue';
 | 
					 | 
				
			||||||
import { hasPermission } from 'src/utils/permission';
 | 
					 | 
				
			||||||
import { PERMISSIONS } from 'src/plugins/schedule/permissions';
 | 
					 | 
				
			||||||
import JobSlot from './JobSlot.vue';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'Eventslot',
 | 
					 | 
				
			||||||
  components: { JobSlot },
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    modelValue: {
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
      type: Object as PropType<FG.Event>,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  emits: {
 | 
					 | 
				
			||||||
    'update:modelValue': (val: FG.Event) => !!val,
 | 
					 | 
				
			||||||
    removeEvent: (val: number) => typeof val === 'number',
 | 
					 | 
				
			||||||
    editEvent: (val: number) => !!val,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  setup(props, { emit }) {
 | 
					 | 
				
			||||||
    const canDelete = computed(() => hasPermission(PERMISSIONS.DELETE));
 | 
					 | 
				
			||||||
    const canEdit = computed(
 | 
					 | 
				
			||||||
      () =>
 | 
					 | 
				
			||||||
        hasPermission(PERMISSIONS.EDIT) &&
 | 
					 | 
				
			||||||
        (props.modelValue?.end || props.modelValue.start) > new Date()
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    const event = computed({
 | 
					 | 
				
			||||||
      get: () => props.modelValue,
 | 
					 | 
				
			||||||
      set: (v) => emit('update:modelValue', v),
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function remove() {
 | 
					 | 
				
			||||||
      emit('removeEvent', props.modelValue.id);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    function edit() {
 | 
					 | 
				
			||||||
      emit('editEvent', props.modelValue.id);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      canDelete,
 | 
					 | 
				
			||||||
      canEdit,
 | 
					 | 
				
			||||||
      edit,
 | 
					 | 
				
			||||||
      event,
 | 
					 | 
				
			||||||
      remove,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style scoped></style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,142 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-card bordered>
 | 
					 | 
				
			||||||
    <div class="text-weight-medium q-px-xs">
 | 
					 | 
				
			||||||
      {{ asHour(modelValue.start) }}
 | 
					 | 
				
			||||||
      <template v-if="modelValue.end">- {{ asHour(modelValue.end) }}</template>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    <div class="q-px-xs">
 | 
					 | 
				
			||||||
      {{ modelValue.type.name }}
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    <div class="col-auto q-px-xs" style="font-size: 10px">
 | 
					 | 
				
			||||||
      {{ modelValue.comment }}
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    <div>
 | 
					 | 
				
			||||||
      <q-select
 | 
					 | 
				
			||||||
        :model-value="modelValue.services"
 | 
					 | 
				
			||||||
        filled
 | 
					 | 
				
			||||||
        :option-label="(opt) => userDisplay(opt)"
 | 
					 | 
				
			||||||
        multiple
 | 
					 | 
				
			||||||
        disable
 | 
					 | 
				
			||||||
        use-chips
 | 
					 | 
				
			||||||
        stack-label
 | 
					 | 
				
			||||||
        label="Dienste"
 | 
					 | 
				
			||||||
        class="col-auto q-px-xs"
 | 
					 | 
				
			||||||
        style="font-size: 6px"
 | 
					 | 
				
			||||||
        counter
 | 
					 | 
				
			||||||
        :max-values="modelValue.required_services"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
      </q-select>
 | 
					 | 
				
			||||||
      <div class="row col-12 justify-end">
 | 
					 | 
				
			||||||
        <q-btn v-if="canEnroll" flat color="primary" label="Eintragen" @click="enrollForJob" />
 | 
					 | 
				
			||||||
        <q-btn v-if="isEnrolled" flat color="negative" label="Austragen" @click="signOutFromJob" />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </q-card>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { defineComponent, onBeforeMount, computed, PropType } from 'vue';
 | 
					 | 
				
			||||||
import { Notify } from 'quasar';
 | 
					 | 
				
			||||||
import { asHour } from 'src/utils/datetime';
 | 
					 | 
				
			||||||
import { useUserStore } from 'src/plugins/user/store';
 | 
					 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
import { useScheduleStore } from 'src/plugins/schedule/store';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'JobSlot',
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    modelValue: {
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
      type: Object as PropType<FG.Job>,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    eventId: {
 | 
					 | 
				
			||||||
      required: true,
 | 
					 | 
				
			||||||
      type: Number,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  emits: { 'update:modelValue': (v: FG.Job) => !!v },
 | 
					 | 
				
			||||||
  setup(props, { emit }) {
 | 
					 | 
				
			||||||
    const store = useScheduleStore();
 | 
					 | 
				
			||||||
    const mainStore = useMainStore();
 | 
					 | 
				
			||||||
    const userStore = useUserStore();
 | 
					 | 
				
			||||||
    const availableUsers = null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onBeforeMount(async () => userStore.getUsers());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function userDisplay(service: FG.Service) {
 | 
					 | 
				
			||||||
      return userStore.findUser(service.userid)?.display_name || service.userid;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const isEnrolled = computed(
 | 
					 | 
				
			||||||
      () =>
 | 
					 | 
				
			||||||
        props.modelValue.services.findIndex(
 | 
					 | 
				
			||||||
          (service) => service.userid == mainStore.currentUser.userid
 | 
					 | 
				
			||||||
        ) !== -1
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const canEnroll = computed(() => {
 | 
					 | 
				
			||||||
      const is = isEnrolled.value;
 | 
					 | 
				
			||||||
      let sum = 0;
 | 
					 | 
				
			||||||
      props.modelValue.services.forEach((s) => (sum += s.value));
 | 
					 | 
				
			||||||
      return sum < props.modelValue.required_services && !is;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function enrollForJob() {
 | 
					 | 
				
			||||||
      const newService: FG.Service = {
 | 
					 | 
				
			||||||
        userid: mainStore.currentUser.userid,
 | 
					 | 
				
			||||||
        is_backup: false,
 | 
					 | 
				
			||||||
        value: 1,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const job = await store.updateJob(props.eventId, props.modelValue.id, { user: newService });
 | 
					 | 
				
			||||||
        emit('update:modelValue', job);
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        console.warn(error);
 | 
					 | 
				
			||||||
        Notify.create({
 | 
					 | 
				
			||||||
          group: false,
 | 
					 | 
				
			||||||
          type: 'negative',
 | 
					 | 
				
			||||||
          message: 'Fehler beim Eintragen als Dienst',
 | 
					 | 
				
			||||||
          timeout: 10000,
 | 
					 | 
				
			||||||
          progress: true,
 | 
					 | 
				
			||||||
          actions: [{ icon: 'mdi-close', color: 'white' }],
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    async function signOutFromJob() {
 | 
					 | 
				
			||||||
      const newService: FG.Service = {
 | 
					 | 
				
			||||||
        userid: mainStore.currentUser.userid,
 | 
					 | 
				
			||||||
        is_backup: false,
 | 
					 | 
				
			||||||
        value: -1,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const job = await store.updateJob(props.eventId, props.modelValue.id, {
 | 
					 | 
				
			||||||
          user: newService,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        emit('update:modelValue', job);
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        console.warn(error);
 | 
					 | 
				
			||||||
        Notify.create({
 | 
					 | 
				
			||||||
          group: false,
 | 
					 | 
				
			||||||
          type: 'negative',
 | 
					 | 
				
			||||||
          message: 'Fehler beim Austragen als Dienst',
 | 
					 | 
				
			||||||
          timeout: 10000,
 | 
					 | 
				
			||||||
          progress: true,
 | 
					 | 
				
			||||||
          actions: [{ icon: 'mdi-close', color: 'white' }],
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      availableUsers,
 | 
					 | 
				
			||||||
      enrollForJob,
 | 
					 | 
				
			||||||
      isEnrolled,
 | 
					 | 
				
			||||||
      signOutFromJob,
 | 
					 | 
				
			||||||
      canEnroll,
 | 
					 | 
				
			||||||
      userDisplay,
 | 
					 | 
				
			||||||
      asHour,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style scoped></style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,9 +0,0 @@
 | 
				
			||||||
declare namespace FG {
 | 
					 | 
				
			||||||
  export interface RecurrenceRule {
 | 
					 | 
				
			||||||
    frequency: string;
 | 
					 | 
				
			||||||
    interval: number;
 | 
					 | 
				
			||||||
    count?: number;
 | 
					 | 
				
			||||||
    until?: Date;
 | 
					 | 
				
			||||||
    weekdays?: Array<number>;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,29 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
 | 
					 | 
				
			||||||
    <EditEvent v-model="event" />
 | 
					 | 
				
			||||||
  </q-page>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { onBeforeMount, defineComponent, ref } from 'vue';
 | 
					 | 
				
			||||||
import EditEvent from '../components/management/EditEvent.vue';
 | 
					 | 
				
			||||||
import { useScheduleStore } from '../store';
 | 
					 | 
				
			||||||
import { useRoute } from 'vue-router';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  components: { EditEvent },
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    const route = useRoute();
 | 
					 | 
				
			||||||
    const store = useScheduleStore();
 | 
					 | 
				
			||||||
    const event = ref<FG.Event | undefined>(undefined);
 | 
					 | 
				
			||||||
    onBeforeMount(async () => {
 | 
					 | 
				
			||||||
      if ('id' in route.params && typeof route.params.id === 'string')
 | 
					 | 
				
			||||||
        event.value = await store.getEvent(parseInt(route.params.id));
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      event,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,95 +0,0 @@
 | 
				
			||||||
<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" />
 | 
					 | 
				
			||||||
    </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 padding class="fit row justify-center content-start items-start q-gutter-sm">
 | 
					 | 
				
			||||||
      <q-tab-panels
 | 
					 | 
				
			||||||
        v-model="tab"
 | 
					 | 
				
			||||||
        style="background-color: transparent"
 | 
					 | 
				
			||||||
        class="q-ma-none q-pa-none fit row justify-center content-start items-start"
 | 
					 | 
				
			||||||
        animated
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <q-tab-panel name="create">
 | 
					 | 
				
			||||||
          <EditEvent />
 | 
					 | 
				
			||||||
        </q-tab-panel>
 | 
					 | 
				
			||||||
        <q-tab-panel name="eventtypes">
 | 
					 | 
				
			||||||
          <EventTypes />
 | 
					 | 
				
			||||||
        </q-tab-panel>
 | 
					 | 
				
			||||||
        <q-tab-panel name="jobtypes">
 | 
					 | 
				
			||||||
          <JobTypes v-if="canEditJobTypes" />
 | 
					 | 
				
			||||||
        </q-tab-panel>
 | 
					 | 
				
			||||||
      </q-tab-panels>
 | 
					 | 
				
			||||||
    </q-page>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { computed, defineComponent, ref } from 'vue';
 | 
					 | 
				
			||||||
import EventTypes from '../components/management/EventTypes.vue';
 | 
					 | 
				
			||||||
import JobTypes from '../components/management/JobTypes.vue';
 | 
					 | 
				
			||||||
import EditEvent from '../components/management/EditEvent.vue';
 | 
					 | 
				
			||||||
import { hasPermission } from 'src/utils/permission';
 | 
					 | 
				
			||||||
import { PERMISSIONS } from '../permissions';
 | 
					 | 
				
			||||||
import { Screen } from 'quasar';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'EventManagement',
 | 
					 | 
				
			||||||
  components: { EditEvent, EventTypes, JobTypes },
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    const canEditJobTypes = computed(() => hasPermission(PERMISSIONS.JOB_TYPE));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    interface Tab {
 | 
					 | 
				
			||||||
      name: string;
 | 
					 | 
				
			||||||
      label: string;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const tabs: Tab[] = [
 | 
					 | 
				
			||||||
      { name: 'create', label: 'Veranstaltungen' },
 | 
					 | 
				
			||||||
      { name: 'eventtypes', label: 'Veranstaltungsarten' },
 | 
					 | 
				
			||||||
      { name: 'jobtypes', label: 'Dienstarten' },
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const drawer = ref<boolean>(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const showDrawer = computed({
 | 
					 | 
				
			||||||
      get: () => {
 | 
					 | 
				
			||||||
        return !Screen.gt.sm && drawer.value;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      set: (val: boolean) => {
 | 
					 | 
				
			||||||
        drawer.value = val;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const tab = ref<string>('create');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      canEditJobTypes,
 | 
					 | 
				
			||||||
      showDrawer,
 | 
					 | 
				
			||||||
      tab,
 | 
					 | 
				
			||||||
      tabs,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,87 +0,0 @@
 | 
				
			||||||
<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" />
 | 
					 | 
				
			||||||
    </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 padding class="fit row justify-center content-start items-start q-gutter-sm">
 | 
					 | 
				
			||||||
      <q-tab-panels
 | 
					 | 
				
			||||||
        v-model="tab"
 | 
					 | 
				
			||||||
        style="background-color: transparent"
 | 
					 | 
				
			||||||
        class="q-ma-none q-pa-none fit row justify-center content-start items-start"
 | 
					 | 
				
			||||||
        animated
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <q-tab-panel name="agendaView">
 | 
					 | 
				
			||||||
          <AgendaView />
 | 
					 | 
				
			||||||
        </q-tab-panel>
 | 
					 | 
				
			||||||
        <q-tab-panel name="eventtypes">
 | 
					 | 
				
			||||||
          <EventTypes />
 | 
					 | 
				
			||||||
        </q-tab-panel>
 | 
					 | 
				
			||||||
      </q-tab-panels>
 | 
					 | 
				
			||||||
    </q-page>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { computed, defineComponent, ref } from 'vue';
 | 
					 | 
				
			||||||
import EventTypes from '../components/management/EventTypes.vue';
 | 
					 | 
				
			||||||
//import CreateEvent from '../components/management/CreateEvent.vue';
 | 
					 | 
				
			||||||
import AgendaView from '../components/overview/AgendaView.vue';
 | 
					 | 
				
			||||||
import { Screen } from 'quasar';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'EventOverview',
 | 
					 | 
				
			||||||
  components: { AgendaView, EventTypes },
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    interface Tab {
 | 
					 | 
				
			||||||
      name: string;
 | 
					 | 
				
			||||||
      label: string;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const tabs: Tab[] = [
 | 
					 | 
				
			||||||
      { name: 'agendaView', label: 'Kalendar' },
 | 
					 | 
				
			||||||
      // { name: 'eventtypes', label: 'Veranstaltungsarten' },
 | 
					 | 
				
			||||||
      // { name: 'jobtypes', label: 'Dienstarten' }
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const drawer = ref<boolean>(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const showDrawer = computed({
 | 
					 | 
				
			||||||
      get: () => {
 | 
					 | 
				
			||||||
        return !Screen.gt.sm && drawer.value;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      set: (val: boolean) => {
 | 
					 | 
				
			||||||
        drawer.value = val;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const tab = ref<string>('agendaView');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      showDrawer,
 | 
					 | 
				
			||||||
      tab,
 | 
					 | 
				
			||||||
      tabs,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,8 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-page padding>
 | 
					 | 
				
			||||||
    <q-card>
 | 
					 | 
				
			||||||
      <q-card-section class="row"> </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-section> </q-card-section>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-page>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,16 +0,0 @@
 | 
				
			||||||
export const PERMISSIONS = {
 | 
					 | 
				
			||||||
  // Can create events
 | 
					 | 
				
			||||||
  CREATE: 'events_create',
 | 
					 | 
				
			||||||
  // Can edit events
 | 
					 | 
				
			||||||
  EDIT: 'events_edit',
 | 
					 | 
				
			||||||
  // Can delete events
 | 
					 | 
				
			||||||
  DELETE: 'events_delete',
 | 
					 | 
				
			||||||
  // Can create and edit EventTypes
 | 
					 | 
				
			||||||
  EVENT_TYPE: 'events_event_type',
 | 
					 | 
				
			||||||
  // Can create and edit JobTypes
 | 
					 | 
				
			||||||
  JOB_TYPE: 'events_job_type',
 | 
					 | 
				
			||||||
  // Can self assign to jobs
 | 
					 | 
				
			||||||
  ASSIGN: 'events_assign',
 | 
					 | 
				
			||||||
  // Can assign other users to jobs
 | 
					 | 
				
			||||||
  ASSIGN_OTHER: 'events_assign_other',
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,22 +0,0 @@
 | 
				
			||||||
import { defineAsyncComponent } from 'vue';
 | 
					 | 
				
			||||||
import { innerRoutes, privateRoutes } from './routes';
 | 
					 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const plugin: FG_Plugin.Plugin = {
 | 
					 | 
				
			||||||
  name: 'Schedule',
 | 
					 | 
				
			||||||
  innerRoutes,
 | 
					 | 
				
			||||||
  internalRoutes: privateRoutes,
 | 
					 | 
				
			||||||
  requiredModules: ['User'],
 | 
					 | 
				
			||||||
  requiredBackendModules: ['events'],
 | 
					 | 
				
			||||||
  version: '0.0.1',
 | 
					 | 
				
			||||||
  widgets: [
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      priority: 0,
 | 
					 | 
				
			||||||
      name: 'stats',
 | 
					 | 
				
			||||||
      permissions: [],
 | 
					 | 
				
			||||||
      widget: defineAsyncComponent(() => import('./components/Widget.vue')),
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  ],
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default plugin;
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,56 +0,0 @@
 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					 | 
				
			||||||
import { PERMISSIONS } from '../permissions';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const innerRoutes: FG_Plugin.MenuRoute[] = [
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    title: 'Dienste',
 | 
					 | 
				
			||||||
    icon: 'mdi-briefcase',
 | 
					 | 
				
			||||||
    permissions: ['user'],
 | 
					 | 
				
			||||||
    route: {
 | 
					 | 
				
			||||||
      path: 'schedule',
 | 
					 | 
				
			||||||
      name: 'schedule',
 | 
					 | 
				
			||||||
      redirect: { name: 'schedule-overview' },
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    children: [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        title: 'Dienstübersicht',
 | 
					 | 
				
			||||||
        icon: 'mdi-account-group',
 | 
					 | 
				
			||||||
        shortcut: true,
 | 
					 | 
				
			||||||
        route: {
 | 
					 | 
				
			||||||
          path: 'schedule-overview',
 | 
					 | 
				
			||||||
          name: 'schedule-overview',
 | 
					 | 
				
			||||||
          component: () => import('../pages/Overview.vue'),
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        title: 'Dienstverwaltung',
 | 
					 | 
				
			||||||
        icon: 'mdi-account-details',
 | 
					 | 
				
			||||||
        shortcut: false,
 | 
					 | 
				
			||||||
        permissions: [PERMISSIONS.CREATE],
 | 
					 | 
				
			||||||
        route: {
 | 
					 | 
				
			||||||
          path: 'schedule-management',
 | 
					 | 
				
			||||||
          name: 'schedule-management',
 | 
					 | 
				
			||||||
          component: () => import('../pages/Management.vue'),
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        title: 'Dienstanfragen',
 | 
					 | 
				
			||||||
        icon: 'mdi-account-switch',
 | 
					 | 
				
			||||||
        shortcut: false,
 | 
					 | 
				
			||||||
        route: {
 | 
					 | 
				
			||||||
          path: 'schedule-requests',
 | 
					 | 
				
			||||||
          name: 'schedule-requests',
 | 
					 | 
				
			||||||
          component: () => import('../pages/Requests.vue'),
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const privateRoutes: FG_Plugin.NamedRouteRecordRaw[] = [
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    name: 'events-edit',
 | 
					 | 
				
			||||||
    path: 'schedule/:id/edit',
 | 
					 | 
				
			||||||
    component: () => import('../pages/Event.vue'),
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,165 +0,0 @@
 | 
				
			||||||
import { api } from 'src/boot/axios';
 | 
					 | 
				
			||||||
import { AxiosError } from 'axios';
 | 
					 | 
				
			||||||
import { defineStore } from 'pinia';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface UserService {
 | 
					 | 
				
			||||||
  user: FG.Service;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function fixJob(job: FG.Job) {
 | 
					 | 
				
			||||||
  job.start = new Date(job.start);
 | 
					 | 
				
			||||||
  if (job.end) job.end = new Date(job.end);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function fixEvent(event: FG.Event) {
 | 
					 | 
				
			||||||
  event.start = new Date(event.start);
 | 
					 | 
				
			||||||
  if (event.end) event.end = new Date(event.end);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  event.jobs.forEach((job) => fixJob(job));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const useScheduleStore = defineStore({
 | 
					 | 
				
			||||||
  id: 'schedule',
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  state: () => ({
 | 
					 | 
				
			||||||
    jobTypes: [] as FG.JobType[],
 | 
					 | 
				
			||||||
    eventTypes: [] as FG.EventType[],
 | 
					 | 
				
			||||||
    templates: [] as FG.Event[],
 | 
					 | 
				
			||||||
  }),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  getters: {},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  actions: {
 | 
					 | 
				
			||||||
    async getJobTypes(force = false) {
 | 
					 | 
				
			||||||
      if (force || this.jobTypes.length == 0)
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
          const { data } = await api.get<FG.JobType[]>('/events/job-types');
 | 
					 | 
				
			||||||
          this.jobTypes = data;
 | 
					 | 
				
			||||||
        } catch (error) {
 | 
					 | 
				
			||||||
          throw error;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      return this.jobTypes;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async addJobType(name: string) {
 | 
					 | 
				
			||||||
      await api.post<FG.JobType>('/events/job-types', { name: name });
 | 
					 | 
				
			||||||
      //TODO: HAndle new JT
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async removeJobType(id: number) {
 | 
					 | 
				
			||||||
      await api.delete(`/events/job-types/${id}`);
 | 
					 | 
				
			||||||
      //Todo Handle delete JT
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async renameJobType(id: number, newName: string) {
 | 
					 | 
				
			||||||
      await api.put(`/events/job-types/${id}`, { name: newName });
 | 
					 | 
				
			||||||
      // TODO handle rename
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async getEventTypes(force = false) {
 | 
					 | 
				
			||||||
      if (force || this.eventTypes.length == 0)
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
          const { data } = await api.get<FG.EventType[]>('/events/event-types');
 | 
					 | 
				
			||||||
          this.eventTypes = data;
 | 
					 | 
				
			||||||
        } catch (error) {
 | 
					 | 
				
			||||||
          throw error;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      return this.eventTypes;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /** Add new EventType
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param name Name of new EventType
 | 
					 | 
				
			||||||
     * @returns EventType object or null if name already exists
 | 
					 | 
				
			||||||
     * @throws Exception if requests fails because of an other reason
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    async addEventType(name: string) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const { data } = await api.post<FG.EventType>('/events/event-types', { name: name });
 | 
					 | 
				
			||||||
        return data;
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        if ('response' in error) {
 | 
					 | 
				
			||||||
          const ae = <AxiosError>error;
 | 
					 | 
				
			||||||
          if (ae.response && ae.response.status == 409 /* CONFLICT */) return null;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        throw error;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    async removeEvent(id: number) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        await api.delete(`/events/${id}`);
 | 
					 | 
				
			||||||
        const idx = this.templates.findIndex((v) => v.id === id);
 | 
					 | 
				
			||||||
        if (idx !== -1) this.templates.splice(idx, 1);
 | 
					 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        const error = <AxiosError>e;
 | 
					 | 
				
			||||||
        if (error.response && error.response.status === 404) return false;
 | 
					 | 
				
			||||||
        throw e;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return true;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async removeEventType(id: number) {
 | 
					 | 
				
			||||||
      await api.delete(`/events/event-types/${id}`);
 | 
					 | 
				
			||||||
      // TODO handle delete
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async renameEventType(id: number, newName: string) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        await api.put(`/events/event-types/${id}`, { name: newName });
 | 
					 | 
				
			||||||
        // TODO handle rename
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        if ('response' in error) {
 | 
					 | 
				
			||||||
          const ae = <AxiosError>error;
 | 
					 | 
				
			||||||
          if (ae.response && ae.response.status == 409 /* CONFLICT */) return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        throw error;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async getTemplates(force = false) {
 | 
					 | 
				
			||||||
      if (force || this.templates.length == 0) {
 | 
					 | 
				
			||||||
        const { data } = await api.get<FG.Event[]>('/events/templates');
 | 
					 | 
				
			||||||
        data.forEach((element) => fixEvent(element));
 | 
					 | 
				
			||||||
        this.templates = data;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return this.templates;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async getEvents(filter: { from?: Date; to?: Date } | undefined = undefined) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const { data } = await api.get<FG.Event[]>('/events', { params: filter });
 | 
					 | 
				
			||||||
        data.forEach((element) => fixEvent(element));
 | 
					 | 
				
			||||||
        return data;
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        throw error;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async getEvent(id: number) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const { data } = await api.get<FG.Event>(`/events/${id}`);
 | 
					 | 
				
			||||||
        fixEvent(data);
 | 
					 | 
				
			||||||
        return data;
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        throw error;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async updateJob(eventId: number, jobId: number, service: FG.Service | UserService) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        const { data } = await api.put<FG.Job>(`/events/${eventId}/jobs/${jobId}`, service);
 | 
					 | 
				
			||||||
        fixJob(data);
 | 
					 | 
				
			||||||
        return data;
 | 
					 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        throw error;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async addEvent(event: FG.Event) {
 | 
					 | 
				
			||||||
      const { data } = await api.post<FG.Event>('/events', event);
 | 
					 | 
				
			||||||
      if (data.is_template) this.templates.push(data);
 | 
					 | 
				
			||||||
      return data;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,39 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-card class="12">
 | 
					 | 
				
			||||||
    <q-card-section class="fit row justify-start content-center items-center">
 | 
					 | 
				
			||||||
      <div class="col-xs-12 col-sm-6 text-center text-h6">Neues Mitglied</div>
 | 
					 | 
				
			||||||
    </q-card-section>
 | 
					 | 
				
			||||||
    <q-card-section>
 | 
					 | 
				
			||||||
      <MainUserSettings :user="user" :new-user="true" @update:user="setUser" />
 | 
					 | 
				
			||||||
    </q-card-section>
 | 
					 | 
				
			||||||
  </q-card>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { defineComponent, ref } from 'vue';
 | 
					 | 
				
			||||||
import MainUserSettings from 'src/plugins/user/components/settings/MainUserSettings.vue';
 | 
					 | 
				
			||||||
import { useUserStore } from '../store';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'NewUser',
 | 
					 | 
				
			||||||
  components: { MainUserSettings },
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    const userStore = useUserStore();
 | 
					 | 
				
			||||||
    const user = ref<FG.User>({
 | 
					 | 
				
			||||||
      userid: '',
 | 
					 | 
				
			||||||
      display_name: '',
 | 
					 | 
				
			||||||
      firstname: '',
 | 
					 | 
				
			||||||
      lastname: '',
 | 
					 | 
				
			||||||
      mail: '',
 | 
					 | 
				
			||||||
      roles: [],
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function setUser(value: FG.User) {
 | 
					 | 
				
			||||||
      await userStore.createUser(value);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return { user, setUser };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style scoped></style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,40 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-card class="col-12">
 | 
					 | 
				
			||||||
    <q-card-section class="fit row justify-start content-center items-center">
 | 
					 | 
				
			||||||
      <div class="col-xs-12 col-sm-6 text-center text-h6">Benutzereinstellungen</div>
 | 
					 | 
				
			||||||
      <div class="col-xs-12 col-sm-6 q-pa-sm">
 | 
					 | 
				
			||||||
        <UserSelector v-model="user" />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </q-card-section>
 | 
					 | 
				
			||||||
    <MainUserSettings :user="user" @update:user="updateUser" />
 | 
					 | 
				
			||||||
  </q-card>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { defineComponent, ref } from 'vue';
 | 
					 | 
				
			||||||
import UserSelector from '../components/UserSelector.vue';
 | 
					 | 
				
			||||||
import MainUserSettings from '../components/settings/MainUserSettings.vue';
 | 
					 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
import { useUserStore } from '../store';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'UpdateUser',
 | 
					 | 
				
			||||||
  components: { UserSelector, MainUserSettings },
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    const mainStore = useMainStore();
 | 
					 | 
				
			||||||
    const userStore = useUserStore();
 | 
					 | 
				
			||||||
    const user = ref(mainStore.currentUser);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function updateUser(value: FG.User) {
 | 
					 | 
				
			||||||
      await userStore.updateUser(value);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      user,
 | 
					 | 
				
			||||||
      updateUser,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style scoped></style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,43 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-select
 | 
					 | 
				
			||||||
    v-model="selected"
 | 
					 | 
				
			||||||
    filled
 | 
					 | 
				
			||||||
    :label="label"
 | 
					 | 
				
			||||||
    :options="users"
 | 
					 | 
				
			||||||
    option-label="display_name"
 | 
					 | 
				
			||||||
    option-value="userid"
 | 
					 | 
				
			||||||
    map-options
 | 
					 | 
				
			||||||
  />
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { computed, defineComponent, PropType, onBeforeMount } from 'vue';
 | 
					 | 
				
			||||||
import { useUserStore } from '../store';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'UserSelector',
 | 
					 | 
				
			||||||
  props: {
 | 
					 | 
				
			||||||
    label: { type: String, default: 'Benutzer' },
 | 
					 | 
				
			||||||
    modelValue: { default: undefined, type: Object as PropType<FG.User | undefined> },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  emits: { 'update:modelValue': (user: FG.User) => !!user },
 | 
					 | 
				
			||||||
  setup(props, { emit }) {
 | 
					 | 
				
			||||||
    const userStore = useUserStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onBeforeMount(() => {
 | 
					 | 
				
			||||||
      void userStore.getUsers(false);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const users = computed(() => userStore.users);
 | 
					 | 
				
			||||||
    const selected = computed({
 | 
					 | 
				
			||||||
      get: () => props.modelValue,
 | 
					 | 
				
			||||||
      set: (value: FG.User | undefined) => (value ? emit('update:modelValue', value) : undefined),
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      selected,
 | 
					 | 
				
			||||||
      users,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,72 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <q-card style="text-align: center">
 | 
					 | 
				
			||||||
    <q-card-section class="row justify-center content-stretch">
 | 
					 | 
				
			||||||
      <div v-if="avatar" class="col-4">
 | 
					 | 
				
			||||||
        <div style="width: 100%; padding-bottom: 100%; position: relative">
 | 
					 | 
				
			||||||
          <q-avatar style="position: absolute; top: 0; left: 0; width: 100%; height: 100%">
 | 
					 | 
				
			||||||
            <img :src="avatarLink" :onerror="error" />
 | 
					 | 
				
			||||||
          </q-avatar>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <div class="col-8">
 | 
					 | 
				
			||||||
        <span class="text-h6">Hallo {{ name }}</span
 | 
					 | 
				
			||||||
        ><br />
 | 
					 | 
				
			||||||
        <span v-if="hasBirthday">Herzlichen Glückwunsch zum Geburtstag!<br /></span>
 | 
					 | 
				
			||||||
        <span v-if="birthday.length > 0"
 | 
					 | 
				
			||||||
          >Heute <span v-if="birthday.length === 1">hat </span><span v-else>haben </span
 | 
					 | 
				
			||||||
          ><span v-for="(user, index) in birthday" :key="index"
 | 
					 | 
				
			||||||
            >{{ user.display_name }}<span v-if="index < birthday.length - 1">, </span></span
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
          Geburtstag.</span
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
        <span v-else>Heute stehen keine Geburtstage an</span>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </q-card-section>
 | 
					 | 
				
			||||||
  </q-card>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import { useMainStore } from 'src/stores';
 | 
					 | 
				
			||||||
import { computed, defineComponent, onMounted, ref } from 'vue';
 | 
					 | 
				
			||||||
import { useUserStore } from '../store';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default defineComponent({
 | 
					 | 
				
			||||||
  name: 'Greeting',
 | 
					 | 
				
			||||||
  setup() {
 | 
					 | 
				
			||||||
    const mainStore = useMainStore();
 | 
					 | 
				
			||||||
    const userStore = useUserStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Ensure users are loaded,so we can query birthdays
 | 
					 | 
				
			||||||
    onMounted(() => userStore.getUsers(false));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const avatar = ref(true);
 | 
					 | 
				
			||||||
    const name = ref(mainStore.currentUser.firstname);
 | 
					 | 
				
			||||||
    const avatarLink = ref(mainStore.currentUser.avatar_url);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function error() {
 | 
					 | 
				
			||||||
      avatar.value = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function userHasBirthday(user: FG.User) {
 | 
					 | 
				
			||||||
      const today = new Date();
 | 
					 | 
				
			||||||
      return (
 | 
					 | 
				
			||||||
        user.birthday &&
 | 
					 | 
				
			||||||
        user.birthday.getMonth() === today.getMonth() &&
 | 
					 | 
				
			||||||
        user.birthday.getDate() === today.getDate()
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const hasBirthday = computed(() => {
 | 
					 | 
				
			||||||
      return userHasBirthday(mainStore.currentUser);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const birthday = computed(() =>
 | 
					 | 
				
			||||||
      userStore.users
 | 
					 | 
				
			||||||
        .filter(userHasBirthday)
 | 
					 | 
				
			||||||
        .filter((user) => user.userid !== mainStore.currentUser.userid)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return { avatar, avatarLink, error, name, hasBirthday, birthday };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue