Compare commits
	
		
			No commits in common. "main" and "v1" have entirely different histories.
		
	
	
		| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
root = true
 | 
			
		||||
 | 
			
		||||
[*]
 | 
			
		||||
charset = utf-8
 | 
			
		||||
indent_style = space
 | 
			
		||||
indent_size = 2
 | 
			
		||||
end_of_line = lf
 | 
			
		||||
insert_final_newline = true
 | 
			
		||||
trim_trailing_whitespace = true
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +0,0 @@
 | 
			
		|||
/dist
 | 
			
		||||
/src-bex/www
 | 
			
		||||
/src-capacitor
 | 
			
		||||
/src-cordova
 | 
			
		||||
/.quasar
 | 
			
		||||
/node_modules
 | 
			
		||||
/src-ssr
 | 
			
		||||
.*
 | 
			
		||||
							
								
								
									
										91
									
								
								.eslintrc.js
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -1,91 +0,0 @@
 | 
			
		|||
const { resolve } = require('path');
 | 
			
		||||
module.exports = {
 | 
			
		||||
  // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
 | 
			
		||||
  // This option interrupts the configuration hierarchy at this file
 | 
			
		||||
  // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
 | 
			
		||||
  root: true,
 | 
			
		||||
 | 
			
		||||
  // https://eslint.vuejs.org/user-guide/#how-to-use-custom-parser
 | 
			
		||||
  // Must use parserOptions instead of "parser" to allow vue-eslint-parser to keep working
 | 
			
		||||
  // `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted
 | 
			
		||||
  parserOptions: {
 | 
			
		||||
    // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser#configuration
 | 
			
		||||
    // https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#eslint
 | 
			
		||||
    // Needed to make the parser take into account 'vue' files
 | 
			
		||||
    extraFileExtensions: ['.vue'],
 | 
			
		||||
    parser: '@typescript-eslint/parser',
 | 
			
		||||
    project: resolve(__dirname, './tsconfig.json'),
 | 
			
		||||
    tsconfigRootDir: __dirname,
 | 
			
		||||
    ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features
 | 
			
		||||
    sourceType: 'module', // Allows for the use of imports
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  env: {
 | 
			
		||||
    browser: true,
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // Rules order is important, please avoid shuffling them
 | 
			
		||||
  extends: [
 | 
			
		||||
    // Base ESLint recommended rules
 | 
			
		||||
    // 'eslint:recommended',
 | 
			
		||||
 | 
			
		||||
    // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage
 | 
			
		||||
    // ESLint typescript rules
 | 
			
		||||
    'plugin:@typescript-eslint/recommended',
 | 
			
		||||
    // consider disabling this class of rules if linting takes too long
 | 
			
		||||
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
 | 
			
		||||
 | 
			
		||||
    // Uncomment any of the lines below to choose desired strictness,
 | 
			
		||||
    // but leave only one uncommented!
 | 
			
		||||
    // See https://eslint.vuejs.org/rules/#available-rules
 | 
			
		||||
    // 'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
 | 
			
		||||
    // 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
 | 
			
		||||
    'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
 | 
			
		||||
 | 
			
		||||
    // https://github.com/prettier/eslint-config-prettier#installation
 | 
			
		||||
    // usage with Prettier, provided by 'eslint-config-prettier'.
 | 
			
		||||
    'plugin:prettier/recommended',
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  plugins: [
 | 
			
		||||
    // required to apply rules which need type information
 | 
			
		||||
    '@typescript-eslint',
 | 
			
		||||
 | 
			
		||||
    // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file
 | 
			
		||||
    // required to lint *.vue files
 | 
			
		||||
    'vue',
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  globals: {
 | 
			
		||||
    ga: true, // Google Analytics
 | 
			
		||||
    cordova: true,
 | 
			
		||||
    __statics: true,
 | 
			
		||||
    __QUASAR_SSR__: true,
 | 
			
		||||
    __QUASAR_SSR_SERVER__: true,
 | 
			
		||||
    __QUASAR_SSR_CLIENT__: true,
 | 
			
		||||
    __QUASAR_SSR_PWA__: true,
 | 
			
		||||
    process: true,
 | 
			
		||||
    Capacitor: true,
 | 
			
		||||
    chrome: true,
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // add your custom rules here
 | 
			
		||||
  rules: {
 | 
			
		||||
    // VueStuff
 | 
			
		||||
    // Defaults to error on eslint-plugin-vue 8.0.3, but let us be not too strict with names
 | 
			
		||||
    'vue/multi-word-component-names': 'off',
 | 
			
		||||
 | 
			
		||||
    // Rejects on promises should always be of the Error type (and allow empty rejects as well)
 | 
			
		||||
    'prefer-promise-reject-errors': ['error', { allowEmptyReject: true }],
 | 
			
		||||
 | 
			
		||||
    // Allow " if ' is contained inside the string, so we can avoid escaping
 | 
			
		||||
    quotes: ['error', 'single', { avoidEscape: true }],
 | 
			
		||||
 | 
			
		||||
    // TypeScript, let us be not too strict
 | 
			
		||||
    '@typescript-eslint/explicit-function-return-type': 'off',
 | 
			
		||||
    '@typescript-eslint/explicit-module-boundary-types': 'off',
 | 
			
		||||
 | 
			
		||||
    // allow debugger during development only
 | 
			
		||||
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,30 +1,10 @@
 | 
			
		|||
.DS_Store
 | 
			
		||||
.thumbs.db
 | 
			
		||||
node_modules
 | 
			
		||||
 | 
			
		||||
# We use yarn, so ignore npm
 | 
			
		||||
package-lock.json
 | 
			
		||||
yarn.lock
 | 
			
		||||
 | 
			
		||||
# Quasar core related directories
 | 
			
		||||
.quasar
 | 
			
		||||
/dist
 | 
			
		||||
 | 
			
		||||
# Cordova related directories and files
 | 
			
		||||
/src-cordova/node_modules
 | 
			
		||||
/src-cordova/platforms
 | 
			
		||||
/src-cordova/plugins
 | 
			
		||||
/src-cordova/www
 | 
			
		||||
 | 
			
		||||
# Capacitor related directories and files
 | 
			
		||||
/src-capacitor/www
 | 
			
		||||
/src-capacitor/android
 | 
			
		||||
/src-capacitor/ios
 | 
			
		||||
/src-capacitor/node_modules
 | 
			
		||||
 | 
			
		||||
# BEX related directories and files
 | 
			
		||||
/src-bex/www
 | 
			
		||||
/src-bex/js/core
 | 
			
		||||
# local env files
 | 
			
		||||
.env.local
 | 
			
		||||
.env.*.local
 | 
			
		||||
 | 
			
		||||
# Log files
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +13,9 @@ yarn-error.log*
 | 
			
		|||
 | 
			
		||||
# Editor directories and files
 | 
			
		||||
.idea
 | 
			
		||||
.vscode
 | 
			
		||||
*.suo
 | 
			
		||||
*.ntvs*
 | 
			
		||||
*.njsproj
 | 
			
		||||
*.sln
 | 
			
		||||
*.sw?
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
yarn-error.log
 | 
			
		||||
.woodpecker/
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +0,0 @@
 | 
			
		|||
// https://github.com/michael-ciniawsky/postcss-load-config
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  plugins: [
 | 
			
		||||
    // to edit target browsers: use "browserslist" field in package.json
 | 
			
		||||
    require('autoprefixer'),
 | 
			
		||||
  ],
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
module.exports = {
 | 
			
		||||
  singleQuote: true,
 | 
			
		||||
  semi: false
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,12 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
  "recommendations": [
 | 
			
		||||
    "dbaeumer.vscode-eslint",
 | 
			
		||||
    "esbenp.prettier-vscode",
 | 
			
		||||
    "octref.vetur"
 | 
			
		||||
  ],
 | 
			
		||||
  "unwantedRecommendations": [
 | 
			
		||||
    "hookyqr.beautify",
 | 
			
		||||
    "dbaeumer.jshint",
 | 
			
		||||
    "ms-vscode.vscode-typescript-tslint-plugin"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,30 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
  "editor.formatOnSave": true,
 | 
			
		||||
  "editor.codeActionsOnSave": {
 | 
			
		||||
    "source.fixAll": true
 | 
			
		||||
  },
 | 
			
		||||
  "javascript.format.insertSpaceBeforeFunctionParenthesis": true,
 | 
			
		||||
  "javascript.format.placeOpenBraceOnNewLineForControlBlocks": false,
 | 
			
		||||
  "javascript.format.placeOpenBraceOnNewLineForFunctions": false,
 | 
			
		||||
  "typescript.format.insertSpaceBeforeFunctionParenthesis": true,
 | 
			
		||||
  "typescript.format.placeOpenBraceOnNewLineForControlBlocks": false,
 | 
			
		||||
  "typescript.format.placeOpenBraceOnNewLineForFunctions": false,
 | 
			
		||||
  "vetur.format.defaultFormatter.html": "prettier",
 | 
			
		||||
  "vetur.format.defaultFormatter.js": "prettier-eslint",
 | 
			
		||||
  "typescript.tsdk": "node_modules/typescript/lib",
 | 
			
		||||
  "vetur.format.defaultFormatterOptions": {
 | 
			
		||||
 | 
			
		||||
    "js-beautify-html": {
 | 
			
		||||
      "wrap_attributes": "force-expand-multiline"
 | 
			
		||||
    },
 | 
			
		||||
    "prettyhtml": {
 | 
			
		||||
      "printWidth": 100,
 | 
			
		||||
      "singleQuote": false,
 | 
			
		||||
      "wrapAttributes": false,
 | 
			
		||||
      "sortAttributes": false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "vetur.format.defaultFormatter.ts": "prettier-tslint",
 | 
			
		||||
  "typescript.format.enable": false,
 | 
			
		||||
  "prettier.configPath": "./package.json"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,15 +0,0 @@
 | 
			
		|||
pipeline:
 | 
			
		||||
  deploy:
 | 
			
		||||
    when:
 | 
			
		||||
      event: tag
 | 
			
		||||
      tag: "@flaschengeist/api-v*"
 | 
			
		||||
    image: node:lts-alpine
 | 
			
		||||
    commands:
 | 
			
		||||
      - cd api
 | 
			
		||||
      - echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" > .npmrc
 | 
			
		||||
      - yarn publish --non-interactive
 | 
			
		||||
    secrets: [ node_auth_token ]
 | 
			
		||||
 | 
			
		||||
depends_on:
 | 
			
		||||
  - lint
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
pipeline:
 | 
			
		||||
  lint:
 | 
			
		||||
    when:
 | 
			
		||||
      branch: [main, develop]
 | 
			
		||||
    image: node:lts-alpine
 | 
			
		||||
    commands:
 | 
			
		||||
      - yarn install
 | 
			
		||||
      - yarn lint
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										21
									
								
								LICENSE
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -1,21 +0,0 @@
 | 
			
		|||
The MIT License (MIT)
 | 
			
		||||
 | 
			
		||||
Copyright 2021 Tim Gröger | Flaschengeist Developers
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
 | 
			
		||||
this software and associated documentation files (the "Software"), to deal in
 | 
			
		||||
the Software without restriction, including without limitation the rights to
 | 
			
		||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 | 
			
		||||
of the Software, and to permit persons to whom the Software is furnished to do
 | 
			
		||||
so, subject to the following conditions:
 | 
			
		||||
 | 
			
		||||
The above copyright notice and this permission notice shall be included in all
 | 
			
		||||
copies or substantial portions of the Software.
 | 
			
		||||
 | 
			
		||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
			
		||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
			
		||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
			
		||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
			
		||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
			
		||||
SOFTWARE.
 | 
			
		||||
							
								
								
									
										92
									
								
								README.md
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -1,88 +1,24 @@
 | 
			
		|||
# Flaschengeist (frontend)
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Modular student club administration system, licensed under the MIT license.
 | 
			
		||||
 | 
			
		||||
## Installation
 | 
			
		||||
 | 
			
		||||
### Requirements
 | 
			
		||||
# newgruecht-vue
 | 
			
		||||
 | 
			
		||||
## Project setup
 | 
			
		||||
```
 | 
			
		||||
 "engines": {
 | 
			
		||||
    "node": ">= 14.18.1",
 | 
			
		||||
    "npm": ">= 6.14.12",
 | 
			
		||||
    "yarn": ">= 1.22.0"
 | 
			
		||||
 }
 | 
			
		||||
npm install
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
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/latest-v16.x/node-v16.13.0-linux-x64.tar.xz
 | 
			
		||||
tar -xJf node-v16.13.0-linux-x64.tar.xz
 | 
			
		||||
export PATH="$(pwd)/node-v16.13.0-linux-x64/bin":"$PATH"
 | 
			
		||||
npm i -g yarn
 | 
			
		||||
npm i -g @quasar/cli
 | 
			
		||||
popd
 | 
			
		||||
### Compiles and hot-reloads for development
 | 
			
		||||
```
 | 
			
		||||
npm run serve
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Install the dependencies
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
yarn install
 | 
			
		||||
### Compiles and minifies for production
 | 
			
		||||
```
 | 
			
		||||
npm run build
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Be aware npm might not work.
 | 
			
		||||
 | 
			
		||||
### Configure Plugins
 | 
			
		||||
 | 
			
		||||
#### Installing a plugin
 | 
			
		||||
 | 
			
		||||
Simply add it as a dependency and install it, for example installing the `pricelist`-plugin:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
yarn add '@flaschengeist/pricelist'
 | 
			
		||||
yarn install
 | 
			
		||||
### Lints and fixes files
 | 
			
		||||
```
 | 
			
		||||
npm run lint
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### 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
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
yarn quasar build
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Notes on mobile apps (Cordova)
 | 
			
		||||
 | 
			
		||||
For mobile applications older web engines should or must be supported,
 | 
			
		||||
as manufaturer often do not update their phones, so for building cordova apps set the `BROWSERSLIST_ENV` environment variable to
 | 
			
		||||
`BROWSERSLIST_ENV=cordova`.
 | 
			
		||||
This will produce ECDMAscript compatible with iOS 13+ and Android Webview 76 (relased October 2019).
 | 
			
		||||
 | 
			
		||||
## Development
 | 
			
		||||
 | 
			
		||||
Please refer to our [development wiki](https://flaschengeist.dev/Flaschengeist/flaschengeist/wiki/Development).
 | 
			
		||||
### Customize configuration
 | 
			
		||||
See [Configuration Reference](https://cli.vuejs.org/config/).
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,167 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <q-input
 | 
			
		||||
    v-model="dateTime"
 | 
			
		||||
    filled
 | 
			
		||||
    :readonly="readonly"
 | 
			
		||||
    :label="label"
 | 
			
		||||
    :placeholder="placeholder"
 | 
			
		||||
    :rules="customRules"
 | 
			
		||||
    :clearable="clearable"
 | 
			
		||||
    v-bind="attrs"
 | 
			
		||||
    @clear="dateTime = ''"
 | 
			
		||||
  >
 | 
			
		||||
    <template #append>
 | 
			
		||||
      <q-icon v-if="'date' || type == 'datetime'" name="mdi-calendar" class="cursor-pointer">
 | 
			
		||||
        <q-popup-proxy ref="qDateProxy" transition-show="scale" transition-hide="scale">
 | 
			
		||||
          <q-date v-model="date" mask="YYYY-MM-DD">
 | 
			
		||||
            <div class="row items-center justify-end">
 | 
			
		||||
              <q-btn v-close-popup label="Schließen" color="primary" flat />
 | 
			
		||||
            </div>
 | 
			
		||||
          </q-date>
 | 
			
		||||
        </q-popup-proxy>
 | 
			
		||||
      </q-icon>
 | 
			
		||||
      <q-icon
 | 
			
		||||
        v-if="type == 'time' || type == 'datetime'"
 | 
			
		||||
        name="mdi-clock-outline"
 | 
			
		||||
        class="cursor-pointer"
 | 
			
		||||
      >
 | 
			
		||||
        <q-popup-proxy ref="qTimeProxy" transition-show="scale" transition-hide="scale">
 | 
			
		||||
          <q-time v-model="time" mask="HH:mm">
 | 
			
		||||
            <div class="row items-center justify-end">
 | 
			
		||||
              <q-btn v-close-popup label="Schließen" color="primary" flat />
 | 
			
		||||
            </div>
 | 
			
		||||
          </q-time>
 | 
			
		||||
        </q-popup-proxy>
 | 
			
		||||
      </q-icon>
 | 
			
		||||
    </template>
 | 
			
		||||
  </q-input>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineComponent, PropType } from 'vue';
 | 
			
		||||
import { date as q_date } from 'quasar';
 | 
			
		||||
import { stringIsDate, stringIsTime, stringIsDateTime, Validator } from '..';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'IsoDateInput',
 | 
			
		||||
  props: {
 | 
			
		||||
    modelValue: { type: Object as PropType<Date | undefined>, default: undefined },
 | 
			
		||||
    type: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'date',
 | 
			
		||||
      validator: (value: string) => ['date', 'time', 'datetime'].indexOf(value) !== -1,
 | 
			
		||||
    },
 | 
			
		||||
    label: { type: String, default: 'Datum' },
 | 
			
		||||
    readonly: Boolean,
 | 
			
		||||
    rules: {
 | 
			
		||||
      type: Array as PropType<Validator<Date>[]>,
 | 
			
		||||
      default: () => [],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  emits: { 'update:modelValue': (date?: Date) => !!date || !date },
 | 
			
		||||
  setup(props, { emit, attrs }) {
 | 
			
		||||
    const customRules = computed(() => [
 | 
			
		||||
      props.type == 'date' ? stringIsDate : props.type == 'time' ? stringIsTime : stringIsDateTime,
 | 
			
		||||
      (value?: string) => {
 | 
			
		||||
        if (props.rules.length > 0 && !!value) {
 | 
			
		||||
          let date: Date | undefined = undefined;
 | 
			
		||||
          if (props.type == 'date') date = modifyDate(value);
 | 
			
		||||
          else if (props.type == 'time') date = modifyTime(value);
 | 
			
		||||
          else {
 | 
			
		||||
            const split = value.split(' ');
 | 
			
		||||
            date = modifyTime(split[1], modifyDate(split[0]));
 | 
			
		||||
          }
 | 
			
		||||
          for (const rule of props.rules) {
 | 
			
		||||
            const r = rule(date);
 | 
			
		||||
            if (typeof r === 'string') return r;
 | 
			
		||||
          }
 | 
			
		||||
          return true;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const clearable = computed(() =>
 | 
			
		||||
      customRules.value.every((r) => (<Validator>r)(undefined) === true)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const placeholder = computed(() => {
 | 
			
		||||
      switch (props.type) {
 | 
			
		||||
        case 'date':
 | 
			
		||||
          return 'YYYY-MM-DD';
 | 
			
		||||
        case 'time':
 | 
			
		||||
          return 'HH:mm';
 | 
			
		||||
        case 'datetime':
 | 
			
		||||
          return 'YYYY-MM-DD HH:mm';
 | 
			
		||||
      }
 | 
			
		||||
      throw 'Invalid type given';
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const date = computed({
 | 
			
		||||
      get: () => q_date.formatDate(props.modelValue, 'YYYY-MM-DD'),
 | 
			
		||||
      set: (v: string) => {
 | 
			
		||||
        const d = modifyDate(v);
 | 
			
		||||
        if (d) emit('update:modelValue', d);
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const time = computed({
 | 
			
		||||
      get: () => q_date.formatDate(props.modelValue, 'HH:mm'),
 | 
			
		||||
      set: (v: string) => {
 | 
			
		||||
        const d = modifyTime(v);
 | 
			
		||||
        if (d) emit('update:modelValue', d);
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const dateTime = computed({
 | 
			
		||||
      get: () => (props.modelValue ? q_date.formatDate(props.modelValue, placeholder.value) : ''),
 | 
			
		||||
      set: (v: string) => {
 | 
			
		||||
        if (!v) emit('update:modelValue', undefined);
 | 
			
		||||
        switch (props.type) {
 | 
			
		||||
          case 'date':
 | 
			
		||||
            date.value = v;
 | 
			
		||||
            break;
 | 
			
		||||
          case 'time':
 | 
			
		||||
            time.value = v;
 | 
			
		||||
            break;
 | 
			
		||||
          case 'datetime':
 | 
			
		||||
            const split = v.split(' ').filter((c) => c !== '');
 | 
			
		||||
            if (split.length == 2) {
 | 
			
		||||
              const d = modifyTime(split[1], modifyDate(split[0]));
 | 
			
		||||
              if (d) emit('update:modelValue', d);
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function modifyTime(v: string, d: Date | undefined = props.modelValue) {
 | 
			
		||||
      if (d && /^\d\d:\d\d$/.test(v)) {
 | 
			
		||||
        const split = v.split(':');
 | 
			
		||||
        return q_date.adjustDate(d, { hours: +split[0], minutes: +split[1] });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function modifyDate(v: string, d: Date | undefined = props.modelValue) {
 | 
			
		||||
      if (!d) d = q_date.buildDate({ hours: 0, minutes: 0, seconds: 0 });
 | 
			
		||||
      if (/^\d{4}-\d\d-\d\d$/.test(v)) {
 | 
			
		||||
        const split = v.split('-');
 | 
			
		||||
        return q_date.adjustDate(d, {
 | 
			
		||||
          year: +split[0],
 | 
			
		||||
          month: +split[1],
 | 
			
		||||
          date: +split[2],
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      attrs,
 | 
			
		||||
      clearable,
 | 
			
		||||
      customRules,
 | 
			
		||||
      date,
 | 
			
		||||
      dateTime,
 | 
			
		||||
      placeholder,
 | 
			
		||||
      time,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,47 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <q-input v-model="password" v-bind="attrs" :label="label" :type="type">
 | 
			
		||||
    <template #append><q-icon :name="name" class="cursor-pointer" @click="toggle" /></template
 | 
			
		||||
  ></q-input>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineComponent, ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'PasswordInput',
 | 
			
		||||
  props: {
 | 
			
		||||
    modelValue: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
    label: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: '',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  emits: {
 | 
			
		||||
    'update:modelValue': (value: string) => !!value,
 | 
			
		||||
  },
 | 
			
		||||
  setup(props, { emit, attrs }) {
 | 
			
		||||
    const isPassword = ref(true);
 | 
			
		||||
    const type = computed(() => (isPassword.value ? 'password' : 'text'));
 | 
			
		||||
    const name = computed(() => (isPassword.value ? 'mdi-eye-off' : 'mdi-eye'));
 | 
			
		||||
    const password = computed({
 | 
			
		||||
      get: () => props.modelValue,
 | 
			
		||||
      set: (value: string) => emit('update:modelValue', value),
 | 
			
		||||
    });
 | 
			
		||||
    function toggle() {
 | 
			
		||||
      isPassword.value = !isPassword.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      attrs,
 | 
			
		||||
      isPassword,
 | 
			
		||||
      name,
 | 
			
		||||
      password,
 | 
			
		||||
      toggle,
 | 
			
		||||
      type,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,46 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <q-avatar>
 | 
			
		||||
    <slot :avatar-u-r-l="avatarURL(modelValue)">
 | 
			
		||||
      <q-img :src="avatarURL(modelValue)" style="min-width: 100%; min-height: 100%">
 | 
			
		||||
        <template #error>
 | 
			
		||||
          <img :src="fallback" style="height: 100%" />
 | 
			
		||||
        </template>
 | 
			
		||||
      </q-img>
 | 
			
		||||
    </slot>
 | 
			
		||||
  </q-avatar>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { PropType, defineComponent } from 'vue';
 | 
			
		||||
import { avatarURL } from '@flaschengeist/api';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Display an avatar for an user
 | 
			
		||||
 *
 | 
			
		||||
 * Slots:
 | 
			
		||||
 *  default - scope: {avatarURL}
 | 
			
		||||
 */
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'UserAvatar',
 | 
			
		||||
  props: {
 | 
			
		||||
    modelValue: {
 | 
			
		||||
      type: [Object, String] as PropType<FG.User | string>,
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
    showZoom: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: false,
 | 
			
		||||
    },
 | 
			
		||||
    fallback: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: 'no-image.svg',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  emits: ['error'],
 | 
			
		||||
  setup() {
 | 
			
		||||
    return {
 | 
			
		||||
      avatarURL,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +0,0 @@
 | 
			
		|||
import IsoDateInput from './IsoDateInput.vue';
 | 
			
		||||
import PasswordInput from './PasswordInput.vue';
 | 
			
		||||
import UserAvatar from './UserAvatar.vue';
 | 
			
		||||
 | 
			
		||||
export { IsoDateInput, PasswordInput, UserAvatar };
 | 
			
		||||
							
								
								
									
										10
									
								
								api/index.ts
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -1,10 +0,0 @@
 | 
			
		|||
export { api, pinia } from './src/internal';
 | 
			
		||||
 | 
			
		||||
export * from './src/stores/';
 | 
			
		||||
 | 
			
		||||
export * from './src/utils/datetime';
 | 
			
		||||
export * from './src/utils/permission';
 | 
			
		||||
export * from './src/utils/persistent';
 | 
			
		||||
export * from './src/utils/user';
 | 
			
		||||
export * from './src/utils/validators';
 | 
			
		||||
export * from './src/utils/misc';
 | 
			
		||||
| 
						 | 
				
			
			@ -1,28 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "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"
 | 
			
		||||
  },
 | 
			
		||||
  "main": "./src/index.ts",
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@quasar/app-webpack": "^3.7.2",
 | 
			
		||||
    "flaschengeist": "^2.0.0",
 | 
			
		||||
    "pinia": "^2.0.8"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@flaschengeist/types": "^1.0.0",
 | 
			
		||||
    "@types/node": "^14.18.0",
 | 
			
		||||
    "typescript": "^4.5.4"
 | 
			
		||||
  },
 | 
			
		||||
  "prettier": {
 | 
			
		||||
    "singleQuote": true,
 | 
			
		||||
    "semi": true,
 | 
			
		||||
    "printWidth": 100,
 | 
			
		||||
    "arrowParens": "always"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
//https://github.com/vuejs/vue-next/issues/3130
 | 
			
		||||
declare module '*.vue' {
 | 
			
		||||
  import { ComponentOptions } from 'vue';
 | 
			
		||||
  const component: ComponentOptions;
 | 
			
		||||
  export default component;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
import axios from 'axios';
 | 
			
		||||
import { createPinia } from 'pinia';
 | 
			
		||||
 | 
			
		||||
export const api = axios.create();
 | 
			
		||||
 | 
			
		||||
export const pinia = createPinia();
 | 
			
		||||
| 
						 | 
				
			
			@ -1,23 +0,0 @@
 | 
			
		|||
import { AxiosError } from 'axios';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check if error is an AxiosError, and optional if a specific status was returned
 | 
			
		||||
 *
 | 
			
		||||
 * @param error Thrown error to check
 | 
			
		||||
 * @param status If set, check if this error has set thouse status code
 | 
			
		||||
 */
 | 
			
		||||
export function isAxiosError(error: unknown, status?: number) {
 | 
			
		||||
  // Check if it is an axios error (with axios 1.0 `error instanceof AxiosError` will be possible)
 | 
			
		||||
  if (typeof error !== 'object' || !error || !('isAxiosError' in error)) return false;
 | 
			
		||||
  // Check status code if status was given
 | 
			
		||||
  if (status !== undefined)
 | 
			
		||||
    return (
 | 
			
		||||
      (<AxiosError>error).response !== undefined && (<AxiosError>error).response?.status === status
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export * from './main';
 | 
			
		||||
export * from './session';
 | 
			
		||||
export * from './user';
 | 
			
		||||
| 
						 | 
				
			
			@ -1,163 +0,0 @@
 | 
			
		|||
import { FG_Plugin } from '@flaschengeist/types';
 | 
			
		||||
import { fixSession, useSessionStore, useUserStore } from '.';
 | 
			
		||||
import { AxiosResponse } from 'axios';
 | 
			
		||||
import { api } from '../internal';
 | 
			
		||||
import { defineStore } from 'pinia';
 | 
			
		||||
import { PersistentStorage } from '../utils/persistent';
 | 
			
		||||
import { LocalStorage, SessionStorage } from 'quasar';
 | 
			
		||||
function reviveSession() {
 | 
			
		||||
  return PersistentStorage.get<FG.Session>('fg_session').then((s) => fixSession(s || undefined));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function clearPersistant() {
 | 
			
		||||
  void PersistentStorage.remove('fg_session');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function saveSession(session?: FG.Session) {
 | 
			
		||||
  if (session === undefined) return clearPersistant();
 | 
			
		||||
  PersistentStorage.set('fg_session', session).catch(() =>
 | 
			
		||||
    console.error('Could not save token to storage')
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useMainStore = defineStore({
 | 
			
		||||
  id: 'main',
 | 
			
		||||
 | 
			
		||||
  state: () => ({
 | 
			
		||||
    session: undefined as FG.Session | undefined,
 | 
			
		||||
    user: undefined as FG.User | undefined,
 | 
			
		||||
    notifications: [] as Array<FG_Plugin.Notification>,
 | 
			
		||||
    shortcuts: [] as Array<FG_Plugin.MenuLink>,
 | 
			
		||||
  }),
 | 
			
		||||
 | 
			
		||||
  getters: {
 | 
			
		||||
    loggedIn(): boolean {
 | 
			
		||||
      return this.session !== undefined;
 | 
			
		||||
    },
 | 
			
		||||
    currentUser(): FG.User {
 | 
			
		||||
      if (this.user === undefined) throw 'Not logged in, this should not be called';
 | 
			
		||||
      return this.user;
 | 
			
		||||
    },
 | 
			
		||||
    currentSession(): FG.Session {
 | 
			
		||||
      if (this.session === undefined) throw 'Not logged in, this should not be called';
 | 
			
		||||
      return this.session;
 | 
			
		||||
    },
 | 
			
		||||
    permissions(): string[] {
 | 
			
		||||
      return this.user?.permissions || [];
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  actions: {
 | 
			
		||||
    /** Ininitalize store from saved session
 | 
			
		||||
     *  Updates session and loads current user
 | 
			
		||||
     */
 | 
			
		||||
    async init() {
 | 
			
		||||
      const sessionStore = useSessionStore();
 | 
			
		||||
      const userStore = useUserStore();
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        this.session = await reviveSession();
 | 
			
		||||
        if (this.session !== undefined) {
 | 
			
		||||
          this.session = await sessionStore.getSession(this.session.token);
 | 
			
		||||
          if (this.session !== undefined) this.user = await userStore.getUser(this.session.userid);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.warn('Could not load token from storage', error);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async login(userid: string, password: string) {
 | 
			
		||||
      const userStore = useUserStore();
 | 
			
		||||
      try {
 | 
			
		||||
        const { data } = await api.post<FG.Session>('/auth', { userid, password });
 | 
			
		||||
        this.session = fixSession(data);
 | 
			
		||||
        this.user = await userStore.getUser(data.userid, true);
 | 
			
		||||
        return true;
 | 
			
		||||
      } catch ({ response }) {
 | 
			
		||||
        this.handleLoggedOut();
 | 
			
		||||
        return (<AxiosResponse | undefined>response)?.status || false;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async logout() {
 | 
			
		||||
      if (!this.session || !this.session.token) return false;
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const token = this.session.token;
 | 
			
		||||
        await api.delete(`/auth/${token}`);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        return false;
 | 
			
		||||
      } finally {
 | 
			
		||||
        this.handleLoggedOut();
 | 
			
		||||
      }
 | 
			
		||||
      return true;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async requestReset(userid: string) {
 | 
			
		||||
      return await api
 | 
			
		||||
        .post('/auth/reset', { userid })
 | 
			
		||||
        .then(() => true)
 | 
			
		||||
        .catch(() => false);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async resetPassword(token: string, password: string) {
 | 
			
		||||
      return await api
 | 
			
		||||
        .post('/auth/reset', { token, password })
 | 
			
		||||
        .then(() => true)
 | 
			
		||||
        .catch(({ response }) =>
 | 
			
		||||
          response && 'status' in response ? (<AxiosResponse>response).status : false
 | 
			
		||||
        );
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async loadNotifications(flaschengeist: FG_Plugin.Flaschengeist) {
 | 
			
		||||
      const { data } = await api.get<FG.Notification[]>('/notifications', {
 | 
			
		||||
        params:
 | 
			
		||||
          this.notifications.length > 0
 | 
			
		||||
            ? { from: this.notifications[this.notifications.length - 1].time }
 | 
			
		||||
            : {},
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const notes = [] as FG_Plugin.Notification[];
 | 
			
		||||
      data.forEach((n) => {
 | 
			
		||||
        n.time = new Date(n.time);
 | 
			
		||||
        const plugin = flaschengeist?.plugins.filter((p) => p.id === n.plugin)[0];
 | 
			
		||||
        if (!plugin) console.debug('Could not find a parser for this notification', n);
 | 
			
		||||
        else notes.push(plugin.notification(n));
 | 
			
		||||
      });
 | 
			
		||||
      this.notifications.push(...notes);
 | 
			
		||||
      return notes;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async removeNotification(id: number) {
 | 
			
		||||
      const idx = this.notifications.findIndex((n) => n.id === id);
 | 
			
		||||
      if (idx >= 0)
 | 
			
		||||
        try {
 | 
			
		||||
          this.notifications.splice(idx, 1);
 | 
			
		||||
          await api.delete(`/notifications/${id}`);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          if (this.notifications.length > idx)
 | 
			
		||||
            this.notifications.splice(idx, this.notifications.length - idx - 1);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async getShortcuts() {
 | 
			
		||||
      const { data } = await api.get<Array<FG_Plugin.MenuLink>>(
 | 
			
		||||
        `users/${this.currentUser.userid}/shortcuts`
 | 
			
		||||
      );
 | 
			
		||||
      this.shortcuts = data;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async setShortcuts() {
 | 
			
		||||
      await api.put(`users/${this.currentUser.userid}/shortcuts`, this.shortcuts);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    handleLoggedOut() {
 | 
			
		||||
      this.$reset();
 | 
			
		||||
      void clearPersistant();
 | 
			
		||||
      LocalStorage.clear();
 | 
			
		||||
      SessionStorage.clear();
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default () => useMainStore;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,71 +0,0 @@
 | 
			
		|||
import { AxiosResponse } from 'axios';
 | 
			
		||||
import { defineStore } from 'pinia';
 | 
			
		||||
import { api } from '../internal';
 | 
			
		||||
import { isAxiosError, useMainStore } from '.';
 | 
			
		||||
 | 
			
		||||
export function fixSession(s?: FG.Session) {
 | 
			
		||||
  return !s ? s : Object.assign(s, { expires: new Date(s.expires) });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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(() => undefined);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async getSessions() {
 | 
			
		||||
      try {
 | 
			
		||||
        const { data } = await api.get<FG.Session[]>('/auth');
 | 
			
		||||
        data.forEach(fixSession);
 | 
			
		||||
 | 
			
		||||
        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) {
 | 
			
		||||
        // Ignore 401, as this means we are already logged out, throw all other
 | 
			
		||||
        if (!isAxiosError(error, 401)) throw error;
 | 
			
		||||
      }
 | 
			
		||||
      return false;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async updateSession(lifetime: number, token: string) {
 | 
			
		||||
      try {
 | 
			
		||||
        const { data } = await api.put<FG.Session>(`auth/${token}`, { value: lifetime });
 | 
			
		||||
        fixSession(data);
 | 
			
		||||
 | 
			
		||||
        const mainStore = useMainStore();
 | 
			
		||||
        if (mainStore.session?.token == data.token) mainStore.session = data;
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,278 +0,0 @@
 | 
			
		|||
import { defineStore } from 'pinia';
 | 
			
		||||
import { api } from '../internal';
 | 
			
		||||
import { isAxiosError, useMainStore } from '.';
 | 
			
		||||
import { DisplayNameMode } from '@flaschengeist/users';
 | 
			
		||||
 | 
			
		||||
export function fixUser(u?: FG.User) {
 | 
			
		||||
  return !u ? u : Object.assign(u, { birthday: u.birthday ? new Date(u.birthday) : undefined });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check if state is outdated / dirty
 | 
			
		||||
 * Value is considered outdated after 15 minutes
 | 
			
		||||
 * @param updated Time of last updated (in milliseconds see Date.now())
 | 
			
		||||
 * @returns True if outdated, false otherwise
 | 
			
		||||
 */
 | 
			
		||||
function isDirty(updated: number) {
 | 
			
		||||
  return Date.now() - updated > 15 * 60 * 1000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useUserStore = defineStore({
 | 
			
		||||
  id: 'users',
 | 
			
		||||
 | 
			
		||||
  state: () => ({
 | 
			
		||||
    roles: [] as FG.Role[],
 | 
			
		||||
    permissions: [] as FG.Permission[],
 | 
			
		||||
    userSettings: {} as FG.UserSettings,
 | 
			
		||||
    // list of all users, include deleted ones, use `users` getter for list of active ones
 | 
			
		||||
    _users: [] as FG.User[],
 | 
			
		||||
    // Internal flags for deciding if lists need to force-loaded
 | 
			
		||||
    _dirty_users: 0,
 | 
			
		||||
    _dirty_roles: 0,
 | 
			
		||||
  }),
 | 
			
		||||
 | 
			
		||||
  getters: {
 | 
			
		||||
    users(state) {
 | 
			
		||||
      const u = state._users.filter((u) => !u.deleted);
 | 
			
		||||
 | 
			
		||||
      switch (this.userSettings['display_name']) {
 | 
			
		||||
        case DisplayNameMode.FIRSTNAME_LASTNAME || DisplayNameMode.FIRSTNAME:
 | 
			
		||||
          u.sort((a, b) => {
 | 
			
		||||
            const a_lastname = a.lastname.toLowerCase();
 | 
			
		||||
            const b_lastname = b.lastname.toLowerCase();
 | 
			
		||||
            const a_firstname = a.firstname.toLowerCase();
 | 
			
		||||
            const b_firstname = b.firstname.toLowerCase();
 | 
			
		||||
            if (a_firstname === b_firstname) {
 | 
			
		||||
              return a_lastname < b_lastname ? -1 : 1;
 | 
			
		||||
            }
 | 
			
		||||
            return a_firstname < b_firstname ? -1 : 1;
 | 
			
		||||
          });
 | 
			
		||||
          break;
 | 
			
		||||
        case <string>DisplayNameMode.DISPLAYNAME:
 | 
			
		||||
          u.sort((a, b) => {
 | 
			
		||||
            const a_displayname = a.display_name.toLowerCase();
 | 
			
		||||
            const b_displayname = b.display_name.toLowerCase();
 | 
			
		||||
            return a_displayname < b_displayname ? -1 : 1;
 | 
			
		||||
          });
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          u.sort((a, b) => {
 | 
			
		||||
            const a_lastname = a.lastname.toLowerCase();
 | 
			
		||||
            const b_lastname = b.lastname.toLowerCase();
 | 
			
		||||
            const a_firstname = a.firstname.toLowerCase();
 | 
			
		||||
            const b_firstname = b.firstname.toLowerCase();
 | 
			
		||||
            if (a_lastname === b_lastname) {
 | 
			
		||||
              return a_firstname < b_firstname ? -1 : 1;
 | 
			
		||||
            }
 | 
			
		||||
            return a_lastname < b_lastname ? -1 : 1;
 | 
			
		||||
          });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return u;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  actions: {
 | 
			
		||||
    /** Simply filter all users by ID */
 | 
			
		||||
    findUser(userid: string) {
 | 
			
		||||
      return this._users.find((user) => user.userid === userid);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /** Retrieve user by ID
 | 
			
		||||
     * @param userid ID of user to retrieve
 | 
			
		||||
     * @param force If set to true the user is loaded from backend even when a local copy is available
 | 
			
		||||
     * @returns Retrieved user (Promise) or raise an error
 | 
			
		||||
     * @throws Probably an AxiosError if loading failed
 | 
			
		||||
     */
 | 
			
		||||
    async getUser(userid: string, force = false) {
 | 
			
		||||
      const idx = this._users.findIndex((user) => user.userid === userid);
 | 
			
		||||
      if (force || idx === -1 || isDirty(this._dirty_users)) {
 | 
			
		||||
        try {
 | 
			
		||||
          const { data } = await api.get<FG.User>(`/users/${userid}`);
 | 
			
		||||
          fixUser(data);
 | 
			
		||||
          if (idx === -1) this._users.push(data);
 | 
			
		||||
          else this._users[idx] = data;
 | 
			
		||||
          return data;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          // Ignore 404, throw all other
 | 
			
		||||
          if (!isAxiosError(error, 404)) throw error;
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        return this._users[idx];
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /** Retrieve list of all users
 | 
			
		||||
     * @param force If set to true a fresh users list is loaded from backend even when a local copy is available
 | 
			
		||||
     * @returns Array of retrieved users (Promise)
 | 
			
		||||
     * @throws Probably an AxiosError if loading failed
 | 
			
		||||
     */
 | 
			
		||||
    async getUsers(force = false) {
 | 
			
		||||
      if (force || isDirty(this._dirty_users)) {
 | 
			
		||||
        const { data } = await api.get<FG.User[]>('/users');
 | 
			
		||||
        data.forEach(fixUser);
 | 
			
		||||
        this._users = data;
 | 
			
		||||
        this._dirty_users = Date.now();
 | 
			
		||||
      }
 | 
			
		||||
      return this._users;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /** Save modifications of user on backend
 | 
			
		||||
     * @param user Modified user to save
 | 
			
		||||
     * @throws Probably an AxiosError if request failed (404 = Invalid userid, 400 = Invalid data)
 | 
			
		||||
     */
 | 
			
		||||
    async updateUser(user: FG.User) {
 | 
			
		||||
      await api.put(`/users/${user.userid}`, user);
 | 
			
		||||
      // Modifcation accepted by backend
 | 
			
		||||
      // Save modifications back to our users list
 | 
			
		||||
      const idx = this._users.findIndex((u) => u.userid === user.userid);
 | 
			
		||||
      if (idx > -1) this._users[idx] = user;
 | 
			
		||||
      // If user was current user, save modifications back to the main store
 | 
			
		||||
      const mainStore = useMainStore();
 | 
			
		||||
      if (user.userid === mainStore.user?.userid) mainStore.user = user;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /** Register a new user
 | 
			
		||||
     * @param user User to register (id not set)
 | 
			
		||||
     * @returns The registered user (id set)
 | 
			
		||||
     * @throws Probably an AxiosError if request failed
 | 
			
		||||
     */
 | 
			
		||||
    async createUser(user: FG.User) {
 | 
			
		||||
      const { data } = await api.post<FG.User>('/users', user);
 | 
			
		||||
      this._users.push(<FG.User>fixUser(data));
 | 
			
		||||
      return data;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /** Delete an user
 | 
			
		||||
     * Throws if failed and resolves void if succeed
 | 
			
		||||
     *
 | 
			
		||||
     * @param user User or ID of user to delete
 | 
			
		||||
     * @throws Probably an AxiosError if request failed
 | 
			
		||||
     */
 | 
			
		||||
    async deleteUser(user: FG.User | string) {
 | 
			
		||||
      if (typeof user === 'object') user = user.userid;
 | 
			
		||||
 | 
			
		||||
      await api.delete(`/users/${user}`);
 | 
			
		||||
      this._users = this._users.filter((u) => u.userid != user);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /** Upload an avatar for an user
 | 
			
		||||
     * Throws if failed and resolves void if succeed
 | 
			
		||||
     *
 | 
			
		||||
     * @param user User or ID of user
 | 
			
		||||
     * @param file Avatar file to upload
 | 
			
		||||
     * @throws Probably an AxiosError if request failed
 | 
			
		||||
     */
 | 
			
		||||
    async uploadAvatar(user: FG.User | string, file: string | File) {
 | 
			
		||||
      if (typeof user === 'object') user = user.userid;
 | 
			
		||||
 | 
			
		||||
      const formData = new FormData();
 | 
			
		||||
      formData.append('file', file);
 | 
			
		||||
      await api.post(`/users/${user}/avatar`, formData, {
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'multipart/form-data',
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /** Delete avatar of an user
 | 
			
		||||
     * @param user User or ID of user
 | 
			
		||||
     * @throws Probably an AxiosError if request failed
 | 
			
		||||
     */
 | 
			
		||||
    async deleteAvatar(user: FG.User | string) {
 | 
			
		||||
      if (typeof user === 'object') user = user.userid;
 | 
			
		||||
 | 
			
		||||
      await api.delete(`/users/${user}/avatar`);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /** Retrieve list of all permissions
 | 
			
		||||
     * @param force If set to true a fresh list is loaded from backend even when a local copy is available
 | 
			
		||||
     * @returns Array of retrieved permissions (Promise)
 | 
			
		||||
     * @throws Probably an AxiosError if request failed
 | 
			
		||||
     */
 | 
			
		||||
    async getPermissions(force = false) {
 | 
			
		||||
      if (force || this.permissions.length === 0) {
 | 
			
		||||
        const { data } = await api.get<FG.Permission[]>('/roles/permissions');
 | 
			
		||||
        this.permissions = data;
 | 
			
		||||
      }
 | 
			
		||||
      return this.permissions;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /** Retrieve list of all roles
 | 
			
		||||
     * @param force If set to true a fresh list is loaded from backend even when a local copy is available
 | 
			
		||||
     * @returns Array of retrieved roles (Promise)
 | 
			
		||||
     * @throws Probably an AxiosError if request failed
 | 
			
		||||
     */
 | 
			
		||||
    async getRoles(force = false) {
 | 
			
		||||
      if (force || isDirty(this._dirty_roles)) {
 | 
			
		||||
        const { data } = await api.get<FG.Role[]>('/roles');
 | 
			
		||||
        this.roles = data;
 | 
			
		||||
        this._dirty_roles = Date.now();
 | 
			
		||||
      }
 | 
			
		||||
      return this.roles;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /** Save modifications of role on the backend
 | 
			
		||||
     * @param role role to save
 | 
			
		||||
     * @throws Probably an AxiosError if request failed
 | 
			
		||||
     */
 | 
			
		||||
    async updateRole(role: FG.Role) {
 | 
			
		||||
      await api.put(`/roles/${role.id}`, role);
 | 
			
		||||
 | 
			
		||||
      const idx = this.roles.findIndex((r) => r.id === role.id);
 | 
			
		||||
      if (idx != -1) this.roles[idx] = role;
 | 
			
		||||
      else this._dirty_roles = 0;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /** Create a new role
 | 
			
		||||
     * @param role Role to create (ID not set)
 | 
			
		||||
     * @returns Created role (ID set)
 | 
			
		||||
     * @throws Probably an AxiosError if request failed
 | 
			
		||||
     */
 | 
			
		||||
    async newRole(role: FG.Role) {
 | 
			
		||||
      const { data } = await api.post<FG.Role>('/roles', role);
 | 
			
		||||
      this.roles.push(data);
 | 
			
		||||
      return data;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /** Delete a role
 | 
			
		||||
     * @param role Role or ID of role to delete
 | 
			
		||||
     * @throws Probably an AxiosError if request failed (409 if role still in use)
 | 
			
		||||
     */
 | 
			
		||||
    async deleteRole(role: FG.Role | number) {
 | 
			
		||||
      if (typeof role === 'object') role = role.id;
 | 
			
		||||
      await api.delete(`/roles/${role}`);
 | 
			
		||||
      this.roles = this.roles.filter((r) => r.id !== role);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /** Get Settings for display name mode
 | 
			
		||||
     * @param force If set to true a fresh list is loaded from backend even when a local copy is available
 | 
			
		||||
     * @throws Probably an AxiosError if request failed
 | 
			
		||||
     * @returns Settings for display name mode
 | 
			
		||||
     */
 | 
			
		||||
    async getDisplayNameModeSetting(force = false): Promise<string> {
 | 
			
		||||
      const mainStore = useMainStore();
 | 
			
		||||
      if (force) {
 | 
			
		||||
        const { data } = await api.get<{ data: string }>(
 | 
			
		||||
          `users/${mainStore.currentUser.userid}/setting/display_name_mode`
 | 
			
		||||
        );
 | 
			
		||||
        this.userSettings['display_name'] = data.data;
 | 
			
		||||
      }
 | 
			
		||||
      return this.userSettings['display_name'];
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    /** Set Settings for display name mode
 | 
			
		||||
     * @param mode New display name mode
 | 
			
		||||
     * @throws Probably an AxiosError if request failed
 | 
			
		||||
     * @returns Settings for display name mode
 | 
			
		||||
     */
 | 
			
		||||
    async setDisplayNameModeSetting(mode: string): Promise<string> {
 | 
			
		||||
      const mainStore = useMainStore();
 | 
			
		||||
      await api.put(`users/${mainStore.currentUser.userid}/setting/display_name_mode`, {
 | 
			
		||||
        data: mode,
 | 
			
		||||
      });
 | 
			
		||||
      this.userSettings['display_name'] = mode;
 | 
			
		||||
      return mode;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,45 +0,0 @@
 | 
			
		|||
export function formatDateTime(
 | 
			
		||||
  date: Date,
 | 
			
		||||
  useDate = true,
 | 
			
		||||
  useTime = false,
 | 
			
		||||
  useSeconds = false,
 | 
			
		||||
  useWeekday = false
 | 
			
		||||
) {
 | 
			
		||||
  const dateTimeFormat = new Intl.DateTimeFormat([], {
 | 
			
		||||
    year: useDate ? 'numeric' : undefined,
 | 
			
		||||
    month: useDate ? '2-digit' : undefined,
 | 
			
		||||
    day: useDate ? '2-digit' : undefined,
 | 
			
		||||
    weekday: useWeekday ? 'long' : undefined,
 | 
			
		||||
    hour: useTime ? '2-digit' : undefined,
 | 
			
		||||
    minute: useTime ? '2-digit' : undefined,
 | 
			
		||||
    second: useTime && useSeconds ? '2-digit' : undefined,
 | 
			
		||||
  });
 | 
			
		||||
  return dateTimeFormat.format(date);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function asDate(date?: Date, placeholder = '') {
 | 
			
		||||
  return date ? formatDateTime(date, true) : placeholder;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function asHour(date?: Date, placeholder = '') {
 | 
			
		||||
  return date ? formatDateTime(date, false, true) : placeholder;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatStartEnd(start: Date, end?: Date) {
 | 
			
		||||
  const today = asDate(new Date());
 | 
			
		||||
  const startDate = asDate(start);
 | 
			
		||||
  const endDate = end ? asDate(end) : '';
 | 
			
		||||
  return (
 | 
			
		||||
    (today !== startDate ? `${startDate}, ` : '') +
 | 
			
		||||
    asHour(start) +
 | 
			
		||||
    (end ? ' - ' + (endDate !== startDate ? `${endDate}, ` : '') + asHour(end) : '')
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function startOfWeek(date: Date, startMonday = true) {
 | 
			
		||||
  const start = new Date(date);
 | 
			
		||||
  const day = date.getDay() || 7;
 | 
			
		||||
  if (startMonday && day !== 1) start.setHours(-24 * (day - 1));
 | 
			
		||||
  else if (!startMonday && day !== 7) start.setHours(-24 * day);
 | 
			
		||||
  return start;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,11 +0,0 @@
 | 
			
		|||
import { watch, WatchSource } from 'vue';
 | 
			
		||||
import { LoadingBar } from 'quasar';
 | 
			
		||||
 | 
			
		||||
function setLoadingBar(loading: WatchSource<boolean>) {
 | 
			
		||||
  return watch<boolean>(loading, (loading) => {
 | 
			
		||||
    if (loading) LoadingBar.start(10000);
 | 
			
		||||
    if (!loading) LoadingBar.stop();
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default setLoadingBar;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
export function clone<T>(o: T): T {
 | 
			
		||||
  return <T>JSON.parse(JSON.stringify(o));
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +0,0 @@
 | 
			
		|||
import { useMainStore } from '../stores';
 | 
			
		||||
 | 
			
		||||
export function hasPermission(permission: string) {
 | 
			
		||||
  const store = useMainStore();
 | 
			
		||||
  return store.permissions.includes(permission);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function hasPermissions(needed: string[]) {
 | 
			
		||||
  const store = useMainStore();
 | 
			
		||||
  return needed.every((value) => store.permissions.includes(value));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function hasSomePermissions(needed: string[]) {
 | 
			
		||||
  const store = useMainStore();
 | 
			
		||||
  return needed.some((value) => store.permissions.includes(value));
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,35 +0,0 @@
 | 
			
		|||
import { LocalStorage, Platform } from 'quasar';
 | 
			
		||||
import { Preferences } from '@capacitor/preferences';
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
type PersitentTypes = Date | RegExp | number | boolean | string | object;
 | 
			
		||||
 | 
			
		||||
export class PersistentStorage {
 | 
			
		||||
  static clear() {
 | 
			
		||||
    if (Platform.is.capacitor) return Preferences.clear();
 | 
			
		||||
    else return Promise.resolve(LocalStorage.clear());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static remove(key: string) {
 | 
			
		||||
    if (Platform.is.capacitor) return Preferences.remove({ key: key });
 | 
			
		||||
    else return Promise.resolve(LocalStorage.remove(key));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static set(key: string, value: PersitentTypes) {
 | 
			
		||||
    if (Platform.is.capacitor) return Preferences.set({ key, value: JSON.stringify(value) });
 | 
			
		||||
    else return Promise.resolve(LocalStorage.set(key, value));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get<T extends PersitentTypes>(key: string) {
 | 
			
		||||
    if (Platform.is.capacitor)
 | 
			
		||||
      return Preferences.get({ key }).then((v) =>
 | 
			
		||||
        v.value === null ? null : (JSON.parse(v.value) as T)
 | 
			
		||||
      );
 | 
			
		||||
    else return Promise.resolve(LocalStorage.getItem<T>(key));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static keys() {
 | 
			
		||||
    if (Platform.is.capacitor) return Preferences.keys().then((v) => v.keys);
 | 
			
		||||
    else return Promise.resolve(LocalStorage.getAllKeys());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
import { api } from '../internal';
 | 
			
		||||
 | 
			
		||||
export function avatarURL(user: FG.User | string, thumbnail = true) {
 | 
			
		||||
  if (typeof user === 'object') user = user.userid;
 | 
			
		||||
  return `${api.defaults?.baseURL || ''}/users/${user}/avatar${thumbnail ? '?thumbnail' : ''}`;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,23 +0,0 @@
 | 
			
		|||
export type Validator<T = unknown> = (value?: T | null) => boolean | string;
 | 
			
		||||
 | 
			
		||||
export function notEmpty(val: unknown) {
 | 
			
		||||
  return !!val || 'Feld darf nicht leer sein!';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function stringIsDate(val: string) {
 | 
			
		||||
  return !val || /^\d{4}-\d\d-\d\d$/.test(val) || 'Datum ist nicht gültig.';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function stringIsTime(val: string) {
 | 
			
		||||
  return !val || /^\d\d:\d\d$/.test(val) || 'Zeit ist nicht gültig.';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function stringIsDateTime(val: string) {
 | 
			
		||||
  return !val || /^\d{4}-\d\d-\d\d \d\d:\d\d$/.test(val) || 'Datum und Zeit ist nicht gültig.';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isEmail(val: string) {
 | 
			
		||||
  return (
 | 
			
		||||
    !val || /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w\w+)+$/.test(val) || 'E-Mail ist nicht gültig.'
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
  "extends": "@quasar/app/tsconfig-preset",
 | 
			
		||||
  "target": "esnext",
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "baseUrl": "./",
 | 
			
		||||
    "lib": ["es2020", "dom"],
 | 
			
		||||
    "types": ["@flaschengeist/types", "@quasar/app", "node"]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
/* eslint-env node */
 | 
			
		||||
module.exports = {
 | 
			
		||||
  presets: ['@quasar/babel-preset-app'],
 | 
			
		||||
};
 | 
			
		||||
  presets: [
 | 
			
		||||
    '@vue/cli-plugin-babel/preset'
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIDazCCAlOgAwIBAgIJAJGH2ozWvd1RMA0GCSqGSIb3DQEBCwUAMFMxCzAJBgNV
 | 
			
		||||
BAYTAkRFMQ8wDQYDVQQIDAZTYXhvbnkxEDAOBgNVBAcMB0RyZXNkZW4xITAfBgNV
 | 
			
		||||
BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDAxMTcwOTA0MDFaFw0z
 | 
			
		||||
MDAxMDQwOTA0MDFaMEQxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZTYXhvbnkxEDAO
 | 
			
		||||
BgNVBAcMB0RyZXNkZW4xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcN
 | 
			
		||||
AQEBBQADggEPADCCAQoCggEBALlkr1UOQypLKicESRnse52d5mAX9MjZQpH0/Y5u
 | 
			
		||||
V5WxpPSasmOpt4MRj5MWTfTK2ukj/jLtPAMsggUh7wMXb1uytHj7T5mtiahXBM0H
 | 
			
		||||
1sUi2nScXR6doQZlmqKWDGrVS7WHULM01WhirsnxI8S8e6Evpk4F5/RafKA8FgYI
 | 
			
		||||
Ongg6S1B16+7T0e/FnILoMjKr1jpgzXnVkPFIneu/qVevSNco5/aw+bc6sjeS/ZA
 | 
			
		||||
65dXFGpDlw0lPRHLT5/CgNyMyiLYov7KwMycZw7uxa1ynO+73tqe5tvO/DiMpAPJ
 | 
			
		||||
EkrSz/StYBsGJxDhwq5RT31tHVtHhTf0rk1BmaoQJ0Aq7iECAwEAAaNRME8wHwYD
 | 
			
		||||
VR0jBBgwFoAUt8P5gBfN9hCUAiWhtPH5fTWnctAwCQYDVR0TBAIwADALBgNVHQ8E
 | 
			
		||||
BAMCBPAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQCD
 | 
			
		||||
fBByVq8AbV1DMrY+MElb/nZA5/cuGnUpBpjSlk5OnYHWtywuQk6veiiJ0S2fNfqf
 | 
			
		||||
RzwOFuZDHKmIcH0574VssLfUynMKP3w3xb2ZNic3AxAdhzZ6LXLx6+qF5tYcL7oC
 | 
			
		||||
UWmj5Mo9SkX5HZLEGamQlVyGOGKNatxep4liyoSeKXr0AOHYfB4AkDhVZn7yQc/v
 | 
			
		||||
But42fLBg4mE+rk4UBYOHA4XdoFwqgTCNZq2RxKzvG9LIcok6lOc6gDnfTsH8GqE
 | 
			
		||||
byGpfIIQAXF8aftCm4dGXxtzMh8C5d0t2Ell9g+Rr8i/enebT2nJ9B9ptldDjhcZ
 | 
			
		||||
7I0ywGsXwrh0EwFsX74/
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
-----BEGIN PRIVATE KEY-----
 | 
			
		||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC5ZK9VDkMqSyon
 | 
			
		||||
BEkZ7HudneZgF/TI2UKR9P2ObleVsaT0mrJjqbeDEY+TFk30ytrpI/4y7TwDLIIF
 | 
			
		||||
Ie8DF29bsrR4+0+ZrYmoVwTNB9bFItp0nF0enaEGZZqilgxq1Uu1h1CzNNVoYq7J
 | 
			
		||||
8SPEvHuhL6ZOBef0WnygPBYGCDp4IOktQdevu09HvxZyC6DIyq9Y6YM151ZDxSJ3
 | 
			
		||||
rv6lXr0jXKOf2sPm3OrI3kv2QOuXVxRqQ5cNJT0Ry0+fwoDcjMoi2KL+ysDMnGcO
 | 
			
		||||
7sWtcpzvu97anubbzvw4jKQDyRJK0s/0rWAbBicQ4cKuUU99bR1bR4U39K5NQZmq
 | 
			
		||||
ECdAKu4hAgMBAAECggEABoMQ3Y34sf2d52zxHGYAGZM4SlvND1kCS5otZdleXjW1
 | 
			
		||||
M5pTdci6V3JAdswrxNNzSQkonqVSnFHt5zw/5v3lvXTTfgRl0WIVGcKkuobx9k65
 | 
			
		||||
Gat8YdzrkQv0mI1otj/zvtaX8ROEA3yj4xgDR5/PP+QqlUcD1MNw6TfzFhcn5pxB
 | 
			
		||||
/RDPmvarMhzMdDW60Uub6Z7e/kVPuXWrW4bDyULd1d1NoSibnFZi+vGY0Lc1ctDW
 | 
			
		||||
2Vl7A8RFTcQi6Cjx/FwgPGJTBE4UMjIBO3wnoPQBMrsSxeGhcarerqIlEafgT4XN
 | 
			
		||||
p9BMtRyaXE7TTb1BXc35ZYNJLDLJKQxABhrEHtFreQKBgQDpiGwuKAFK8BLPlbAx
 | 
			
		||||
zkShhKd9fhlwm2bfRv3cojPQZsxn0BjefmtrISbKCD79Ivyn7TnOyYAoKAxdp2q9
 | 
			
		||||
wtz94aAXV2lfhUw2lhcb/aw4sXuY/s1XnVyoglOO8pYRCUN0o80pKuWFsaDyy/uL
 | 
			
		||||
LhINff1oMNCa7vmMdu8Ccz0o/wKBgQDLOqdTQhSFs4f1yhlDDH3pqT6eKvtFNeRJ
 | 
			
		||||
usxYDnAyRXHRqwhQ86z1nBZIgwXqq7PfO9V5Y/l6/2HmmA2ufjS8aBTNpCUMuvJk
 | 
			
		||||
y98Z4hTjKRdnVlMUjHq9ahCixJVQ8pcCnWRFdeAwSKhHQiJEFLYeYOIrUeCIYJI4
 | 
			
		||||
FiCshSPI3wKBgGU0ErWZ7p18FprRIs8itYlNhIwUxo+POPCPwloIDO5GblSa0Pwy
 | 
			
		||||
yvhdIIMzOaDXtahMXN3pYtmEKX+4msBrnvuC+K7E2cxkZtfNCWy+7RCQkaCG45QR
 | 
			
		||||
hOMdv3pWVIRDgHEevz0U8uySQs6VaYgySe6A5/1sEiriX1DpBcEJEbsfAoGAKUCb
 | 
			
		||||
rGvSbJ1XsM24OQL1IBQJsON6o77fuxOe3RT5M0sjYnL8OipsZmKrp0ZpUgxOc7ba
 | 
			
		||||
i0x+3LewMLWWuV/G5qOd7WwvVRkxkMJNZByfLskthf1g2d/2HjLEc7XBtW+4tYAr
 | 
			
		||||
VWoq+sIU3noPKJCnsxzpa++vyx8HLzlWoo5YCDMCgYBJvGH2zMgInlQNO/2XY5nl
 | 
			
		||||
E53EZMex+RDq8Wzr4tRM3IrCGc2t8WKEQ/9teKNH0tg9xib0vhqqmiGl1xNfqJVo
 | 
			
		||||
ePJyfgFabeUx9goG3mgTdV9woSRlBJso62dM0DAC/jsJoHnVzgokysR4/BfW9Da+
 | 
			
		||||
AYTxRZSNbfmsTHawXqG8Fw==
 | 
			
		||||
-----END PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										117
									
								
								package.json
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -1,78 +1,61 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "newgruecht-vue",
 | 
			
		||||
  "version": "1.1.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "version": "2.1.0",
 | 
			
		||||
  "productName": "flaschengeist-frontend",
 | 
			
		||||
  "name": "flaschengeist",
 | 
			
		||||
  "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|dist|.*)/**/}*.{js,ts,vue}'",
 | 
			
		||||
    "lint": "eslint --ext .js,.ts,.vue ./src ./api"
 | 
			
		||||
    "serve": "vue-cli-service serve",
 | 
			
		||||
    "build": "vue-cli-service build",
 | 
			
		||||
    "lint": "vue-cli-service lint"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@flaschengeist/api": "^1.0.0",
 | 
			
		||||
    "@flaschengeist/balance": "^1.0.0",
 | 
			
		||||
    "@flaschengeist/pricelist-old": "^1.0.0",
 | 
			
		||||
    "@flaschengeist/schedule": "^1.0.0",
 | 
			
		||||
    "@flaschengeist/users": "^1.0.0",
 | 
			
		||||
    "axios": "^1.4.0",
 | 
			
		||||
    "pinia": "^2.0.8",
 | 
			
		||||
    "quasar": "^2.11.10",
 | 
			
		||||
    "vue": "^3.0.0",
 | 
			
		||||
    "vue-router": "^4.0.0"
 | 
			
		||||
    "@mdi/font": "^4.9.95",
 | 
			
		||||
    "@mdi/js": "^4.9.95",
 | 
			
		||||
    "core-js": "^3.6.5",
 | 
			
		||||
    "vue": "^2.6.10",
 | 
			
		||||
    "vue-router": "^3.2.0",
 | 
			
		||||
    "vuetify": "^2.3.8",
 | 
			
		||||
    "vuex": "^3.4.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@capacitor/core": "^5.0.0",
 | 
			
		||||
    "@capacitor/preferences": "^5.0.0",
 | 
			
		||||
    "@flaschengeist/types": "^1.0.0",
 | 
			
		||||
    "@quasar/app-webpack": "^3.7.2",
 | 
			
		||||
    "@quasar/extras": "^1.16.3",
 | 
			
		||||
    "@types/node": "^14.18.0",
 | 
			
		||||
    "@types/webpack": "^5.28.0",
 | 
			
		||||
    "@types/webpack-env": "^1.16.3",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^5.8.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^5.8.0",
 | 
			
		||||
    "@vue/devtools": "^6.5.0",
 | 
			
		||||
    "eslint": "^8.5.0",
 | 
			
		||||
    "eslint-config-prettier": "^8.3.0",
 | 
			
		||||
    "eslint-plugin-prettier": "^4.0.0",
 | 
			
		||||
    "eslint-plugin-vue": "^9.14.1",
 | 
			
		||||
    "eslint-webpack-plugin": "^4.0.1",
 | 
			
		||||
    "modify-source-webpack-plugin": "^4.1.0",
 | 
			
		||||
    "prettier": "^2.5.1",
 | 
			
		||||
    "typescript": "^4.5.4",
 | 
			
		||||
    "vuedraggable": "^4.1.0"
 | 
			
		||||
    "@vue/cli-plugin-babel": "^4.3.1",
 | 
			
		||||
    "@vue/cli-plugin-eslint": "^4.3.1",
 | 
			
		||||
    "@vue/cli-plugin-router": "^4.3.1",
 | 
			
		||||
    "@vue/cli-plugin-vuex": "^4.3.1",
 | 
			
		||||
    "@vue/cli-service": "^4.3.1",
 | 
			
		||||
    "@vue/eslint-config-prettier": "^6.0.0",
 | 
			
		||||
    "axios": "^0.19.2",
 | 
			
		||||
    "babel-eslint": "^10.1.0",
 | 
			
		||||
    "eslint": "^5.16.0",
 | 
			
		||||
    "eslint-plugin-prettier": "^3.1.3",
 | 
			
		||||
    "eslint-plugin-vue": "^5.0.0",
 | 
			
		||||
    "material-design-icons-iconfont": "^5.0.1",
 | 
			
		||||
    "prettier": "^1.19.1",
 | 
			
		||||
    "sass": "^1.26.5",
 | 
			
		||||
    "sass-loader": "^8.0.2",
 | 
			
		||||
    "vue-cli-plugin-vuetify": "^2.0.5",
 | 
			
		||||
    "vue-template-compiler": "^2.6.10",
 | 
			
		||||
    "vuetify-loader": "^1.4.4"
 | 
			
		||||
  },
 | 
			
		||||
  "prettier": {
 | 
			
		||||
    "singleQuote": true,
 | 
			
		||||
    "semi": true,
 | 
			
		||||
    "printWidth": 100,
 | 
			
		||||
    "arrowParens": "always"
 | 
			
		||||
  },
 | 
			
		||||
  "browserslist": {
 | 
			
		||||
    "defaults": [
 | 
			
		||||
      "Firefox esr",
 | 
			
		||||
      "last 6 Chrome versions",
 | 
			
		||||
      "last 4 Firefox versions",
 | 
			
		||||
      "last 4 Edge versions",
 | 
			
		||||
      "last 4 Safari versions",
 | 
			
		||||
      "last 4 ChromeAndroid versions",
 | 
			
		||||
      "last 1 FirefoxAndroid versions"
 | 
			
		||||
  "eslintConfig": {
 | 
			
		||||
    "root": true,
 | 
			
		||||
    "env": {
 | 
			
		||||
      "node": true
 | 
			
		||||
    },
 | 
			
		||||
    "extends": [
 | 
			
		||||
      "plugin:vue/essential",
 | 
			
		||||
      "plugin:prettier/recommended",
 | 
			
		||||
      "@vue/prettier",
 | 
			
		||||
      "eslint:recommended"
 | 
			
		||||
    ],
 | 
			
		||||
    "cordova": [
 | 
			
		||||
      "iOS >= 13.0",
 | 
			
		||||
      "Android >= 76",
 | 
			
		||||
      "ChromeAndroid >= 76"
 | 
			
		||||
    ]
 | 
			
		||||
    "rules": {
 | 
			
		||||
      "no-console": "off"
 | 
			
		||||
    },
 | 
			
		||||
    "parserOptions": {
 | 
			
		||||
      "parser": "babel-eslint"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": ">= 14.18.1",
 | 
			
		||||
    "npm": ">= 6.14.12",
 | 
			
		||||
    "yarn": ">= 1.22.0"
 | 
			
		||||
  }
 | 
			
		||||
  "browserslist": [
 | 
			
		||||
    "> 1%",
 | 
			
		||||
    "last 2 versions"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
// You can add your plugins here
 | 
			
		||||
module.exports = [
 | 
			
		||||
//  '@flaschengeist/balance',
 | 
			
		||||
//  '@flaschengeist/schedule',
 | 
			
		||||
//  '@flaschengeist/pricelist',
 | 
			
		||||
  '@flaschengeist/schedule',
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 3.5 KiB  | 
| 
		 Before Width: | Height: | Size: 15 KiB  | 
| 
		 Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 4.2 KiB  | 
| 
						 | 
				
			
			@ -1,92 +0,0 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg
 | 
			
		||||
   xmlns:dc="http://purl.org/dc/elements/1.1/"
 | 
			
		||||
   xmlns:cc="http://creativecommons.org/ns#"
 | 
			
		||||
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
 | 
			
		||||
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
 | 
			
		||||
   width="1024"
 | 
			
		||||
   height="1024"
 | 
			
		||||
   viewBox="0 0 270.93333 270.93334"
 | 
			
		||||
   version="1.1"
 | 
			
		||||
   id="svg8"
 | 
			
		||||
   inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
 | 
			
		||||
   sodipodi:docname="flaschengeist-logo-white.svg"
 | 
			
		||||
   inkscape:export-filename="/Users/crimsen/git/flaschengeist-frontend/public/flaschengeist-logo.png"
 | 
			
		||||
   inkscape:export-xdpi="96"
 | 
			
		||||
   inkscape:export-ydpi="96">
 | 
			
		||||
  <defs
 | 
			
		||||
     id="defs2" />
 | 
			
		||||
  <sodipodi:namedview
 | 
			
		||||
     id="base"
 | 
			
		||||
     pagecolor="#ffffff"
 | 
			
		||||
     bordercolor="#666666"
 | 
			
		||||
     borderopacity="1.0"
 | 
			
		||||
     inkscape:pageopacity="0.0"
 | 
			
		||||
     inkscape:pageshadow="2"
 | 
			
		||||
     inkscape:zoom="0.7"
 | 
			
		||||
     inkscape:cx="391.55984"
 | 
			
		||||
     inkscape:cy="526.94717"
 | 
			
		||||
     inkscape:document-units="mm"
 | 
			
		||||
     inkscape:current-layer="layer1"
 | 
			
		||||
     showgrid="false"
 | 
			
		||||
     units="px"
 | 
			
		||||
     inkscape:window-width="1680"
 | 
			
		||||
     inkscape:window-height="997"
 | 
			
		||||
     inkscape:window-x="0"
 | 
			
		||||
     inkscape:window-y="25"
 | 
			
		||||
     inkscape:window-maximized="1"
 | 
			
		||||
     inkscape:document-rotation="0" />
 | 
			
		||||
  <metadata
 | 
			
		||||
     id="metadata5">
 | 
			
		||||
    <rdf:RDF>
 | 
			
		||||
      <cc:Work
 | 
			
		||||
         rdf:about="">
 | 
			
		||||
        <dc:format>image/svg+xml</dc:format>
 | 
			
		||||
        <dc:type
 | 
			
		||||
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
 | 
			
		||||
        <dc:title></dc:title>
 | 
			
		||||
      </cc:Work>
 | 
			
		||||
    </rdf:RDF>
 | 
			
		||||
  </metadata>
 | 
			
		||||
  <g
 | 
			
		||||
     inkscape:label="Ebene 1"
 | 
			
		||||
     inkscape:groupmode="layer"
 | 
			
		||||
     id="layer1"
 | 
			
		||||
     transform="translate(0,-26.06665)">
 | 
			
		||||
    <circle
 | 
			
		||||
       id="path10"
 | 
			
		||||
       cx="135.46666"
 | 
			
		||||
       cy="161.53333"
 | 
			
		||||
       style="stroke-width:0.26506463;fill:#1976d2;fill-opacity:1;opacity:0"
 | 
			
		||||
       r="135.46666" />
 | 
			
		||||
    <path
 | 
			
		||||
       style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.44061947;stroke-opacity:1"
 | 
			
		||||
       d="m 154.04963,46.75413 c -2.62014,0.516924 -5.22545,1.168424 -7.80617,1.95206 -42.75163,12.981828 -43.91253,68.68501 -54.129684,109.64785 -13.512037,49.65839 -58.120549,32.45922 -53.364321,57.25551 4.247764,22.14545 69.262455,44.34715 71.908285,44.72513 7.43909,1.06272 -52.780019,-26.79368 -40.437456,-42.16974 10.871821,-13.54384 54.907216,-1.28617 101.792266,-18.34148 41.23972,-15.24969 76.0405,-52.05406 67.35884,-95.66274 -8.0739,-40.556134 -44.95534,-65.37088 -85.32176,-57.40659 z m 2.80071,29.231473 5.1e-4,-9.9e-5 c 2.13365,-0.334266 3.95652,0.01931 5.31987,1.031879 4.77648,3.547266 2.89647,14.166887 -4.19904,23.719127 -7.09532,9.55196 -16.7189,14.41932 -21.49476,10.87151 -4.77606,-3.54747 -2.89605,-14.166665 4.19913,-23.718615 4.83297,-6.506353 11.08277,-11.106027 16.17429,-11.903802 z m 47.56936,23.874148 c 2.13386,-0.334401 3.95691,0.01915 5.32037,1.031789 4.77577,3.54775 2.89554,14.16692 -4.19964,23.7187 -7.09514,9.55189 -16.7186,14.41945 -21.49466,10.87203 -4.77578,-3.54775 -2.89554,-14.16693 4.19965,-23.71872 4.83296,-6.50635 11.08277,-11.10602 16.17428,-11.903799 z M 151.12801,132.2755 c 2.17877,-0.55401 4.13861,-0.53197 5.77959,0.065 7.31131,2.65972 6.76636,15.88363 -1.21719,29.5365 -7.98356,13.65282 -20.38249,22.56458 -27.69389,19.90504 -7.31132,-2.65972 -6.76637,-15.88364 1.21719,-29.53651 5.96208,-10.19594 14.66479,-18.12654 21.9143,-19.97007 z"
 | 
			
		||||
       id="path897"
 | 
			
		||||
       inkscape:connector-curvature="0"
 | 
			
		||||
       sodipodi:nodetypes="cccsssccccccscccccccccccccccc" />
 | 
			
		||||
    <ellipse
 | 
			
		||||
       style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332;stroke-opacity:1"
 | 
			
		||||
       id="path962"
 | 
			
		||||
       cx="128.25729"
 | 
			
		||||
       cy="26.431171"
 | 
			
		||||
       rx="17.575893"
 | 
			
		||||
       ry="21.733631"
 | 
			
		||||
       transform="rotate(16.845913)" />
 | 
			
		||||
    <ellipse
 | 
			
		||||
       style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.29560357;stroke-opacity:1"
 | 
			
		||||
       id="path964"
 | 
			
		||||
       cx="245.22121"
 | 
			
		||||
       cy="-179.49768"
 | 
			
		||||
       rx="18.099041"
 | 
			
		||||
       ry="22.821976"
 | 
			
		||||
       transform="matrix(0.33166949,0.94339565,-0.88087771,0.47334392,0,0)" />
 | 
			
		||||
    <path
 | 
			
		||||
       id="path568"
 | 
			
		||||
       style="fill:#ffffff;stroke:#fafdff;stroke-width:0.356;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
			
		||||
       d="m 106.97295,259.61975 c 0.0134,-0.25385 0.0292,-0.51409 0.0479,-0.78105 0.59479,-8.54288 2.97751,-9.88474 2.97751,-9.88474 l 5.54253,1.04351 c 0,0 1.48321,-0.66332 2.12354,-0.41427 0.64035,0.24904 1.37204,1.54389 1.37204,1.54389 l 19.02927,1.6087 c 0,0 0.94865,-0.94047 1.49023,-0.87767 0.54159,0.0628 1.11451,1.0657 1.11451,1.0657 l 5.49906,0.38814 c 0,0 0.7213,-0.79744 1.26976,-0.81461 0.54847,-0.0172 0.96423,0.87635 1.55867,0.86729 5.38159,0.0341 6.75238,-5.59098 8.60298,-8.67755 2.71811,-4.53347 7.42673,-7.64228 12.37681,-7.81427 13.99182,-0.48614 40.20387,8.31062 59.56072,9.9243 0,0 1.79066,-1.55292 6.57665,0.26655 4.78599,1.81946 9.12249,14.44555 8.18358,22.71047 -0.93889,8.26491 -3.47489,19.11211 -13.95088,21.6659 -3.19793,0.77959 -5.68428,-1.12259 -5.68428,-1.12259 -19.43608,0.50444 -53.00574,1.66081 -58.30446,0.46928 -8.87322,-1.99535 -10.84377,-6.76824 -12.19967,-11.35447 -1.60897,-5.44215 -2.79837,-6.93993 -6.77895,-7.55802 -0.51289,-0.0791 -0.87857,0.7764 -1.40831,0.66359 -0.52994,-0.11191 -1.10453,-1.28294 -1.10453,-1.28294 l -5.72273,-0.25317 c 0,0 -0.78608,0.71794 -1.26373,0.67 -0.47765,-0.048 -1.12057,-0.92034 -1.12057,-0.92034 l -18.5064,-1.6898 c 0,0 -0.84969,1.03174 -1.57251,1.09172 -0.72282,0.06 -2.20406,-1.04108 -2.20406,-1.04108 l -5.80786,0.0336 c 0,0 -2.10591,-1.65583 -1.69677,-9.52605 z m 49.4674,-3.83693 c 0,0 11.61657,-9.2335 15.58779,-9.09888 10.08861,0.34198 53.22418,5.36627 53.22418,5.36627 0,0 -51.7418,-11.56961 -56.92412,-9.67165 -3.70211,1.35585 -11.88785,13.40426 -11.88785,13.40426 z" />
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 5.8 KiB  | 
| 
		 Before Width: | Height: | Size: 70 KiB  | 
| 
						 | 
				
			
			@ -1,92 +0,0 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg
 | 
			
		||||
   xmlns:dc="http://purl.org/dc/elements/1.1/"
 | 
			
		||||
   xmlns:cc="http://creativecommons.org/ns#"
 | 
			
		||||
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
 | 
			
		||||
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
 | 
			
		||||
   width="1024"
 | 
			
		||||
   height="1024"
 | 
			
		||||
   viewBox="0 0 270.93333 270.93334"
 | 
			
		||||
   version="1.1"
 | 
			
		||||
   id="svg8"
 | 
			
		||||
   inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
 | 
			
		||||
   sodipodi:docname="flaschengeist-logo.svg"
 | 
			
		||||
   inkscape:export-filename="/Users/crimsen/git/flaschengeist-frontend/public/flaschengeist-logo.png"
 | 
			
		||||
   inkscape:export-xdpi="96"
 | 
			
		||||
   inkscape:export-ydpi="96">
 | 
			
		||||
  <defs
 | 
			
		||||
     id="defs2" />
 | 
			
		||||
  <sodipodi:namedview
 | 
			
		||||
     id="base"
 | 
			
		||||
     pagecolor="#ffffff"
 | 
			
		||||
     bordercolor="#666666"
 | 
			
		||||
     borderopacity="1.0"
 | 
			
		||||
     inkscape:pageopacity="0.0"
 | 
			
		||||
     inkscape:pageshadow="2"
 | 
			
		||||
     inkscape:zoom="0.7"
 | 
			
		||||
     inkscape:cx="391.55984"
 | 
			
		||||
     inkscape:cy="526.94717"
 | 
			
		||||
     inkscape:document-units="mm"
 | 
			
		||||
     inkscape:current-layer="layer1"
 | 
			
		||||
     showgrid="false"
 | 
			
		||||
     units="px"
 | 
			
		||||
     inkscape:window-width="1680"
 | 
			
		||||
     inkscape:window-height="997"
 | 
			
		||||
     inkscape:window-x="0"
 | 
			
		||||
     inkscape:window-y="25"
 | 
			
		||||
     inkscape:window-maximized="1"
 | 
			
		||||
     inkscape:document-rotation="0" />
 | 
			
		||||
  <metadata
 | 
			
		||||
     id="metadata5">
 | 
			
		||||
    <rdf:RDF>
 | 
			
		||||
      <cc:Work
 | 
			
		||||
         rdf:about="">
 | 
			
		||||
        <dc:format>image/svg+xml</dc:format>
 | 
			
		||||
        <dc:type
 | 
			
		||||
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
 | 
			
		||||
        <dc:title />
 | 
			
		||||
      </cc:Work>
 | 
			
		||||
    </rdf:RDF>
 | 
			
		||||
  </metadata>
 | 
			
		||||
  <g
 | 
			
		||||
     inkscape:label="Ebene 1"
 | 
			
		||||
     inkscape:groupmode="layer"
 | 
			
		||||
     id="layer1"
 | 
			
		||||
     transform="translate(0,-26.06665)">
 | 
			
		||||
    <circle
 | 
			
		||||
       id="path10"
 | 
			
		||||
       cx="135.46666"
 | 
			
		||||
       cy="161.53333"
 | 
			
		||||
       style="stroke-width:0.26506463;fill:#1976d2;fill-opacity:1"
 | 
			
		||||
       r="135.46666" />
 | 
			
		||||
    <path
 | 
			
		||||
       style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.44061947;stroke-opacity:1"
 | 
			
		||||
       d="m 154.04963,46.75413 c -2.62014,0.516924 -5.22545,1.168424 -7.80617,1.95206 -42.75163,12.981828 -43.91253,68.68501 -54.129684,109.64785 -13.512037,49.65839 -58.120549,32.45922 -53.364321,57.25551 4.247764,22.14545 69.262455,44.34715 71.908285,44.72513 7.43909,1.06272 -52.780019,-26.79368 -40.437456,-42.16974 10.871821,-13.54384 54.907216,-1.28617 101.792266,-18.34148 41.23972,-15.24969 76.0405,-52.05406 67.35884,-95.66274 -8.0739,-40.556134 -44.95534,-65.37088 -85.32176,-57.40659 z m 2.80071,29.231473 5.1e-4,-9.9e-5 c 2.13365,-0.334266 3.95652,0.01931 5.31987,1.031879 4.77648,3.547266 2.89647,14.166887 -4.19904,23.719127 -7.09532,9.55196 -16.7189,14.41932 -21.49476,10.87151 -4.77606,-3.54747 -2.89605,-14.166665 4.19913,-23.718615 4.83297,-6.506353 11.08277,-11.106027 16.17429,-11.903802 z m 47.56936,23.874148 c 2.13386,-0.334401 3.95691,0.01915 5.32037,1.031789 4.77577,3.54775 2.89554,14.16692 -4.19964,23.7187 -7.09514,9.55189 -16.7186,14.41945 -21.49466,10.87203 -4.77578,-3.54775 -2.89554,-14.16693 4.19965,-23.71872 4.83296,-6.50635 11.08277,-11.10602 16.17428,-11.903799 z M 151.12801,132.2755 c 2.17877,-0.55401 4.13861,-0.53197 5.77959,0.065 7.31131,2.65972 6.76636,15.88363 -1.21719,29.5365 -7.98356,13.65282 -20.38249,22.56458 -27.69389,19.90504 -7.31132,-2.65972 -6.76637,-15.88364 1.21719,-29.53651 5.96208,-10.19594 14.66479,-18.12654 21.9143,-19.97007 z"
 | 
			
		||||
       id="path897"
 | 
			
		||||
       inkscape:connector-curvature="0"
 | 
			
		||||
       sodipodi:nodetypes="cccsssccccccscccccccccccccccc" />
 | 
			
		||||
    <ellipse
 | 
			
		||||
       style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332;stroke-opacity:1"
 | 
			
		||||
       id="path962"
 | 
			
		||||
       cx="128.25729"
 | 
			
		||||
       cy="26.431171"
 | 
			
		||||
       rx="17.575893"
 | 
			
		||||
       ry="21.733631"
 | 
			
		||||
       transform="rotate(16.845913)" />
 | 
			
		||||
    <ellipse
 | 
			
		||||
       style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.29560357;stroke-opacity:1"
 | 
			
		||||
       id="path964"
 | 
			
		||||
       cx="245.22121"
 | 
			
		||||
       cy="-179.49768"
 | 
			
		||||
       rx="18.099041"
 | 
			
		||||
       ry="22.821976"
 | 
			
		||||
       transform="matrix(0.33166949,0.94339565,-0.88087771,0.47334392,0,0)" />
 | 
			
		||||
    <path
 | 
			
		||||
       id="path568"
 | 
			
		||||
       style="fill:#ffffff;stroke:#1976d2;stroke-width:0.356;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
			
		||||
       d="m 106.97295,259.61975 c 0.0134,-0.25385 0.0292,-0.51409 0.0479,-0.78105 0.59479,-8.54288 2.97751,-9.88474 2.97751,-9.88474 l 5.54253,1.04351 c 0,0 1.48321,-0.66332 2.12354,-0.41427 0.64035,0.24904 1.37204,1.54389 1.37204,1.54389 l 19.02927,1.6087 c 0,0 0.94865,-0.94047 1.49023,-0.87767 0.54159,0.0628 1.11451,1.0657 1.11451,1.0657 l 5.49906,0.38814 c 0,0 0.7213,-0.79744 1.26976,-0.81461 0.54847,-0.0172 0.96423,0.87635 1.55867,0.86729 5.38159,0.0341 6.75238,-5.59098 8.60298,-8.67755 2.71811,-4.53347 7.42673,-7.64228 12.37681,-7.81427 13.99182,-0.48614 40.20387,8.31062 59.56072,9.9243 0,0 1.79066,-1.55292 6.57665,0.26655 4.78599,1.81946 9.12249,14.44555 8.18358,22.71047 -0.93889,8.26491 -3.47489,19.11211 -13.95088,21.6659 -3.19793,0.77959 -5.68428,-1.12259 -5.68428,-1.12259 -19.43608,0.50444 -53.00574,1.66081 -58.30446,0.46928 -8.87322,-1.99535 -10.84377,-6.76824 -12.19967,-11.35447 -1.60897,-5.44215 -2.79837,-6.93993 -6.77895,-7.55802 -0.51289,-0.0791 -0.87857,0.7764 -1.40831,0.66359 -0.52994,-0.11191 -1.10453,-1.28294 -1.10453,-1.28294 l -5.72273,-0.25317 c 0,0 -0.78608,0.71794 -1.26373,0.67 -0.47765,-0.048 -1.12057,-0.92034 -1.12057,-0.92034 l -18.5064,-1.6898 c 0,0 -0.84969,1.03174 -1.57251,1.09172 -0.72282,0.06 -2.20406,-1.04108 -2.20406,-1.04108 l -5.80786,0.0336 c 0,0 -2.10591,-1.65583 -1.69677,-9.52605 z m 49.4674,-3.83693 c 0,0 11.61657,-9.2335 15.58779,-9.09888 10.08861,0.34198 53.22418,5.36627 53.22418,5.36627 0,0 -51.7418,-11.56961 -56.92412,-9.67165 -3.70211,1.35585 -11.88785,13.40426 -11.88785,13.40426 z" />
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 5.8 KiB  | 
| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="de">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="utf-8">
 | 
			
		||||
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
			
		||||
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
 | 
			
		||||
    <link rel="icon" href="<%= BASE_URL %>wuicon.ico">
 | 
			
		||||
    <title>Flaschengeist</title>
 | 
			
		||||
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
 | 
			
		||||
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <noscript>
 | 
			
		||||
      <strong>We're sorry but newgruecht-vue doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
 | 
			
		||||
    </noscript>
 | 
			
		||||
    <div id="app"></div>
 | 
			
		||||
    <!-- built files will be auto injected -->
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,168 +0,0 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg
 | 
			
		||||
   xmlns:dc="http://purl.org/dc/elements/1.1/"
 | 
			
		||||
   xmlns:cc="http://creativecommons.org/ns#"
 | 
			
		||||
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
 | 
			
		||||
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
 | 
			
		||||
   width="1024"
 | 
			
		||||
   height="1024"
 | 
			
		||||
   viewBox="0 0 270.93333 270.93334"
 | 
			
		||||
   version="1.1"
 | 
			
		||||
   id="svg37"
 | 
			
		||||
   inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
 | 
			
		||||
   sodipodi:docname="no-image.svg">
 | 
			
		||||
  <defs
 | 
			
		||||
     id="defs31">
 | 
			
		||||
    <rect
 | 
			
		||||
       x="-328.72475"
 | 
			
		||||
       y="24.854798"
 | 
			
		||||
       width="167.56944"
 | 
			
		||||
       height="62.537879"
 | 
			
		||||
       id="rect1100" />
 | 
			
		||||
  </defs>
 | 
			
		||||
  <sodipodi:namedview
 | 
			
		||||
     id="base"
 | 
			
		||||
     pagecolor="#ffffff"
 | 
			
		||||
     bordercolor="#666666"
 | 
			
		||||
     borderopacity="1.0"
 | 
			
		||||
     inkscape:pageopacity="0.0"
 | 
			
		||||
     inkscape:pageshadow="2"
 | 
			
		||||
     inkscape:zoom="0.66"
 | 
			
		||||
     inkscape:cx="-222.85714"
 | 
			
		||||
     inkscape:cy="248.57143"
 | 
			
		||||
     inkscape:document-units="mm"
 | 
			
		||||
     inkscape:current-layer="layer1"
 | 
			
		||||
     inkscape:document-rotation="0"
 | 
			
		||||
     showgrid="false"
 | 
			
		||||
     units="px"
 | 
			
		||||
     inkscape:window-width="2560"
 | 
			
		||||
     inkscape:window-height="1303"
 | 
			
		||||
     inkscape:window-x="0"
 | 
			
		||||
     inkscape:window-y="25"
 | 
			
		||||
     inkscape:window-maximized="1" />
 | 
			
		||||
  <metadata
 | 
			
		||||
     id="metadata34">
 | 
			
		||||
    <rdf:RDF>
 | 
			
		||||
      <cc:Work
 | 
			
		||||
         rdf:about="">
 | 
			
		||||
        <dc:format>image/svg+xml</dc:format>
 | 
			
		||||
        <dc:type
 | 
			
		||||
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
 | 
			
		||||
        <dc:title></dc:title>
 | 
			
		||||
      </cc:Work>
 | 
			
		||||
    </rdf:RDF>
 | 
			
		||||
  </metadata>
 | 
			
		||||
  <g
 | 
			
		||||
     inkscape:label="Ebene 1"
 | 
			
		||||
     inkscape:groupmode="layer"
 | 
			
		||||
     id="layer1">
 | 
			
		||||
    <circle
 | 
			
		||||
       id="path10"
 | 
			
		||||
       cx="135.46666"
 | 
			
		||||
       cy="135.46666"
 | 
			
		||||
       style="fill:#1976d2;fill-opacity:1;stroke-width:0.265065;opacity:1"
 | 
			
		||||
       r="135.46666" />
 | 
			
		||||
    <path
 | 
			
		||||
       id="path897"
 | 
			
		||||
       style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#1976d2;stroke-width:2.4226772;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
 | 
			
		||||
       d="M 443.15234 56.630859 A 66.428573 82.142858 16.845913 0 0 371.42188 118.32031 A 66.428573 82.142858 16.845913 0 0 411.19531 216.18945 A 66.428573 82.142858 16.845913 0 0 417.72266 217.72852 C 381.34438 302.48672 370.45756 410.53422 348.14648 499.98438 C 297.07736 687.66963 128.47878 622.66455 146.45508 716.38281 C 160.50823 789.6481 350.53291 863.07431 404.40039 881.20117 C 404.36927 881.70781 404.3327 882.22702 404.30664 882.7207 C 402.76028 912.46642 410.7207 918.72461 410.7207 918.72461 L 432.67188 918.59766 C 432.67187 918.59766 438.27004 922.75802 441.00195 922.53125 C 443.73387 922.30455 446.94531 918.40625 446.94531 918.40625 L 516.89062 924.79297 C 516.89062 924.79297 519.31971 928.09007 521.125 928.27148 C 522.93029 928.45267 525.90234 925.73828 525.90234 925.73828 L 547.53125 926.69531 C 547.53125 926.69531 549.70216 931.12195 551.70508 931.54492 C 553.70725 931.97129 555.09081 928.73815 557.0293 929.03711 C 572.07401 931.3732 576.56924 937.03281 582.65039 957.60156 C 587.77505 974.93535 595.22123 992.9761 628.75781 1000.5176 C 648.78447 1005.021 775.66385 1000.6487 849.12305 998.74219 C 849.12305 998.74219 858.5188 1005.9328 870.60547 1002.9863 C 910.19976 993.33421 919.78542 952.33706 923.33398 921.09961 C 926.88262 889.86212 910.49308 842.14037 892.4043 835.26367 C 874.31552 828.38693 867.54688 834.25781 867.54688 834.25781 C 794.38713 828.15886 695.31802 794.91067 642.43555 796.74805 C 623.72658 797.39809 605.92942 809.14688 595.65625 826.28125 C 588.66186 837.94703 583.48245 859.20701 563.14258 859.07812 C 560.89588 859.11237 559.32296 855.73577 557.25 855.80078 C 555.17708 855.86568 552.45117 858.87891 552.45117 858.87891 L 531.66797 857.41211 C 531.66797 857.41211 529.50203 853.62212 527.45508 853.38477 C 525.40816 853.14741 521.82422 856.70312 521.82422 856.70312 L 449.90234 850.62305 C 449.90234 850.62305 447.13702 845.72836 444.7168 844.78711 C 442.29665 843.84582 436.68945 846.35352 436.68945 846.35352 L 415.74219 842.4082 C 415.74219 842.4082 407.89779 846.84558 405.02539 873.69922 C 358.98121 845.1153 229.07948 771.28872 265.40039 726.04102 C 306.49077 674.8517 472.92361 721.17976 650.12695 656.71875 C 726.82706 628.35646 797.61671 580.25753 845.84766 519.08398 A 66.376657 87.827348 48.964508 0 0 927.6875 519.24805 A 66.376657 87.827348 48.964508 0 0 980.98047 413.88477 A 66.376657 87.827348 48.964508 0 0 907.37109 380.61328 C 911.18565 353.15829 910.56576 324.56748 904.71094 295.1582 C 874.19541 141.87518 734.80037 48.0882 582.23438 78.189453 C 572.33148 80.143182 562.48437 82.604632 552.73047 85.566406 C 533.35813 91.448951 516.25153 99.658419 501.06836 109.80859 A 66.428573 82.142858 16.845913 0 0 458.80469 58.953125 A 66.428573 82.142858 16.845913 0 0 443.15234 56.630859 z M 598.64062 188.20703 C 604.22194 188.22929 609.06312 189.70004 612.92773 192.57031 C 630.98057 205.9773 623.87627 246.11384 597.05859 282.2168 C 570.24164 318.31869 533.86885 336.71569 515.81836 323.30664 C 497.76711 309.89888 504.87302 269.76201 531.68945 233.66016 C 549.9558 209.06922 573.57677 191.68513 592.82031 188.66992 L 592.82227 188.66992 C 594.83831 188.35408 596.78019 188.19961 598.64062 188.20703 z M 778.42969 278.44141 C 784.01155 278.46343 788.85382 279.93226 792.71875 282.80273 C 810.7689 296.21155 803.66213 336.34605 776.8457 372.44727 C 750.02943 408.54893 713.65672 426.94663 695.60547 413.53906 C 677.55528 400.13024 684.66205 359.99578 711.47852 323.89453 C 729.74482 299.3036 753.36587 281.91757 772.60938 278.90234 C 774.62562 278.58637 776.56907 278.43406 778.42969 278.44141 z M 582.87695 399.91016 C 586.53338 399.95128 589.93604 400.53593 593.03711 401.66406 C 620.67041 411.71655 618.60959 461.69743 588.43555 513.29883 C 558.26146 564.90004 511.39926 598.58305 483.76562 588.53125 C 456.13229 578.47876 458.1931 528.49788 488.36719 476.89648 C 510.90103 438.36065 543.79364 408.38759 571.19336 401.41992 C 575.31072 400.37297 579.22053 399.86903 582.87695 399.91016 z M 643.83594 816.76172 C 685.1496 816.94824 851.34766 854.11133 851.34766 854.11133 C 851.34766 854.11133 688.31573 835.12065 650.18555 833.82812 C 635.17621 833.31932 591.27148 868.21875 591.27148 868.21875 C 591.27148 868.21875 622.20895 822.68111 636.20117 817.55664 C 637.73138 816.99622 640.33478 816.74591 643.83594 816.76172 z "
 | 
			
		||||
       transform="scale(0.26458333)" />
 | 
			
		||||
    <path
 | 
			
		||||
       id="path10-8"
 | 
			
		||||
       style="fill-opacity:1;stroke-width:3.64724414;fill:#ffffff;stroke-miterlimit:4;stroke-dasharray:none;stroke:#ffffff;stroke-opacity:1;opacity:0.80057803"
 | 
			
		||||
       d="M 512 0 A 511.99997 511.99997 0 0 0 0 512 A 511.99997 511.99997 0 0 0 512 1024 A 511.99997 511.99997 0 0 0 659.25 1002.3672 C 706.36552 1003.0651 793.17503 1000.1943 849.12305 998.74219 C 849.12305 998.74219 858.5188 1005.9328 870.60547 1002.9863 C 910.19976 993.33421 919.78542 952.33706 923.33398 921.09961 C 926.20746 895.80534 916.007 859.7095 902.41797 843.23633 A 511.99997 511.99997 0 0 0 1024 512 A 511.99997 511.99997 0 0 0 512 0 z "
 | 
			
		||||
       transform="scale(0.26458333)" />
 | 
			
		||||
    <rect
 | 
			
		||||
       style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.33699;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
			
		||||
       id="rect942-1"
 | 
			
		||||
       width="76.028526"
 | 
			
		||||
       height="73.388649"
 | 
			
		||||
       x="-21.278112"
 | 
			
		||||
       y="109.32571"
 | 
			
		||||
       ry="3.5350549"
 | 
			
		||||
       transform="rotate(-30.00892)" />
 | 
			
		||||
    <rect
 | 
			
		||||
       style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.391977;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
			
		||||
       id="rect944-7"
 | 
			
		||||
       width="69.4505"
 | 
			
		||||
       height="56.151112"
 | 
			
		||||
       x="-17.909185"
 | 
			
		||||
       y="113.14103"
 | 
			
		||||
       ry="0"
 | 
			
		||||
       transform="rotate(-30.00892)" />
 | 
			
		||||
    <path
 | 
			
		||||
       id="path10-3-94"
 | 
			
		||||
       style="fill:#1976d2;fill-opacity:1;stroke-width:0.0683297"
 | 
			
		||||
       d="m 68.884108,90.871044 a 34.92124,34.92124 0 0 0 -11.74769,43.865396 l 2.70637,4.68588 a 34.92124,34.92124 0 0 0 15.52651,12.5468 L 123.2496,124.31547 a 34.92124,34.92124 0 0 0 -2.38462,-18.10096 l -4.46922,-7.73814 A 34.92124,34.92124 0 0 0 73.568128,88.16575 Z" />
 | 
			
		||||
    <path
 | 
			
		||||
       id="path897-6-8"
 | 
			
		||||
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#1976d2;stroke-width:0.165241;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
			
		||||
       d="m 69.039278,95.120659 a 5.6025989,4.5307973 76.836993 0 0 -2.13225,6.090461 5.6025989,4.5307973 76.836993 0 0 5.68756,4.42371 5.6025989,4.5307973 76.836993 0 0 0.43811,-0.13196 c 0.74268,6.24695 3.78537,13.00002 5.51895,19.04423 3.38604,12.82723 -8.78931,14.73905 -4.53067,19.66107 1.76551,2.04052 6.4553,2.39549 11.0381,2.16453 l 3.43497,-1.98389 c -3.77377,-0.36303 -7.4885,-1.26121 -7.11831,-3.66758 0.68073,-4.42505 12.09089,-7.36618 20.358052,-17.21816 3.5626,-4.29154 6.10303,-9.54716 6.86491,-14.80546 a 4.5272563,5.9903123 18.955588 0 0 4.83918,-2.78197 4.5272563,5.9903123 18.955588 0 0 -0.44664,-8.041018 4.5272563,5.9903123 18.955588 0 0 -5.48235,0.545791 c -0.71126,-1.751685 -1.72325,-3.419125 -3.07226,-4.956384 -7.031112,-8.012324 -18.463402,-8.796579 -26.447502,-1.814405 -0.51824,0.453207 -1.01578,0.934617 -1.49084,1.442271 -0.94352,1.008269 -1.67389,2.076631 -2.2244,3.194051 a 5.6025989,4.5307973 76.836993 0 0 -4.23087,-1.562013 5.6025989,4.5307973 76.836993 0 0 -1.00374,0.396734 z m 13.67184,2.467287 c 0.33015,-0.189526 0.66651,-0.267379 0.99269,-0.229688 1.52358,0.176032 2.47307,2.788932 2.12069,5.836062 -0.35237,3.04705 -1.87308,5.37436 -3.3966,5.19813 -1.52351,-0.17613 -2.47289,-2.78913 -2.12054,-5.83615 0.24002,-2.07551 1.04204,-3.90793 2.07575,-4.742444 l 1.7e-4,-9.5e-5 c 0.10831,-0.08739 0.21769,-0.162799 0.32783,-0.225833 z m 13.69685,-0.803574 c 0.33018,-0.189545 0.6664,-0.267533 0.9926,-0.229843 1.52349,0.176235 2.4729,2.789172 2.12054,5.836151 -0.35234,3.04702 -1.87298,5.37441 -3.39651,5.1983 -1.52348,-0.17624 -2.4729,-2.78917 -2.12054,-5.83617 0.24,-2.075497 1.04211,-3.908161 2.07582,-4.742688 0.10834,-0.0874 0.21795,-0.162716 0.32809,-0.22575 z m -7.40627,13.845038 c 0.21745,-0.12213 0.43816,-0.20399 0.65981,-0.24312 1.97501,-0.34891 3.55829,2.67343 3.53636,6.75042 -0.0219,4.077 -1.64072,7.66488 -3.61571,8.01383 -1.975,0.34891 -3.55819,-2.67326 -3.53627,-6.75027 0.0164,-3.04468 0.93655,-5.93703 2.31716,-7.28321 0.20748,-0.20228 0.42129,-0.36535 0.63865,-0.48765 z m 17.352832,21.22847 c -0.10315,0.0588 -0.20272,0.11824 -0.29832,0.17807 -1.05105,0.65671 -1.69275,1.90208 -1.75,3.22056 l 1.74663,-1.00878 c 0.10074,-0.33823 0.21509,-0.61452 0.34506,-0.77008 0.0713,-0.0853 0.2166,-0.18885 0.42393,-0.30733 0.72661,-0.41504 2.22115,-1.01463 3.95458,-1.65068 l 5.84666,-3.37678 c -3.78613,1.29335 -7.9478,2.39117 -10.26846,3.71516 z" />
 | 
			
		||||
    <rect
 | 
			
		||||
       style="opacity:1;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.33699;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
			
		||||
       id="rect942"
 | 
			
		||||
       width="76.028526"
 | 
			
		||||
       height="73.388649"
 | 
			
		||||
       x="97.497429"
 | 
			
		||||
       y="66.694847"
 | 
			
		||||
       ry="3.5350549" />
 | 
			
		||||
    <rect
 | 
			
		||||
       style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.391977;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
			
		||||
       id="rect944"
 | 
			
		||||
       width="69.4505"
 | 
			
		||||
       height="56.151112"
 | 
			
		||||
       x="100.86639"
 | 
			
		||||
       y="70.51017"
 | 
			
		||||
       ry="0" />
 | 
			
		||||
    <path
 | 
			
		||||
       id="path10-3"
 | 
			
		||||
       style="fill:#1976d2;fill-opacity:1;stroke-width:0.0683297"
 | 
			
		||||
       d="M 132.97782,70.510038 A 34.92124,34.92124 0 0 0 100.8663,102.61974 v 5.41128 a 34.92124,34.92124 0 0 0 7.17007,18.63021 h 55.29238 a 34.92124,34.92124 0 0 0 6.98797,-16.86711 v -8.93604 A 34.92124,34.92124 0 0 0 138.38694,70.510038 Z" />
 | 
			
		||||
    <path
 | 
			
		||||
       id="path897-6"
 | 
			
		||||
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#1976d2;stroke-width:0.165241;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
			
		||||
       d="m 130.9868,74.267588 a 4.5307973,5.6025989 16.845913 0 0 -4.89247,4.20761 4.5307973,5.6025989 16.845913 0 0 2.71268,6.675244 4.5307973,5.6025989 16.845913 0 0 0.44538,0.10485 c -2.4812,5.78097 -3.22383,13.15053 -4.74557,19.251518 -3.4832,12.80118 -14.98258,8.3674 -13.7565,14.75951 0.5083,2.64997 4.39189,5.30288 8.47586,7.39491 h 3.96671 c -3.08632,-2.20176 -5.85386,-4.8374 -4.32979,-6.73605 2.80259,-3.4914 14.15415,-0.33165 26.2404,-4.72825 5.23137,-1.93447 10.05977,-5.215 13.34938,-9.38737 a 4.5272563,5.9903123 48.964508 0 0 5.58183,0.0112 4.5272563,5.9903123 48.964508 0 0 3.63483,-7.186478 4.5272563,5.9903123 48.964508 0 0 -5.0204,-2.26929 c 0.26017,-1.87259 0.21778,-3.82264 -0.18155,-5.82851 -2.08133,-10.454754 -11.58886,-16.851564 -21.9947,-14.798494 -0.67543,0.13326 -1.34705,0.3013 -2.01232,0.50331 -1.32131,0.40122 -2.4881,0.96108 -3.52367,1.65338 a 4.5307973,5.6025989 16.845913 0 0 -2.8825,-3.46863 4.5307973,5.6025989 16.845913 0 0 -1.0676,-0.15845 z m 10.60512,8.974304 c 0.38068,10e-4 0.71089,0.10181 0.97449,0.29758 1.2313,0.91443 0.74671,3.65194 -1.08241,6.11436 -1.82906,2.46235 -4.30989,3.71712 -5.54104,2.80255 -1.23119,-0.91448 -0.74644,-3.65202 1.08259,-6.11436 1.24587,-1.67724 2.85684,-2.8629 4.16936,-3.06855 h 1.8e-4 c 0.1375,-0.0215 0.26994,-0.0321 0.39683,-0.0316 z m 12.26265,6.15442 c 0.38072,0.001 0.71088,0.10162 0.97449,0.2974 1.23112,0.91456 0.74644,3.65206 -1.08258,6.11436 -1.82902,2.46234 -4.30984,3.71721 -5.54104,2.80274 -1.23112,-0.91456 -0.74645,-3.65206 1.08258,-6.11437 1.24586,-1.67724 2.85702,-2.86307 4.16954,-3.06873 0.13752,-0.0215 0.27011,-0.0319 0.39701,-0.0314 z m -13.33783,8.28494 c 0.24939,0.003 0.48145,0.0425 0.69297,0.11947 1.88474,0.68563 1.74421,4.094668 -0.31382,7.614168 -2.05805,3.51949 -5.25426,5.8168 -7.13902,5.13121 -1.88474,-0.68563 -1.74422,-4.09448 0.31382,-7.61399 1.53693,-2.62835 3.78032,-4.672758 5.64914,-5.147988 0.28082,-0.0714 0.54752,-0.10567 0.79691,-0.10287 z m 4.40955,27.061498 c -0.11872,-7e-4 -0.23468,0.001 -0.34739,0.005 -1.2386,0.043 -2.41713,0.80049 -3.12612,1.9136 h 2.01701 c 0.2564,-0.24251 0.4936,-0.42457 0.68395,-0.49428 0.10436,-0.0382 0.28201,-0.0552 0.52081,-0.0541 0.83678,0.004 2.43085,0.23225 4.25002,0.54842 h 6.75175 c -3.92545,-0.7736 -8.07829,-1.90434 -10.75003,-1.91848 z" />
 | 
			
		||||
    <rect
 | 
			
		||||
       style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.33699;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
			
		||||
       id="rect942-6"
 | 
			
		||||
       width="76.028526"
 | 
			
		||||
       height="73.388649"
 | 
			
		||||
       x="183.4511"
 | 
			
		||||
       y="-21.159952"
 | 
			
		||||
       ry="3.5350549"
 | 
			
		||||
       transform="rotate(27.418518)" />
 | 
			
		||||
    <rect
 | 
			
		||||
       style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.391977;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
			
		||||
       id="rect944-6"
 | 
			
		||||
       width="69.4505"
 | 
			
		||||
       height="56.151112"
 | 
			
		||||
       x="186.82002"
 | 
			
		||||
       y="-17.344629"
 | 
			
		||||
       ry="0"
 | 
			
		||||
       transform="rotate(27.418518)" />
 | 
			
		||||
    <path
 | 
			
		||||
       id="path10-3-9"
 | 
			
		||||
       style="fill:#1976d2;fill-opacity:1;stroke-width:0.0683297"
 | 
			
		||||
       d="m 202.32517,85.418659 a 34.92124,34.92124 0 0 0 -43.2904,13.715795 l -2.49182,4.803416 a 34.92124,34.92124 0 0 0 -2.21435,19.83912 l 49.0812,25.46141 a 34.92124,34.92124 0 0 0 13.97007,-11.7545 l 4.11493,-7.93223 A 34.92124,34.92124 0 0 0 207.12667,87.90949 Z" />
 | 
			
		||||
    <path
 | 
			
		||||
       id="path897-6-3"
 | 
			
		||||
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#1976d2;stroke-width:0.165241;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
 | 
			
		||||
       d="m 198.82751,87.837273 a 4.5307973,5.6025989 44.264431 0 0 -6.28043,1.482039 4.5307973,5.6025989 44.264431 0 0 -0.6659,7.174546 4.5307973,5.6025989 44.264431 0 0 0.34706,0.298162 c -4.86454,3.98902 -8.91733,10.18876 -13.07755,14.90366 -8.9867,9.75921 -17.15262,0.52818 -19.00775,6.76684 -0.76908,2.58635 1.45663,6.72959 4.11849,10.46723 l 3.52111,1.82662 c -1.72575,-3.37564 -2.96872,-6.98962 -0.74155,-7.97318 4.09551,-1.80864 12.71689,6.22341 25.47003,7.88625 5.5345,0.69181 11.33116,0.003 16.17256,-2.18564 a 4.5272563,5.9903123 76.383026 0 0 4.94964,2.5803 4.5272563,5.9903123 76.383026 0 0 6.5358,-4.70541 4.5272563,5.9903123 76.383026 0 0 -3.41147,-4.3262 c 1.09325,-1.54243 1.95359,-3.29295 2.5228,-5.25738 2.96674,-10.23876 -2.52713,-20.295083 -12.70946,-23.264393 -0.66092,-0.192737 -1.33447,-0.352844 -2.01803,-0.479873 -1.35764,-0.252295 -2.65117,-0.292618 -3.88921,-0.154954 a 4.5307973,5.6025989 44.264431 0 0 -0.96143,-4.406339 4.5307973,5.6025989 44.264431 0 0 -0.87472,-0.632269 z m 5.28126,12.849707 c 0.33746,0.17619 0.58416,0.41773 0.72799,0.71289 0.67191,1.37871 -1.01884,3.58556 -3.7764,4.92908 -2.75748,1.34349 -5.53743,1.31492 -6.20913,-0.0638 -0.67178,-1.3787 1.01911,-3.58551 3.77656,-4.929 1.87826,-0.91512 3.85425,-1.22576 5.11402,-0.80391 l 1.7e-4,8e-5 c 0.13196,0.0442 0.25439,0.0958 0.3668,0.15469 z m 8.05112,11.10986 c 0.33749,0.17621 0.58422,0.41755 0.72807,0.71273 0.67168,1.37874 -1.01913,3.58554 -3.77655,4.929 -2.75744,1.3435 -5.53743,1.31502 -6.20922,-0.0637 -0.67169,-1.37874 1.01912,-3.58554 3.77655,-4.92901 1.87826,-0.91512 3.8545,-1.22583 5.11428,-0.80399 0.13197,0.0442 0.25445,0.0961 0.36687,0.15495 z m -15.65465,1.21237 c 0.21999,0.1175 0.4078,0.25943 0.56011,0.42515 1.3573,1.47651 -0.33727,4.43789 -3.7848,6.61434 -3.44753,2.17643 -7.34258,2.74386 -8.69992,1.26738 -1.3573,-1.47651 0.33717,-4.43772 3.78471,-6.61418 2.57461,-1.62536 5.50741,-2.40706 7.38513,-1.96834 0.28216,0.0659 0.53468,0.15833 0.75477,0.27565 z m -8.54725,26.05213 c -0.10506,-0.0553 -0.20878,-0.10718 -0.31067,-0.15553 -1.11927,-0.53219 -2.51422,-0.40249 -3.65614,0.2591 l 1.79043,0.92881 c 0.33927,-0.0972 0.63366,-0.14958 0.83473,-0.12381 0.11023,0.0142 0.27575,0.0809 0.48722,0.19181 0.74094,0.38887 2.05083,1.32553 3.52006,2.44389 l 5.9933,3.10909 c -3.12826,-2.49432 -6.2939,-5.41036 -8.65901,-6.65322 z" />
 | 
			
		||||
    <text
 | 
			
		||||
       xml:space="preserve"
 | 
			
		||||
       id="text1098"
 | 
			
		||||
       style="font-style:normal;font-weight:normal;font-size:22.57779999999999987px;line-height:1.35;font-family:sans-serif;white-space:pre;shape-inside:url(#rect1100);fill:#000000;fill-opacity:1;stroke:none;"
 | 
			
		||||
       x="52.690414"
 | 
			
		||||
       y="0"
 | 
			
		||||
       transform="translate(380.03788,147.12437)"><tspan
 | 
			
		||||
         x="-284.65002"
 | 
			
		||||
         y="47.162986"><tspan
 | 
			
		||||
           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:22.5778px;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Bold Condensed';text-align:center;text-anchor:middle">Kein Bild </tspan></tspan><tspan
 | 
			
		||||
         x="-292.36704"
 | 
			
		||||
         y="78.223231"><tspan
 | 
			
		||||
           style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:22.5778px;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Bold Condensed';text-align:center;text-anchor:middle">vorhanden</tspan></tspan></text>
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 18 KiB  | 
| 
		 After Width: | Height: | Size: 3.4 KiB  | 
							
								
								
									
										227
									
								
								quasar.conf.js
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -1,227 +0,0 @@
 | 
			
		|||
/*
 | 
			
		||||
 * This file runs in a Node context (it's NOT transpiled by Babel), so use only
 | 
			
		||||
 * the ES6 features that are supported by your Node version. https://node.green/
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// Configuration for your app
 | 
			
		||||
// https://quasar.dev/quasar-cli/quasar-conf-js
 | 
			
		||||
 | 
			
		||||
/* eslint-env node */
 | 
			
		||||
/* eslint-disable @typescript-eslint/no-var-requires */
 | 
			
		||||
const ESLintPlugin = require('eslint-webpack-plugin');
 | 
			
		||||
const { ModifySourcePlugin, ReplaceOperation } = require('modify-source-webpack-plugin');
 | 
			
		||||
const { configure } = require('quasar/wrappers');
 | 
			
		||||
 | 
			
		||||
const operation = () => {
 | 
			
		||||
  const custom_plgns = require('./plugin.config.js');
 | 
			
		||||
  const required_plgns = require('./src/vendor-plugin.config.js');
 | 
			
		||||
  const plugins = [...custom_plgns, ...required_plgns].map(
 | 
			
		||||
    (v) => `import("${v}").catch(() => "${v}")`
 | 
			
		||||
  );
 | 
			
		||||
  const replace = new ReplaceOperation(
 | 
			
		||||
    'all',
 | 
			
		||||
    `\\/\\* *INSERT_PLUGIN_LIST *\\*\\/`,
 | 
			
		||||
    `${plugins.join(', ')}`
 | 
			
		||||
  );
 | 
			
		||||
  return replace;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = configure(function (/* ctx */) {
 | 
			
		||||
  return {
 | 
			
		||||
    // https://quasar.dev/quasar-cli/supporting-ts
 | 
			
		||||
    supportTS: {
 | 
			
		||||
      tsCheckerConfig: {
 | 
			
		||||
        eslint: {
 | 
			
		||||
          enabled: true,
 | 
			
		||||
          files: './src/**/*.{ts,tsx,js,jsx,vue}',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // https://quasar.dev/quasar-cli/prefetch-feature
 | 
			
		||||
    // preFetch: true,
 | 
			
		||||
 | 
			
		||||
    // app boot file (/src/boot)
 | 
			
		||||
    // --> boot files are part of "main.js"
 | 
			
		||||
    // https://quasar.dev/quasar-cli/boot-files
 | 
			
		||||
    boot: ['axios', 'store', 'plugins', 'login', 'init'],
 | 
			
		||||
 | 
			
		||||
    // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css
 | 
			
		||||
    css: ['app.scss'],
 | 
			
		||||
 | 
			
		||||
    // https://github.com/quasarframework/quasar/tree/dev/extras
 | 
			
		||||
    extras: [
 | 
			
		||||
      // 'eva-icons',
 | 
			
		||||
      // 'fontawesome-v5',
 | 
			
		||||
      // 'ionicons-v5',
 | 
			
		||||
      // 'line-awesome',
 | 
			
		||||
      // 'material-icons',
 | 
			
		||||
      'mdi-v7',
 | 
			
		||||
      // 'themify',
 | 
			
		||||
 | 
			
		||||
      // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
 | 
			
		||||
      'roboto-font', // optional, you are not bound to it
 | 
			
		||||
    ],
 | 
			
		||||
 | 
			
		||||
    // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build
 | 
			
		||||
    build: {
 | 
			
		||||
      vueRouterMode: 'history', // available values: 'hash', 'history'
 | 
			
		||||
      //publicPath: 'flaschengeist2',
 | 
			
		||||
      // transpile: false,
 | 
			
		||||
 | 
			
		||||
      // Add dependencies for transpiling with Babel (Array of string/regex)
 | 
			
		||||
      // (from node_modules, which are by default not transpiled).
 | 
			
		||||
      // Applies only if "transpile" is set to true.
 | 
			
		||||
      // transpileDependencies: [],
 | 
			
		||||
 | 
			
		||||
      // rtl: false,
 | 
			
		||||
 | 
			
		||||
      // analyze: true,
 | 
			
		||||
 | 
			
		||||
      // Options below are automatically set depending on the env, set them if you want to override
 | 
			
		||||
      // extractCSS: false,
 | 
			
		||||
 | 
			
		||||
      // https://quasar.dev/quasar-cli/handling-webpack
 | 
			
		||||
      // "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
 | 
			
		||||
      chainWebpack(chain) {
 | 
			
		||||
        chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [
 | 
			
		||||
          {
 | 
			
		||||
            extensions: ['ts', 'js', 'vue'],
 | 
			
		||||
            exclude: ['node_modules', 'src-capacitor'],
 | 
			
		||||
          },
 | 
			
		||||
        ]);
 | 
			
		||||
        chain.plugin('modify-source-webpack-plugin').use(ModifySourcePlugin, [
 | 
			
		||||
          {
 | 
			
		||||
            rules: [
 | 
			
		||||
              {
 | 
			
		||||
                test: /plugins\.ts$/,
 | 
			
		||||
                operations: [operation()],
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ]);
 | 
			
		||||
        chain.merge({
 | 
			
		||||
          snapshot: {
 | 
			
		||||
            managedPaths: [],
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer
 | 
			
		||||
    devServer: {
 | 
			
		||||
      https: false,
 | 
			
		||||
      port: 8080,
 | 
			
		||||
      open: false, // opens browser window automatically
 | 
			
		||||
      watchFiles: { paths: ['/node_modules/@flaschengeist/**/*'] },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
 | 
			
		||||
    framework: {
 | 
			
		||||
      iconSet: 'mdi-v6', // Quasar icon set
 | 
			
		||||
      lang: 'de', // Quasar language pack
 | 
			
		||||
      config: {
 | 
			
		||||
        dark: 'auto',
 | 
			
		||||
        loadingBar: {
 | 
			
		||||
          position: 'top',
 | 
			
		||||
          color: 'warning',
 | 
			
		||||
          size: '5px',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      // For special cases outside of where the auto-import stategy can have an impact
 | 
			
		||||
      // (like functional components as one of the examples),
 | 
			
		||||
      // you can manually specify Quasar components/directives to be available everywhere:
 | 
			
		||||
      //
 | 
			
		||||
      // components: [],
 | 
			
		||||
      // directives: [],
 | 
			
		||||
 | 
			
		||||
      // Quasar plugins
 | 
			
		||||
      plugins: ['LocalStorage', 'SessionStorage', 'Dialog', 'Loading', 'Notify', 'LoadingBar'],
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // animations: 'all', // --- includes all animations
 | 
			
		||||
    // https://quasar.dev/options/animations
 | 
			
		||||
    animations: [],
 | 
			
		||||
 | 
			
		||||
    // https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr
 | 
			
		||||
    ssr: {
 | 
			
		||||
      pwa: false,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa
 | 
			
		||||
    pwa: {
 | 
			
		||||
      workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
 | 
			
		||||
      workboxOptions: {}, // only for GenerateSW
 | 
			
		||||
      manifest: {
 | 
			
		||||
        name: 'Flaschengeist',
 | 
			
		||||
        short_name: 'Flaschengeist',
 | 
			
		||||
        description: 'Modular student club administration system',
 | 
			
		||||
        display: 'standalone',
 | 
			
		||||
        orientation: 'portrait',
 | 
			
		||||
        background_color: '#ffffff',
 | 
			
		||||
        theme_color: '#027be3',
 | 
			
		||||
        icons: [
 | 
			
		||||
          {
 | 
			
		||||
            src: 'flaschengeist-logo.svg',
 | 
			
		||||
            sizes: 'any',
 | 
			
		||||
            type: 'image/svg+xml',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            src: 'favicon-128x128.png',
 | 
			
		||||
            sizes: '128x128',
 | 
			
		||||
            type: 'image/png',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            src: 'favicon-256x256.png',
 | 
			
		||||
            sizes: '256x256',
 | 
			
		||||
            type: 'image/png',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
 | 
			
		||||
    cordova: {
 | 
			
		||||
      // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
 | 
			
		||||
    capacitor: {
 | 
			
		||||
      hideSplashscreen: true,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
 | 
			
		||||
    electron: {
 | 
			
		||||
      bundler: 'packager', // 'packager' or 'builder'
 | 
			
		||||
 | 
			
		||||
      packager: {
 | 
			
		||||
        // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
 | 
			
		||||
        // OS X / Mac App Store
 | 
			
		||||
        // appBundleId: '',
 | 
			
		||||
        // appCategoryType: '',
 | 
			
		||||
        // osxSign: '',
 | 
			
		||||
        // protocol: 'myapp://path',
 | 
			
		||||
        // Windows only
 | 
			
		||||
        // win32metadata: { ... }
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      builder: {
 | 
			
		||||
        // https://www.electron.build/configuration/configuration
 | 
			
		||||
 | 
			
		||||
        appId: 'flaschengeist-frontend',
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      // More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration
 | 
			
		||||
      nodeIntegration: true,
 | 
			
		||||
 | 
			
		||||
      extendWebpack(/* cfg */) {
 | 
			
		||||
        // do something with Electron main process Webpack cfg
 | 
			
		||||
        // chainWebpack also available besides this extendWebpack
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    bin: {
 | 
			
		||||
      linuxAndroidStudio: '/home/crimsen/.local/share/JetBrains/Toolbox/scripts/studio',
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +0,0 @@
 | 
			
		|||
/* eslint-disable */
 | 
			
		||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
 | 
			
		||||
//  REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
 | 
			
		||||
import 'quasar/dist/types/feature-flag';
 | 
			
		||||
 | 
			
		||||
declare module 'quasar/dist/types/feature-flag' {
 | 
			
		||||
  interface QuasarFeatureFlags {
 | 
			
		||||
    capacitor: true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
  "appId": "dev.flaschengeist",
 | 
			
		||||
  "appName": "flaschengeist-frontend",
 | 
			
		||||
  "bundledWebRuntime": false,
 | 
			
		||||
  "npmClient": "yarn",
 | 
			
		||||
  "webDir": "www",
 | 
			
		||||
  "android": {
 | 
			
		||||
    "minWebViewVersion": 71
 | 
			
		||||
  },
 | 
			
		||||
  "ios": {
 | 
			
		||||
    "allowsLinkPreview": false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,35 +0,0 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
  <head>
 | 
			
		||||
    <title>Quasar</title>
 | 
			
		||||
 | 
			
		||||
    <meta charset="utf-8">
 | 
			
		||||
    <meta name="description" content="Quasar Capacitor App">
 | 
			
		||||
    <meta name="format-detection" content="telephone=no">
 | 
			
		||||
    <meta name="msapplication-tap-highlight" content="no">
 | 
			
		||||
    <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, viewport-fit=cover">
 | 
			
		||||
 | 
			
		||||
    <style>
 | 
			
		||||
      .page {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
        height: 100vh;
 | 
			
		||||
        text-align: center;
 | 
			
		||||
      }
 | 
			
		||||
    </style>
 | 
			
		||||
  </head>
 | 
			
		||||
 | 
			
		||||
  <body>
 | 
			
		||||
    <div class="page">
 | 
			
		||||
      <div>
 | 
			
		||||
        This file will be auto-generated. Do not edit.
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div>
 | 
			
		||||
        Run "quasar dev" or "quasar build" with Capacitor mode.
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "flaschengeist",
 | 
			
		||||
  "version": "2.0.0",
 | 
			
		||||
  "description": "Modular student club administration system",
 | 
			
		||||
  "author": "Tim Gröger <flaschengeist@wu5.de>",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@capacitor/android": "^5.0.0-beta.0",
 | 
			
		||||
    "@capacitor/app": "^5.0.0",
 | 
			
		||||
    "@capacitor/cli": "^5.0.0",
 | 
			
		||||
    "@capacitor/core": "^5.0.0",
 | 
			
		||||
    "@capacitor/ios": "^5.0.0",
 | 
			
		||||
    "@capacitor/preferences": "^5.0.0",
 | 
			
		||||
    "@capacitor/splash-screen": "^5.0.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +0,0 @@
 | 
			
		|||
/* eslint-disable */
 | 
			
		||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
 | 
			
		||||
//  REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
 | 
			
		||||
import 'quasar/dist/types/feature-flag';
 | 
			
		||||
 | 
			
		||||
declare module 'quasar/dist/types/feature-flag' {
 | 
			
		||||
  interface QuasarFeatureFlags {
 | 
			
		||||
    electron: true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,56 +0,0 @@
 | 
			
		|||
import { app, BrowserWindow, nativeTheme } from 'electron'
 | 
			
		||||
import path from 'path'
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
  if (process.platform === 'win32' && nativeTheme.shouldUseDarkColors === true) {
 | 
			
		||||
    require('fs').unlinkSync(require('path').join(app.getPath('userData'), 'DevTools Extensions'))
 | 
			
		||||
  }
 | 
			
		||||
} catch (_) { }
 | 
			
		||||
 | 
			
		||||
let mainWindow
 | 
			
		||||
 | 
			
		||||
function createWindow () {
 | 
			
		||||
  /**
 | 
			
		||||
   * Initial window options
 | 
			
		||||
   */
 | 
			
		||||
  mainWindow = new BrowserWindow({
 | 
			
		||||
    width: 1000,
 | 
			
		||||
    height: 600,
 | 
			
		||||
    useContentSize: true,
 | 
			
		||||
    webPreferences: {
 | 
			
		||||
      contextIsolation: true,
 | 
			
		||||
      // More info: /quasar-cli/developing-electron-apps/electron-preload-script
 | 
			
		||||
      preload: path.resolve(__dirname, process.env.QUASAR_ELECTRON_PRELOAD)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  mainWindow.loadURL(process.env.APP_URL)
 | 
			
		||||
 | 
			
		||||
  if (process.env.DEBUGGING) {
 | 
			
		||||
    // if on DEV or Production with debug enabled
 | 
			
		||||
    mainWindow.webContents.openDevTools()
 | 
			
		||||
  } else {
 | 
			
		||||
    // we're on production; no access to devtools pls
 | 
			
		||||
    mainWindow.webContents.on('devtools-opened', () => {
 | 
			
		||||
      mainWindow.webContents.closeDevTools()
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  mainWindow.on('closed', () => {
 | 
			
		||||
    mainWindow = null
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
app.on('ready', createWindow)
 | 
			
		||||
 | 
			
		||||
app.on('window-all-closed', () => {
 | 
			
		||||
  if (process.platform !== 'darwin') {
 | 
			
		||||
    app.quit()
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
app.on('activate', () => {
 | 
			
		||||
  if (mainWindow === null) {
 | 
			
		||||
    createWindow()
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -1,17 +0,0 @@
 | 
			
		|||
/**
 | 
			
		||||
 * This file is used specifically for security reasons.
 | 
			
		||||
 * Here you can access Nodejs stuff and inject functionality into
 | 
			
		||||
 * the renderer thread (accessible there through the "window" object)
 | 
			
		||||
 *
 | 
			
		||||
 * WARNING!
 | 
			
		||||
 * If you import anything from node_modules, then make sure that the package is specified
 | 
			
		||||
 * in package.json > dependencies and NOT in devDependencies
 | 
			
		||||
 *
 | 
			
		||||
 * Example (injects window.myAPI.doAThing() into renderer thread):
 | 
			
		||||
 *
 | 
			
		||||
 *   const { contextBridge } = require('electron')
 | 
			
		||||
 *
 | 
			
		||||
 *   contextBridge.exposeInMainWorld('myAPI', {
 | 
			
		||||
 *     doAThing: () => {}
 | 
			
		||||
 *   })
 | 
			
		||||
 */
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 19 KiB  | 
| 
		 Before Width: | Height: | Size: 8.7 KiB  | 
							
								
								
									
										232
									
								
								src/App.vue
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -1,10 +1,232 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <router-view />
 | 
			
		||||
  <v-app>
 | 
			
		||||
    <TitleBar />
 | 
			
		||||
    <router-view />
 | 
			
		||||
    <v-footer app>
 | 
			
		||||
      <span class="px-4 d-none d-sm-flex"
 | 
			
		||||
        >© {{ new Date().getFullYear() }}
 | 
			
		||||
        <v-btn x-small text class="text-none subtitle-1" href="https://wu5.de">
 | 
			
		||||
          Studentenclub Wu5 e.V.
 | 
			
		||||
        </v-btn>
 | 
			
		||||
      </span>
 | 
			
		||||
      <span>
 | 
			
		||||
        <v-btn text x-small href="https://wu5.de/impressum">
 | 
			
		||||
          Impressum
 | 
			
		||||
        </v-btn>
 | 
			
		||||
        <v-btn text x-small href="https://wu5.de/datenschutz">
 | 
			
		||||
          Datenschutzerklärung
 | 
			
		||||
        </v-btn>
 | 
			
		||||
        <v-btn
 | 
			
		||||
          text
 | 
			
		||||
          x-small
 | 
			
		||||
          v-if="isLoggedIn"
 | 
			
		||||
          href="https://groeger-clan.duckdns.org/redmine/projects/geruecht/issues/new"
 | 
			
		||||
        >
 | 
			
		||||
          Bugs?
 | 
			
		||||
        </v-btn>
 | 
			
		||||
      </span>
 | 
			
		||||
      <v-spacer />
 | 
			
		||||
      <div v-if="isLoggedIn && !change" :key="render">
 | 
			
		||||
        <v-hover
 | 
			
		||||
          v-slot:default="{ hover }"
 | 
			
		||||
          open-delay="200"
 | 
			
		||||
          close-delay="200"
 | 
			
		||||
          class="d-none d-sm-flex"
 | 
			
		||||
        >
 | 
			
		||||
          <v-sheet
 | 
			
		||||
            :elevation="hover ? 16 : 0"
 | 
			
		||||
            color="#f5f5f5"
 | 
			
		||||
            @click="change = !change"
 | 
			
		||||
            >{{ calcTime }}</v-sheet
 | 
			
		||||
          >
 | 
			
		||||
        </v-hover>
 | 
			
		||||
        <v-hover
 | 
			
		||||
          v-slot:default="{ hover }"
 | 
			
		||||
          open-delay="200"
 | 
			
		||||
          close-delay="200"
 | 
			
		||||
          class="d-flex d-sm-none"
 | 
			
		||||
        >
 | 
			
		||||
          <v-sheet
 | 
			
		||||
            :elevation="hover ? 16 : 0"
 | 
			
		||||
            color="#f5f5f5"
 | 
			
		||||
            @click="change = !change"
 | 
			
		||||
            >{{ calcTimeLittle }}</v-sheet
 | 
			
		||||
          >
 | 
			
		||||
        </v-hover>
 | 
			
		||||
      </div>
 | 
			
		||||
      <v-dialog v-model="change" max-width="300">
 | 
			
		||||
        <v-card>
 | 
			
		||||
          <v-card-title>
 | 
			
		||||
            Zeit bis zum Logout ändern
 | 
			
		||||
          </v-card-title>
 | 
			
		||||
          <v-card-text>
 | 
			
		||||
            <v-combobox
 | 
			
		||||
              solo
 | 
			
		||||
              :items="lifeTimes"
 | 
			
		||||
              item-text="text"
 | 
			
		||||
              item-value="value"
 | 
			
		||||
              v-model="selectLifetime"
 | 
			
		||||
              return-object
 | 
			
		||||
            />
 | 
			
		||||
          </v-card-text>
 | 
			
		||||
          <v-card-actions>
 | 
			
		||||
            <v-spacer />
 | 
			
		||||
            <v-btn text @click="change = false">Abbrechen</v-btn>
 | 
			
		||||
            <v-btn color="primary" text @click="save()">Speichern</v-btn>
 | 
			
		||||
          </v-card-actions>
 | 
			
		||||
        </v-card>
 | 
			
		||||
      </v-dialog>
 | 
			
		||||
    </v-footer>
 | 
			
		||||
  </v-app>
 | 
			
		||||
</template>
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
<script>
 | 
			
		||||
import TitleBar from './components/TitleBar'
 | 
			
		||||
import { mapGetters, mapActions } from 'vuex'
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'App',
 | 
			
		||||
});
 | 
			
		||||
  components: { TitleBar },
 | 
			
		||||
  data: () => ({
 | 
			
		||||
    render: 0,
 | 
			
		||||
    timer: null,
 | 
			
		||||
    change: false,
 | 
			
		||||
    selectLifetime: { text: '30 Minuten', value: 1800 },
 | 
			
		||||
    lifeTimes: [
 | 
			
		||||
      {
 | 
			
		||||
        text: '5 Minuten',
 | 
			
		||||
        value: 300
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '10 Minuten',
 | 
			
		||||
        value: 600
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '15 Minuten',
 | 
			
		||||
        value: 900
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '30 Minuten',
 | 
			
		||||
        value: 1800
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '1 Stunde',
 | 
			
		||||
        value: 3600
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '2 Stunden',
 | 
			
		||||
        value: 7200
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '3 Stunden',
 | 
			
		||||
        value: 10800
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '1 Tag',
 | 
			
		||||
        value: 86400
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '2 Tage',
 | 
			
		||||
        value: 172800
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '1 Woche',
 | 
			
		||||
        value: 604800
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '1 Monat',
 | 
			
		||||
        value: 2678400
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }),
 | 
			
		||||
  created() {
 | 
			
		||||
    if (this.isLoggedIn) {
 | 
			
		||||
      this.getLifetime()
 | 
			
		||||
    }
 | 
			
		||||
    this.timer = setInterval(this.test, 1000)
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions(['setLifetime', 'saveLifetime', 'logout', 'getLifetime']),
 | 
			
		||||
    test() {
 | 
			
		||||
      if (this.isLoggedIn) {
 | 
			
		||||
        if (this.lifeTime == 0) this.logout()
 | 
			
		||||
        this.setLifetime(this.lifeTime - 1)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    save() {
 | 
			
		||||
      this.saveLifetime(this.selectLifetime.value)
 | 
			
		||||
      this.change = false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters(['isLoggedIn', 'lifeTime', 'getMinute', 'getSeconds']),
 | 
			
		||||
    calcTime() {
 | 
			
		||||
      var minutes = this.lifeTime / 60
 | 
			
		||||
      var seconds = this.lifeTime % 60
 | 
			
		||||
      var minutesString =
 | 
			
		||||
        minutes < 10 ? '0' + Math.floor(minutes % 60) : Math.floor(minutes % 60)
 | 
			
		||||
      var secondsString =
 | 
			
		||||
        seconds < 10 ? '0' + Math.floor(seconds) : Math.floor(seconds)
 | 
			
		||||
      if (minutes > 60) {
 | 
			
		||||
        var hours = minutes / 60
 | 
			
		||||
        if (hours > 24) {
 | 
			
		||||
          var days = hours / 24
 | 
			
		||||
          var now = new Date()
 | 
			
		||||
          var dayMonth = new Date(
 | 
			
		||||
            now.getFullYear(),
 | 
			
		||||
            now.getMonth() + 1,
 | 
			
		||||
            0
 | 
			
		||||
          ).getDate()
 | 
			
		||||
          if (days >= dayMonth) {
 | 
			
		||||
            return Math.floor(days / dayMonth) + ' Monate bis zum Logout'
 | 
			
		||||
          } else {
 | 
			
		||||
            return Math.floor(days) + ' Tage bis zum Logout'
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          return (
 | 
			
		||||
            Math.floor(hours) +
 | 
			
		||||
            ':' +
 | 
			
		||||
            minutesString +
 | 
			
		||||
            ':' +
 | 
			
		||||
            secondsString +
 | 
			
		||||
            ' Stunden bis zum Logout'
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        return minutesString + ':' + secondsString + ' Minuten bis zum Logout'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    calcTimeLittle() {
 | 
			
		||||
      var minutes = this.lifeTime / 60
 | 
			
		||||
      var seconds = this.lifeTime % 60
 | 
			
		||||
      var minutesString =
 | 
			
		||||
        minutes < 10 ? '0' + Math.floor(minutes % 60) : Math.floor(minutes % 60)
 | 
			
		||||
      var secondsString =
 | 
			
		||||
        seconds < 10 ? '0' + Math.floor(seconds) : Math.floor(seconds)
 | 
			
		||||
      if (minutes > 60) {
 | 
			
		||||
        var hours = minutes / 60
 | 
			
		||||
        if (hours > 24) {
 | 
			
		||||
          var days = hours / 24
 | 
			
		||||
          var now = new Date()
 | 
			
		||||
          var dayMonth = new Date(
 | 
			
		||||
            now.getFullYear(),
 | 
			
		||||
            now.getMonth() + 1,
 | 
			
		||||
            0
 | 
			
		||||
          ).getDate()
 | 
			
		||||
          if (days >= dayMonth) {
 | 
			
		||||
            return Math.floor(days / dayMonth) + 'M'
 | 
			
		||||
          } else {
 | 
			
		||||
            return Math.floor(days) + 'D'
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          return Math.floor(hours) + ':' + minutesString + ':' + secondsString
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        return minutesString + ':' + secondsString
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    clearInterval(this.timer)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
		 After Width: | Height: | Size: 4.4 KiB  | 
| 
		 After Width: | Height: | Size: 322 KiB  | 
| 
		 After Width: | Height: | Size: 6.7 KiB  | 
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 539 B  | 
| 
		 After Width: | Height: | Size: 1.9 KiB  | 
| 
						 | 
				
			
			@ -1,109 +0,0 @@
 | 
			
		|||
/**
 | 
			
		||||
 * This boot file registers interceptors for axios
 | 
			
		||||
 */
 | 
			
		||||
import { useMainStore, api } from '@flaschengeist/api';
 | 
			
		||||
import { AxiosError } from 'axios';
 | 
			
		||||
import { boot } from 'quasar/wrappers';
 | 
			
		||||
import config from 'src/config';
 | 
			
		||||
import { clone } from '@flaschengeist/api';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Minify data sent to backend server
 | 
			
		||||
 *
 | 
			
		||||
 * Drop unneeded entities which can be identified by ID.
 | 
			
		||||
 *
 | 
			
		||||
 * @param obj Object to minify
 | 
			
		||||
 * @param cloned If this entity is already cloned (JSON En+Decoded)
 | 
			
		||||
 * @returns Minified object (some types are converted, like a Date object is now a ISO string)
 | 
			
		||||
 */
 | 
			
		||||
function minify(entity: unknown, cloned = false) {
 | 
			
		||||
  if (!cloned) entity = clone(entity);
 | 
			
		||||
 | 
			
		||||
  if (typeof entity === 'object') {
 | 
			
		||||
    const obj = entity as { [index: string]: unknown };
 | 
			
		||||
 | 
			
		||||
    for (const prop in obj) {
 | 
			
		||||
      if (obj.hasOwnProperty(prop) && !!obj[prop]) {
 | 
			
		||||
        if (Array.isArray(obj[prop])) {
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-unsafe-return
 | 
			
		||||
          obj[prop] = (<Array<unknown>>obj[prop]).map((v) => minify(v, true));
 | 
			
		||||
        } else if (
 | 
			
		||||
          typeof obj[prop] === 'object' &&
 | 
			
		||||
          Object.keys(<object>obj[prop]).includes('id') &&
 | 
			
		||||
          typeof (<{ id: unknown }>obj[prop])['id'] === 'number' &&
 | 
			
		||||
          !isNaN((<{ id: number }>obj[prop])['id'])
 | 
			
		||||
        ) {
 | 
			
		||||
          obj[prop] = (<{ id: unknown }>obj[prop])['id'];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return obj;
 | 
			
		||||
  }
 | 
			
		||||
  return entity;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default boot(({ router }) => {
 | 
			
		||||
  // Persisted value is read in plugins.ts boot file!
 | 
			
		||||
  if (api.defaults.baseURL === undefined) api.defaults.baseURL = config.baseURL;
 | 
			
		||||
 | 
			
		||||
  /***
 | 
			
		||||
   * Intercept requests
 | 
			
		||||
   *   - insert Token if available
 | 
			
		||||
   *   - minify JSON requests
 | 
			
		||||
   */
 | 
			
		||||
  api.interceptors.request.use((config) => {
 | 
			
		||||
    const store = useMainStore();
 | 
			
		||||
    if (store.session?.token) {
 | 
			
		||||
      config.headers = Object.assign(config.headers || {}, {
 | 
			
		||||
        Authorization: `Bearer ${store.session.token}`,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    // Minify JSON requests
 | 
			
		||||
    if (
 | 
			
		||||
      !!config.data &&
 | 
			
		||||
      (config.headers === undefined ||
 | 
			
		||||
        config.headers['Content-Type'] === undefined ||
 | 
			
		||||
        config.headers['Content-Type'] === 'application/json')
 | 
			
		||||
    )
 | 
			
		||||
      config.data = minify(config.data);
 | 
			
		||||
    return config;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  /***
 | 
			
		||||
   * Intercept responses
 | 
			
		||||
   *   - filter 401 --> handleLoggedOut
 | 
			
		||||
   *   - filter timeout or 502-504 --> backendOffline
 | 
			
		||||
   */
 | 
			
		||||
  api.interceptors.response.use(
 | 
			
		||||
    (response) => response,
 | 
			
		||||
    async (error) => {
 | 
			
		||||
      const store = useMainStore();
 | 
			
		||||
 | 
			
		||||
      if (error) {
 | 
			
		||||
        const e = <AxiosError>error;
 | 
			
		||||
        const current = router.currentRoute.value;
 | 
			
		||||
        if (
 | 
			
		||||
          e.code === 'ECONNABORTED' ||
 | 
			
		||||
          (e.response && e.response.status >= 502 && e.response.status <= 504)
 | 
			
		||||
        ) {
 | 
			
		||||
          let next = current.path;
 | 
			
		||||
          if ((current.name == 'login' || current.name == 'offline') && current.query.redirect)
 | 
			
		||||
            next = <string>current.query.redirect;
 | 
			
		||||
          await router.push({
 | 
			
		||||
            name: 'offline',
 | 
			
		||||
            query: { redirect: next },
 | 
			
		||||
          });
 | 
			
		||||
        } else if (e.response && e.response.status == 401) {
 | 
			
		||||
          store.handleLoggedOut();
 | 
			
		||||
          if (current.name != 'login') {
 | 
			
		||||
            await router.push({
 | 
			
		||||
              name: 'login',
 | 
			
		||||
              query: { redirect: current.fullPath },
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,97 +0,0 @@
 | 
			
		|||
/**
 | 
			
		||||
 * This boot file initalizes the store from persistent storage and load all plugins
 | 
			
		||||
 */
 | 
			
		||||
import {
 | 
			
		||||
  PersistentStorage,
 | 
			
		||||
  api,
 | 
			
		||||
  isAxiosError,
 | 
			
		||||
  saveSession,
 | 
			
		||||
  useMainStore,
 | 
			
		||||
} from '@flaschengeist/api';
 | 
			
		||||
import { Notify, Platform } from 'quasar';
 | 
			
		||||
import { loadPlugins } from './plugins';
 | 
			
		||||
import { boot } from 'quasar/wrappers';
 | 
			
		||||
import routes from 'src/router/routes';
 | 
			
		||||
 | 
			
		||||
async function loadBaseUrl() {
 | 
			
		||||
  try {
 | 
			
		||||
    const url = await PersistentStorage.get<string>('baseURL');
 | 
			
		||||
    if (url !== null) api.defaults.baseURL = url;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.warn('Could not load BaseURL', e);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
// eslint-disable-next-line
 | 
			
		||||
class BackendError extends Error { }
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Loading backend information
 | 
			
		||||
 * @returns Backend object or null
 | 
			
		||||
 */
 | 
			
		||||
async function getBackend() {
 | 
			
		||||
  const { data } = await api.get<FG.Backend>('/');
 | 
			
		||||
  if (!data || typeof data !== 'object' || !('plugins' in data))
 | 
			
		||||
    throw new BackendError('Invalid backend response received');
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Boot file for loading baseURL + Session from PersistentStorage + loading and initializing all plugins
 | 
			
		||||
 */
 | 
			
		||||
export default boot(async ({ app, router }) => {
 | 
			
		||||
  const store = useMainStore();
 | 
			
		||||
 | 
			
		||||
  // FIRST(!) get the base URL
 | 
			
		||||
  await loadBaseUrl();
 | 
			
		||||
 | 
			
		||||
  // Init the store, load current session and user, if available
 | 
			
		||||
  try {
 | 
			
		||||
    await store.init();
 | 
			
		||||
  } finally {
 | 
			
		||||
    // Any changes on the session is written back to the persistent store
 | 
			
		||||
    store.$subscribe((mutation, state) => {
 | 
			
		||||
      saveSession(state.session);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Load all plugins
 | 
			
		||||
  try {
 | 
			
		||||
    // Fetch backend data
 | 
			
		||||
    const backend = await getBackend();
 | 
			
		||||
    // Load enabled plugins
 | 
			
		||||
    const flaschengeist = await loadPlugins(backend, routes);
 | 
			
		||||
    // Add loaded routes to router
 | 
			
		||||
    flaschengeist.routes.forEach((route) => router.addRoute(route));
 | 
			
		||||
    // save plugins in VM-variable
 | 
			
		||||
    app.provide('flaschengeist', flaschengeist);
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    // Handle errors from loading the backend information
 | 
			
		||||
    if (error instanceof BackendError || isAxiosError(error)) {
 | 
			
		||||
      router.isReady().finally(() => {
 | 
			
		||||
        // if (Platform.is.capacitor) void router.push({ name: 'setup_backend' });
 | 
			
		||||
        if (Platform.is.capacitor) {
 | 
			
		||||
          //void router.push({ name: 'setup_backend' })
 | 
			
		||||
          Notify.create({
 | 
			
		||||
            type: 'negative',
 | 
			
		||||
            message:
 | 
			
		||||
              'Backend nicht erreichbar! Prüfe deine Internetverbindung oder probiere es später nochmal.',
 | 
			
		||||
            timeout: 0,
 | 
			
		||||
            icon: 'mdi-alert-circle-outline',
 | 
			
		||||
            closeBtn: true,
 | 
			
		||||
          });
 | 
			
		||||
        } else void router.push({ name: 'offline', params: { refresh: 1 } });
 | 
			
		||||
      });
 | 
			
		||||
    } else if (typeof error === 'string') {
 | 
			
		||||
      // Handle plugin not found errors
 | 
			
		||||
      void router.push({ name: 'error' });
 | 
			
		||||
      Notify.create({
 | 
			
		||||
        type: 'negative',
 | 
			
		||||
        message: `Fehler beim Laden: Bitte wende dich an den Admin (${error})!`,
 | 
			
		||||
        timeout: 10000,
 | 
			
		||||
        progress: true,
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      console.error('Unknown error in init.ts:', error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,33 +0,0 @@
 | 
			
		|||
/**
 | 
			
		||||
 * This boot file registers login / authentification related axios interceptors
 | 
			
		||||
 */
 | 
			
		||||
import { useMainStore, hasPermissions } from '@flaschengeist/api';
 | 
			
		||||
import { boot } from 'quasar/wrappers';
 | 
			
		||||
 | 
			
		||||
export default boot(({ router }) => {
 | 
			
		||||
  /**
 | 
			
		||||
   * Login guard
 | 
			
		||||
   * Check if user tries to access the secured area and validates token
 | 
			
		||||
   */
 | 
			
		||||
  router.beforeEach((to, from) => {
 | 
			
		||||
    const store = useMainStore();
 | 
			
		||||
 | 
			
		||||
    // Skip loops
 | 
			
		||||
    if (to.name == 'login' && from.name == 'login') return false;
 | 
			
		||||
 | 
			
		||||
    // Secured area '/in/...' requires to be authenticated
 | 
			
		||||
    if (to.path.startsWith('/in') && (!store.session || store.session.expires <= new Date())) {
 | 
			
		||||
      store.handleLoggedOut();
 | 
			
		||||
      return { name: 'login' };
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Permission guard
 | 
			
		||||
   * Check permissions for route, cancel navigation on errors
 | 
			
		||||
   */
 | 
			
		||||
  router.beforeResolve((to) => {
 | 
			
		||||
    if (!!to.meta.permissions && !hasPermissions(<FG.Permission[]>to.meta.permissions))
 | 
			
		||||
      return false;
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,295 +0,0 @@
 | 
			
		|||
import { FG_Plugin } from '@flaschengeist/types';
 | 
			
		||||
import { RouteRecordRaw } from 'vue-router';
 | 
			
		||||
 | 
			
		||||
/****************************************************
 | 
			
		||||
 ******** 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'
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Here does some magic happens, WebPack will automatically replace the following comment with the import statements
 | 
			
		||||
const PLUGINS = <Array<Promise<ImportPlgn>>>[
 | 
			
		||||
  /*INSERT_PLUGIN_LIST*/
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
// Handle Notifications
 | 
			
		||||
export const translateNotification = (note: FG.Notification): FG_Plugin.Notification => note;
 | 
			
		||||
 | 
			
		||||
// Combine routes, shortcuts and widgets from plugins
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper function, set permissions from MenuRoute to meta from RouteRecordRaw
 | 
			
		||||
 * @param object MenuRoute to set route meta
 | 
			
		||||
 */
 | 
			
		||||
function setPermissions(object: FG_Plugin.MenuRoute) {
 | 
			
		||||
  if (object.permissions !== undefined) {
 | 
			
		||||
    if (object.route.meta === undefined) object.route.meta = {};
 | 
			
		||||
    object.route.meta['permissions'] = object.permissions;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper function to convert MenuRoute to the parents RouteRecordRaw
 | 
			
		||||
 * @param parent Parent RouteRecordRaw
 | 
			
		||||
 * @param children MenuRoute to convert
 | 
			
		||||
 */
 | 
			
		||||
function convertRoutes(parent: RouteRecordRaw, children?: FG_Plugin.MenuRoute[]) {
 | 
			
		||||
  if (children !== undefined) {
 | 
			
		||||
    children.forEach((child) => {
 | 
			
		||||
      setPermissions(child);
 | 
			
		||||
      convertRoutes(child.route, child.children);
 | 
			
		||||
      if (parent.children === undefined) parent.children = [];
 | 
			
		||||
      parent.children.push(child.route);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Combines routes from plugin MenuRoute to Vue-Router RouteRecordRaw to get a clean route-tree
 | 
			
		||||
 * @param target
 | 
			
		||||
 * @param source
 | 
			
		||||
 * @param mainPath
 | 
			
		||||
 */
 | 
			
		||||
function combineMenuRoutes(
 | 
			
		||||
  target: RouteRecordRaw[],
 | 
			
		||||
  source: FG_Plugin.MenuRoute[],
 | 
			
		||||
  mainPath: '/' | '/in' = '/'
 | 
			
		||||
): RouteRecordRaw[] {
 | 
			
		||||
  // Search parent
 | 
			
		||||
  target.forEach((target) => {
 | 
			
		||||
    if (target.path === mainPath) {
 | 
			
		||||
      // Parent found = target
 | 
			
		||||
      source.forEach((sourceMainConfig: FG_Plugin.MenuRoute) => {
 | 
			
		||||
        // Check if source is already in target
 | 
			
		||||
        const targetMainConfig = target.children?.find((targetMainConfig: RouteRecordRaw) => {
 | 
			
		||||
          return sourceMainConfig.route.path === targetMainConfig.path;
 | 
			
		||||
        });
 | 
			
		||||
        // Already in target routes, add only children
 | 
			
		||||
        if (targetMainConfig) {
 | 
			
		||||
          convertRoutes(targetMainConfig, sourceMainConfig.children);
 | 
			
		||||
        } else {
 | 
			
		||||
          // Append to target
 | 
			
		||||
          if (target.children === undefined) {
 | 
			
		||||
            target.children = [];
 | 
			
		||||
          }
 | 
			
		||||
          convertRoutes(sourceMainConfig.route, sourceMainConfig.children);
 | 
			
		||||
          if (
 | 
			
		||||
            sourceMainConfig.children &&
 | 
			
		||||
            sourceMainConfig.children.length > 0 &&
 | 
			
		||||
            !sourceMainConfig.route.component
 | 
			
		||||
          )
 | 
			
		||||
            Object.assign(sourceMainConfig.route, {
 | 
			
		||||
              component: () => import('src/components/navigation/EmptyParent.vue'),
 | 
			
		||||
            });
 | 
			
		||||
          target.children.push(sourceMainConfig.route);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  return target;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function combineRoutes(
 | 
			
		||||
  target: RouteRecordRaw[],
 | 
			
		||||
  source: FG_Plugin.NamedRouteRecordRaw[],
 | 
			
		||||
  mainPath: '/' | '/in'
 | 
			
		||||
) {
 | 
			
		||||
  // Search parent
 | 
			
		||||
  target.forEach((target) => {
 | 
			
		||||
    if (target.path === mainPath) {
 | 
			
		||||
      // Parent found = target
 | 
			
		||||
      source.forEach((sourceRoute) => {
 | 
			
		||||
        // Check if source is already in target
 | 
			
		||||
        const targetRoot = target.children?.find(
 | 
			
		||||
          (targetRoot) => sourceRoute.path === targetRoot.path
 | 
			
		||||
        );
 | 
			
		||||
        // Already in target routes, add only children
 | 
			
		||||
        if (targetRoot) {
 | 
			
		||||
          if (targetRoot.children === undefined) targetRoot.children = [];
 | 
			
		||||
          targetRoot.children.push(...(sourceRoute.children || []));
 | 
			
		||||
        } else {
 | 
			
		||||
          // Append to target
 | 
			
		||||
          if (target.children === undefined) target.children = [];
 | 
			
		||||
          if (
 | 
			
		||||
            sourceRoute.children &&
 | 
			
		||||
            sourceRoute.children.length > 0 &&
 | 
			
		||||
            sourceRoute.component === undefined
 | 
			
		||||
          )
 | 
			
		||||
            Object.assign(sourceRoute, {
 | 
			
		||||
              component: () => import('src/components/navigation/EmptyParent.vue'),
 | 
			
		||||
            });
 | 
			
		||||
          target.children.push(sourceRoute);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Combine MenuRoutes into Flaschengeist MenuLinks for the main menu
 | 
			
		||||
 * @param target Flaschengeist list of menu links
 | 
			
		||||
 * @param source MenuRoutes to combine
 | 
			
		||||
 */
 | 
			
		||||
function combineMenuLinks(target: FG_Plugin.MenuLink[], source: FG_Plugin.MenuRoute) {
 | 
			
		||||
  let idx = target.findIndex((link) => link.title == source.title);
 | 
			
		||||
  // Link not found, add new one
 | 
			
		||||
  if (idx === -1) {
 | 
			
		||||
    idx += target.push({
 | 
			
		||||
      title: source.title,
 | 
			
		||||
      icon: source.icon,
 | 
			
		||||
      link: source.route.name,
 | 
			
		||||
      permissions: source.permissions,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  if (target[idx].children === undefined) {
 | 
			
		||||
    target[idx].children = [];
 | 
			
		||||
  }
 | 
			
		||||
  source.children?.forEach((sourceChild) => {
 | 
			
		||||
    target[idx].children?.push({
 | 
			
		||||
      title: sourceChild.title,
 | 
			
		||||
      icon: sourceChild.icon,
 | 
			
		||||
      link: sourceChild.route.name,
 | 
			
		||||
      permissions: sourceChild.permissions,
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Combine shortcuts from Plugin MenuRouts into the Flaschenbeist Shortcut list
 | 
			
		||||
 * @param target Flaschengeist list of shortcuts
 | 
			
		||||
 * @param source MenuRoutes to extract shortcuts from
 | 
			
		||||
 */
 | 
			
		||||
function combineShortcuts(target: FG_Plugin.Shortcut[], source: FG_Plugin.MenuRoute[]) {
 | 
			
		||||
  source.forEach((route) => {
 | 
			
		||||
    if (route.shortcut) {
 | 
			
		||||
      target.push(<FG_Plugin.Shortcut>{
 | 
			
		||||
        link: route.route.name,
 | 
			
		||||
        icon: route.icon,
 | 
			
		||||
        permissions: route.permissions,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (route.children) {
 | 
			
		||||
      combineShortcuts(target, route.children);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Load a Flaschengeist plugin
 | 
			
		||||
 * @param loadedPlugins Flaschgeist object
 | 
			
		||||
 * @param plugin Plugin to load
 | 
			
		||||
 * @param router VueRouter instance
 | 
			
		||||
 */
 | 
			
		||||
function loadPlugin(
 | 
			
		||||
  loadedPlugins: FG_Plugin.Flaschengeist,
 | 
			
		||||
  plugin: FG_Plugin.Plugin,
 | 
			
		||||
  backend: FG.Backend
 | 
			
		||||
) {
 | 
			
		||||
  // Check if already loaded
 | 
			
		||||
  if (loadedPlugins.plugins.findIndex((p) => p.id === plugin.id) !== -1) return true;
 | 
			
		||||
 | 
			
		||||
  // Check backend dependencies
 | 
			
		||||
  if (
 | 
			
		||||
    !plugin.requiredModules.every(
 | 
			
		||||
      (required) =>
 | 
			
		||||
        backend.plugins[required[0]] !== undefined &&
 | 
			
		||||
        (required.length == 1 ||
 | 
			
		||||
          true) /* validate the version, semver440 from python is... tricky on node*/
 | 
			
		||||
    )
 | 
			
		||||
  ) {
 | 
			
		||||
    console.error(`Plugin ${plugin.id}: Backend modules not satisfied`);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Start combining and loading routes, shortcuts etc
 | 
			
		||||
  if (plugin.internalRoutes) {
 | 
			
		||||
    combineRoutes(loadedPlugins.routes, plugin.internalRoutes, '/in');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (plugin.innerRoutes) {
 | 
			
		||||
    // Routes for Vue Router
 | 
			
		||||
    combineMenuRoutes(loadedPlugins.routes, plugin.innerRoutes, '/in');
 | 
			
		||||
    // Combine links for menu
 | 
			
		||||
    plugin.innerRoutes.forEach((route) => combineMenuLinks(loadedPlugins.menuLinks, route));
 | 
			
		||||
    // Combine shortcuts
 | 
			
		||||
    combineShortcuts(loadedPlugins.shortcuts, plugin.innerRoutes);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (plugin.outerRoutes) {
 | 
			
		||||
    combineMenuRoutes(loadedPlugins.routes, plugin.outerRoutes);
 | 
			
		||||
    combineShortcuts(loadedPlugins.outerShortcuts, plugin.outerRoutes);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (plugin.widgets.length > 0) {
 | 
			
		||||
    plugin.widgets.forEach((widget) => (widget.name = plugin.id + '.' + widget.name));
 | 
			
		||||
    Array.prototype.push.apply(loadedPlugins.widgets, plugin.widgets);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!plugin.settingWidgets) plugin.settingWidgets = [];
 | 
			
		||||
  if (plugin.settingWidgets.length > 0) {
 | 
			
		||||
    plugin.settingWidgets.forEach((widget) => (widget.name = plugin.id + '.' + widget.name));
 | 
			
		||||
    Array.prototype.push.apply(loadedPlugins.settingWidgets, plugin.settingWidgets);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loadedPlugins.plugins.push({
 | 
			
		||||
    id: plugin.id,
 | 
			
		||||
    name: plugin.name,
 | 
			
		||||
    version: plugin.version,
 | 
			
		||||
    notification: plugin.notification?.bind({}) || translateNotification,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function loadPlugins(backend: FG.Backend, baseRoutes: RouteRecordRaw[]) {
 | 
			
		||||
  const loadedPlugins: FG_Plugin.Flaschengeist = {
 | 
			
		||||
    routes: baseRoutes,
 | 
			
		||||
    plugins: [],
 | 
			
		||||
    menuLinks: [],
 | 
			
		||||
    shortcuts: [],
 | 
			
		||||
    outerShortcuts: [],
 | 
			
		||||
    widgets: [],
 | 
			
		||||
    settingWidgets: [],
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Wait for all plugins to be loaded
 | 
			
		||||
  const results = await Promise.allSettled(PLUGINS);
 | 
			
		||||
 | 
			
		||||
  // Check if loaded successfully
 | 
			
		||||
  results.forEach((result) => {
 | 
			
		||||
    if (result.status === 'rejected') {
 | 
			
		||||
      throw <string>result.reason;
 | 
			
		||||
    } else {
 | 
			
		||||
      if (
 | 
			
		||||
        !(
 | 
			
		||||
          validatePlugin(result.value.default) &&
 | 
			
		||||
          loadPlugin(loadedPlugins, result.value.default, backend)
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
        throw result.value.default.id;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Sort widgets by priority
 | 
			
		||||
  /** @todo Remove priority with first beta */
 | 
			
		||||
  loadedPlugins.widgets.sort(
 | 
			
		||||
    (a, b) => <number>(b.order || b.priority) - <number>(a.order || a.priority)
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  /** @todo Can be cleaned up with first beta */
 | 
			
		||||
  loadedPlugins.menuLinks.sort((a, b) => {
 | 
			
		||||
    const diff = a.order && b.order ? b.order - a.order : 0;
 | 
			
		||||
    return diff ? diff : a.title.toString().localeCompare(b.title.toString());
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return loadedPlugins;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
/**
 | 
			
		||||
 * This boot file installs the global pinia instance
 | 
			
		||||
 */
 | 
			
		||||
import { pinia } from '@flaschengeist/api';
 | 
			
		||||
import { boot } from 'quasar/wrappers';
 | 
			
		||||
 | 
			
		||||
export default boot(({ app }) => {
 | 
			
		||||
  app.use(pinia);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <v-snackbar :timeout="0" color="error" :value="visible" top>
 | 
			
		||||
            <v-list color="error" dense>
 | 
			
		||||
                <v-list-item v-for="(error, index) in errors" :key="index" dense>
 | 
			
		||||
                    <v-list-item-title class="caption" style="color: white;">
 | 
			
		||||
                        {{error.message}}
 | 
			
		||||
                    </v-list-item-title>
 | 
			
		||||
                </v-list-item>
 | 
			
		||||
            </v-list>
 | 
			
		||||
            <v-btn icon color="white" @click="deleteErrors">
 | 
			
		||||
                <v-icon>
 | 
			
		||||
                    mdi-close
 | 
			
		||||
                </v-icon>
 | 
			
		||||
            </v-btn>
 | 
			
		||||
        </v-snackbar>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { mapGetters, mapActions } from 'vuex'
 | 
			
		||||
    export default {
 | 
			
		||||
        name: "ConnectionError",
 | 
			
		||||
        methods: {
 | 
			
		||||
            ...mapActions({
 | 
			
		||||
                deleteErrors: 'connectionError/deleteErrors'
 | 
			
		||||
            })
 | 
			
		||||
        },
 | 
			
		||||
        computed: {
 | 
			
		||||
            ...mapGetters({
 | 
			
		||||
                errors: 'connectionError/errors',
 | 
			
		||||
                visible: 'connectionError/visible'
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,48 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <v-bottom-sheet persistent v-model="show" hide-overlay>
 | 
			
		||||
    <v-card>
 | 
			
		||||
      <v-card-title>
 | 
			
		||||
        Cookie und Local Storage Hinweis
 | 
			
		||||
      </v-card-title>
 | 
			
		||||
      <v-card-text>
 | 
			
		||||
        Diese Webseite benutzt den Local Storage. Dabei werden Daten in ihm
 | 
			
		||||
        gespeichert, welche notwendig sind um sich einzuloggen und eingeloggt zu
 | 
			
		||||
        bleiben. Außerdem sind diese Daten notwendig um mit dem Server zu
 | 
			
		||||
        kommunizieren. Dabei wird ein Key 'user' angelegt, in welchem ein
 | 
			
		||||
        Accesstoken, Benutzername, sowie der Name des Benutzers und deren Rechte
 | 
			
		||||
        gespeichert. Dazu kommt ein Key 'cookie:accepted', falls sie diesem
 | 
			
		||||
        zustimmen. Diese Daten bleiben solange erhalten bis Sie sich ausloggen
 | 
			
		||||
        oder der Accesstoken abgelaufen ist und Sie ausgeloggt werden.
 | 
			
		||||
      </v-card-text>
 | 
			
		||||
      <v-card-actions>
 | 
			
		||||
        <v-spacer />
 | 
			
		||||
        <v-btn text @click="disableNotification()">Ablehnen</v-btn>
 | 
			
		||||
        <v-btn text color="primary" @click="acceptNotification()">Akzeptieren</v-btn>
 | 
			
		||||
      </v-card-actions>
 | 
			
		||||
    </v-card>
 | 
			
		||||
  </v-bottom-sheet>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapGetters, mapActions } from 'vuex'
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'CookieNotification',
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions(['acceptNotification', 'disableNotification', 'getCookieAccepted'])
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.getCookieAccepted()
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters({
 | 
			
		||||
      model: 'cookieNotification',
 | 
			
		||||
      cookie: 'cookieAccepted'
 | 
			
		||||
    }),
 | 
			
		||||
    show() {
 | 
			
		||||
      return !this.cookie ? this.model : false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,101 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <q-card
 | 
			
		||||
    bordered
 | 
			
		||||
    style="position: relative; min-height: 3em"
 | 
			
		||||
    :class="{ 'cursor-pointer': modelValue.link }"
 | 
			
		||||
    @click="click"
 | 
			
		||||
  >
 | 
			
		||||
    <q-btn
 | 
			
		||||
      round
 | 
			
		||||
      dense
 | 
			
		||||
      icon="mdi-trash-can"
 | 
			
		||||
      size="sm"
 | 
			
		||||
      color="negative"
 | 
			
		||||
      class="q-ma-xs"
 | 
			
		||||
      title="Löschen"
 | 
			
		||||
      style="position: absolute; top: 0; right: 0; z-index: 999"
 | 
			
		||||
      @click.stop.prevent="dismiss"
 | 
			
		||||
    />
 | 
			
		||||
    <q-card-section class="q-pa-xs">
 | 
			
		||||
      <div class="text-overline">{{ dateString }}</div>
 | 
			
		||||
      <q-item style="padding: 1px">
 | 
			
		||||
        <q-item-section v-if="modelValue.icon" side
 | 
			
		||||
          ><q-icon color="primary" :name="modelValue.icon"
 | 
			
		||||
        /></q-item-section>
 | 
			
		||||
        <q-item-section>{{ modelValue.text }}</q-item-section>
 | 
			
		||||
      </q-item>
 | 
			
		||||
    </q-card-section>
 | 
			
		||||
    <q-card-actions v-if="modelValue.reject || modelValue.accept">
 | 
			
		||||
      <q-btn
 | 
			
		||||
        v-if="modelValue.accept"
 | 
			
		||||
        icon="mdi-check"
 | 
			
		||||
        color="positive"
 | 
			
		||||
        label="Annehmen"
 | 
			
		||||
        flat
 | 
			
		||||
        dense
 | 
			
		||||
        size="sm"
 | 
			
		||||
        @click.stop.prevent="accept"
 | 
			
		||||
      />
 | 
			
		||||
      <q-btn
 | 
			
		||||
        v-if="modelValue.reject"
 | 
			
		||||
        icon="mdi-close"
 | 
			
		||||
        color="negative"
 | 
			
		||||
        label="Ablehnen"
 | 
			
		||||
        flat
 | 
			
		||||
        dense
 | 
			
		||||
        size="sm"
 | 
			
		||||
        @click.stop.prevent="reject"
 | 
			
		||||
      />
 | 
			
		||||
    </q-card-actions>
 | 
			
		||||
  </q-card>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, PropType, computed } from 'vue';
 | 
			
		||||
import { formatDateTime } from '@flaschengeist/api';
 | 
			
		||||
import { FG_Plugin } from '@flaschengeist/types';
 | 
			
		||||
import { useRouter } from 'vue-router';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'Notification',
 | 
			
		||||
  props: {
 | 
			
		||||
    modelValue: {
 | 
			
		||||
      required: true,
 | 
			
		||||
      type: Object as PropType<FG_Plugin.Notification>,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  emits: {
 | 
			
		||||
    remove: (id: number) => !!id,
 | 
			
		||||
  },
 | 
			
		||||
  setup(props, { emit }) {
 | 
			
		||||
    const router = useRouter();
 | 
			
		||||
    const dateString = computed(() => formatDateTime(props.modelValue.time, true, true));
 | 
			
		||||
 | 
			
		||||
    async function click() {
 | 
			
		||||
      if (props.modelValue.link) await router.push(props.modelValue.link);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function accept() {
 | 
			
		||||
      if (typeof props.modelValue.accept === 'function')
 | 
			
		||||
        void props.modelValue.accept().finally(() => emit('remove', props.modelValue.id));
 | 
			
		||||
      else emit('remove', props.modelValue.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function reject() {
 | 
			
		||||
      if (typeof props.modelValue.reject === 'function')
 | 
			
		||||
        void props.modelValue.reject().finally(() => emit('remove', props.modelValue.id));
 | 
			
		||||
      else emit('remove', props.modelValue.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function dismiss() {
 | 
			
		||||
      if (typeof props.modelValue.dismiss === 'function')
 | 
			
		||||
        void props.modelValue.dismiss().finally(() => emit('remove', props.modelValue.id));
 | 
			
		||||
      else emit('remove', props.modelValue.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { accept, click, dateString, dismiss, reject };
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,146 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <v-content>
 | 
			
		||||
    <v-container class="fill-height" fluid>
 | 
			
		||||
      <v-row align="center" justify="center">
 | 
			
		||||
        <v-col cols="12" sm="8" md="4">
 | 
			
		||||
          <v-card class="elevation-12">
 | 
			
		||||
            <v-toolbar color="blue accent-4" dark flat dense>
 | 
			
		||||
              <v-toolbar-title>Password vergessen</v-toolbar-title>
 | 
			
		||||
              <v-spacer />
 | 
			
		||||
            </v-toolbar>
 | 
			
		||||
            <v-card-text>
 | 
			
		||||
              <v-form lazy-validation ref="reset">
 | 
			
		||||
                <v-text-field
 | 
			
		||||
                  label="E-Mail oder Nutzername"
 | 
			
		||||
                  v-model="input"
 | 
			
		||||
                  hint="Hier bitte deinen Nutzernamen oder deine E-Mail angeben. Sollte eins der beiden Daten gefunden werden, wird an deine hinterlegte E-Mail ein Password zum Zurücksetzen gesendet."
 | 
			
		||||
                  persistent-hint
 | 
			
		||||
                  :prepend-icon="prependIcon"
 | 
			
		||||
                  required
 | 
			
		||||
                  :rules="[notEmpty, isMail ? email : true]"
 | 
			
		||||
                  @keyup.enter="resetPassword()"
 | 
			
		||||
                >
 | 
			
		||||
                </v-text-field>
 | 
			
		||||
              </v-form>
 | 
			
		||||
            </v-card-text>
 | 
			
		||||
            <v-card-actions>
 | 
			
		||||
              <v-spacer />
 | 
			
		||||
              <v-btn
 | 
			
		||||
                color="primary"
 | 
			
		||||
                @click="resetPassword()"
 | 
			
		||||
                @submit.prevent="resetPassword()"
 | 
			
		||||
              >
 | 
			
		||||
                Zurücksetzen
 | 
			
		||||
              </v-btn>
 | 
			
		||||
            </v-card-actions>
 | 
			
		||||
          </v-card>
 | 
			
		||||
        </v-col>
 | 
			
		||||
      </v-row>
 | 
			
		||||
    </v-container>
 | 
			
		||||
    <v-snackbar bottom :timeout="0" :value="response.value" :color="response.error? 'error' : 'success'">
 | 
			
		||||
      {{ response.message }}
 | 
			
		||||
      <v-btn icon @click="response.value = false">
 | 
			
		||||
        <v-icon color="white">
 | 
			
		||||
          mdi-close
 | 
			
		||||
        </v-icon>
 | 
			
		||||
      </v-btn>
 | 
			
		||||
    </v-snackbar>
 | 
			
		||||
  </v-content>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import url from '@/plugins/routes'
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'ResetPassword',
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      input: '',
 | 
			
		||||
      response: {
 | 
			
		||||
        error: false,
 | 
			
		||||
        value: false,
 | 
			
		||||
        message: null
 | 
			
		||||
      },
 | 
			
		||||
      defaultResponse: {
 | 
			
		||||
        error: false,
 | 
			
		||||
        value: false,
 | 
			
		||||
        message: null
 | 
			
		||||
      },
 | 
			
		||||
      notEmpty: data => {
 | 
			
		||||
        return data ? true : 'Darf nicht leer sein.'
 | 
			
		||||
      },
 | 
			
		||||
      email: value => {
 | 
			
		||||
        if (value.length > 0) {
 | 
			
		||||
          const pattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
 | 
			
		||||
          return pattern.test(value) || 'keine gültige E-Mail'
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    resetPassword() {
 | 
			
		||||
      if (this.$refs.reset.validate()) {
 | 
			
		||||
        console.log(this.input, this.isMail)
 | 
			
		||||
        if (this.isMail) {
 | 
			
		||||
          axios
 | 
			
		||||
            .post(url.resetPassword, { mail: this.input })
 | 
			
		||||
            .then(data => {
 | 
			
		||||
              console.log(data)
 | 
			
		||||
              this.setMessage(data.data.mail, false)
 | 
			
		||||
            })
 | 
			
		||||
            .catch(error => {
 | 
			
		||||
              console.log(error)
 | 
			
		||||
              this.setMessage(error, true)
 | 
			
		||||
            })
 | 
			
		||||
            .finally(() => {
 | 
			
		||||
              this.$refs.reset.reset()
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
          axios
 | 
			
		||||
            .post(url.resetPassword, { username: this.input })
 | 
			
		||||
            .then(data => {
 | 
			
		||||
              console.log(data)
 | 
			
		||||
              this.setMessage(data.data.mail, false)
 | 
			
		||||
            })
 | 
			
		||||
            .catch(error => {
 | 
			
		||||
              console.log(error)
 | 
			
		||||
              this.setMessage(error, true)
 | 
			
		||||
            })
 | 
			
		||||
            .finally(() => {
 | 
			
		||||
              this.$refs.reset.reset()
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    setMessage(mail, error) {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        this.response.error = true
 | 
			
		||||
        this.response.value = true
 | 
			
		||||
        this.response.message =
 | 
			
		||||
          'Es ist ein Fehler aufgetreten. Wende dich an einen Administrator oder probiere es erneut.'
 | 
			
		||||
      } else {
 | 
			
		||||
        this.response.error = false
 | 
			
		||||
        this.response.value = true
 | 
			
		||||
        this.response.message = `Es wurde ein neues Password an ${mail} versendet`
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    prependIcon() {
 | 
			
		||||
      if (this.input) {
 | 
			
		||||
        return this.input.includes('@') ? 'mdi-email' : 'mdi-account'
 | 
			
		||||
      }
 | 
			
		||||
      return 'mdi-account'
 | 
			
		||||
    },
 | 
			
		||||
    isMail() {
 | 
			
		||||
      if (this.input) {
 | 
			
		||||
        return this.input.includes('@')
 | 
			
		||||
      }
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,94 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <v-app-bar
 | 
			
		||||
            app
 | 
			
		||||
            clipped-left
 | 
			
		||||
            clipped-right
 | 
			
		||||
            color="blue accent-4"
 | 
			
		||||
            class="elevation-4"
 | 
			
		||||
            dark
 | 
			
		||||
            dense
 | 
			
		||||
    >
 | 
			
		||||
      <v-btn icon @click="reload()">
 | 
			
		||||
        <v-img src="@/assets/logo-64.png" contain height="40"></v-img>
 | 
			
		||||
      </v-btn>
 | 
			
		||||
      <v-toolbar-title>Flaschengeist <span class="caption">v1.1.0 beta 0</span></v-toolbar-title>
 | 
			
		||||
      <v-spacer/>
 | 
			
		||||
      <v-btn icon v-if="getRouteName == 'resetPassword'" @click="goTo('login')">
 | 
			
		||||
        <v-icon>
 | 
			
		||||
          mdi-home
 | 
			
		||||
        </v-icon>
 | 
			
		||||
      </v-btn>
 | 
			
		||||
      <v-btn icon v-if="getRouteName == 'priceListNoLogin'" @click="goBack()">
 | 
			
		||||
        <v-icon>
 | 
			
		||||
          {{ back }}
 | 
			
		||||
        </v-icon>
 | 
			
		||||
      </v-btn>
 | 
			
		||||
      <v-btn icon v-if="isFinanzer" :disabled="locked" @click="goTo('overview')">
 | 
			
		||||
        <v-icon>{{ attach_money }}</v-icon>
 | 
			
		||||
      </v-btn>
 | 
			
		||||
      <v-btn icon v-if="isGastro" :disabled="locked" @click="goTo('gastroPricelist')">
 | 
			
		||||
        <v-icon>{{ gastro }}</v-icon>
 | 
			
		||||
      </v-btn>
 | 
			
		||||
      <v-btn icon v-if="isBar" @click="goTo('geruecht')">
 | 
			
		||||
        <v-icon>{{ local_bar }}</v-icon>
 | 
			
		||||
      </v-btn>
 | 
			
		||||
      <v-btn icon v-if="isUser" :disabled="locked" @click="goTo('add')">
 | 
			
		||||
        <v-icon>{{ person }}</v-icon>
 | 
			
		||||
      </v-btn>
 | 
			
		||||
      <v-btn icon @click="goTo('priceListNoLogin')">
 | 
			
		||||
        <v-icon>{{ list }}</v-icon>
 | 
			
		||||
      </v-btn>
 | 
			
		||||
    </v-app-bar>
 | 
			
		||||
    <ConnectionError/>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapActions, mapGetters } from 'vuex'
 | 
			
		||||
import {
 | 
			
		||||
  mdiCurrencyEur,
 | 
			
		||||
  mdiGlassCocktail,
 | 
			
		||||
  mdiAccount,
 | 
			
		||||
  mdiFileMultiple,
 | 
			
		||||
  mdiFoodForkDrink,
 | 
			
		||||
  mdiArrowLeftBoldCircle
 | 
			
		||||
} from '@mdi/js'
 | 
			
		||||
import ConnectionError from "@/components/ConnectionError";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'TitleBar',
 | 
			
		||||
  components: {ConnectionError},
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      attach_money: mdiCurrencyEur,
 | 
			
		||||
      local_bar: mdiGlassCocktail,
 | 
			
		||||
      person: mdiAccount,
 | 
			
		||||
      list: mdiFileMultiple,
 | 
			
		||||
      gastro: mdiFoodForkDrink,
 | 
			
		||||
      back: mdiArrowLeftBoldCircle
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters(['isBar', 'isFinanzer', 'isUser', 'isLoggedIn', 'isGastro']),
 | 
			
		||||
    ...mapGetters({locked: 'barUsers/locked'}),
 | 
			
		||||
    getRouteName() {
 | 
			
		||||
      return this.$route.name
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions(['logout']),
 | 
			
		||||
    reload() {
 | 
			
		||||
      location.reload()
 | 
			
		||||
    },
 | 
			
		||||
    goTo(name) {
 | 
			
		||||
      this.$router.push({name: name})
 | 
			
		||||
    },
 | 
			
		||||
    goBack() {
 | 
			
		||||
      window.history.length > 1 ? this.$router.go(-1) : this.$router.push({name: 'main'})
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,56 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <q-card class="col-4">
 | 
			
		||||
    <q-card-section class="row fit justify-center items-center content-center">
 | 
			
		||||
      <q-avatar size="150px">
 | 
			
		||||
        <q-img :src="pic" placeholder-src="logo-black.svg" />
 | 
			
		||||
      </q-avatar>
 | 
			
		||||
    </q-card-section>
 | 
			
		||||
    <q-card-section>
 | 
			
		||||
      <div class="text-h6">{{ firstname }} {{ lastname }}</div>
 | 
			
		||||
      <div class="text-subtitle2">{{ club }}</div>
 | 
			
		||||
      <q-separator />
 | 
			
		||||
      <div class="text-subtitle2">Aufgabe bei Flaschengeist:</div>
 | 
			
		||||
      <div>
 | 
			
		||||
        {{ job }}
 | 
			
		||||
      </div>
 | 
			
		||||
    </q-card-section>
 | 
			
		||||
 | 
			
		||||
    <q-card-section class="q-pt-none">
 | 
			
		||||
      {{ description }}
 | 
			
		||||
    </q-card-section>
 | 
			
		||||
  </q-card>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'Developer',
 | 
			
		||||
  props: {
 | 
			
		||||
    pic: {
 | 
			
		||||
      default: 'logo-dark.svg',
 | 
			
		||||
      type: String,
 | 
			
		||||
    },
 | 
			
		||||
    firstname: {
 | 
			
		||||
      required: true,
 | 
			
		||||
      type: String,
 | 
			
		||||
    },
 | 
			
		||||
    lastname: {
 | 
			
		||||
      required: true,
 | 
			
		||||
      type: String,
 | 
			
		||||
    },
 | 
			
		||||
    job: {
 | 
			
		||||
      default: 'student',
 | 
			
		||||
      type: String,
 | 
			
		||||
    },
 | 
			
		||||
    club: {
 | 
			
		||||
      default: 'Studentenclub Wu5 e.V.',
 | 
			
		||||
      type: String,
 | 
			
		||||
    },
 | 
			
		||||
    description: {
 | 
			
		||||
      default:
 | 
			
		||||
        'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ',
 | 
			
		||||
      type: String,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,446 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <v-container v-if="!first_loading">
 | 
			
		||||
      <v-card class="mx-auto" outlined :loading="loading">
 | 
			
		||||
        <v-card-title>
 | 
			
		||||
          <div class="title">Dienstgetränke</div>
 | 
			
		||||
          <v-chip class="ma-2" color="red" text-color="white" v-if="locked"
 | 
			
		||||
            >Gesperrt</v-chip
 | 
			
		||||
          >
 | 
			
		||||
          <v-btn class="menuBtn" @click.stop="isMenuShow = !isMenuShow" icon>
 | 
			
		||||
            <v-icon>{{ menuIcon }}</v-icon>
 | 
			
		||||
          </v-btn>
 | 
			
		||||
        </v-card-title>
 | 
			
		||||
        <v-card-subtitle v-if="limit - free_drink_list_history_job_credit > 0">
 | 
			
		||||
          Noch
 | 
			
		||||
          {{ ((limit - free_drink_list_history_job_credit) / 100).toFixed(2) }}
 | 
			
		||||
          € übrig!!
 | 
			
		||||
        </v-card-subtitle>
 | 
			
		||||
        <v-card-text>
 | 
			
		||||
          <v-row v-if="!first_loading">
 | 
			
		||||
            <v-col cols="12">
 | 
			
		||||
              <v-row>
 | 
			
		||||
                <v-col
 | 
			
		||||
                  v-for="freeDrink in free_drink_list_config_job"
 | 
			
		||||
                  :key="freeDrink.id"
 | 
			
		||||
                  cols="6"
 | 
			
		||||
                  xs="3"
 | 
			
		||||
                  sm="4"
 | 
			
		||||
                  class="drinkCol"
 | 
			
		||||
                >
 | 
			
		||||
                  <v-btn
 | 
			
		||||
                    class="drinkBtn"
 | 
			
		||||
                    block
 | 
			
		||||
                    :disabled="locked"
 | 
			
		||||
                    :color="color"
 | 
			
		||||
                    @click="addAmount(freeDrink, freeDrinkTypeJob)"
 | 
			
		||||
                    >{{ freeDrink.label }}</v-btn
 | 
			
		||||
                  >
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
              <v-row v-if="!first_loading" class="justify-end">
 | 
			
		||||
                <v-col cols="3">
 | 
			
		||||
                  <v-list-item>
 | 
			
		||||
                    <v-list-item-content class="text-center">
 | 
			
		||||
                      <v-list-item-action-text :class="getColor()">
 | 
			
		||||
                        {{
 | 
			
		||||
                          (free_drink_list_history_job_credit / 100).toFixed(2)
 | 
			
		||||
                        }}
 | 
			
		||||
                        €
 | 
			
		||||
                      </v-list-item-action-text>
 | 
			
		||||
                    </v-list-item-content>
 | 
			
		||||
                  </v-list-item>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
            </v-col>
 | 
			
		||||
          </v-row>
 | 
			
		||||
        </v-card-text>
 | 
			
		||||
      </v-card>
 | 
			
		||||
      <v-card class="mx-auto" outlined :loading="loading">
 | 
			
		||||
        <v-card-title>
 | 
			
		||||
          <div class="title">Bandgetränke</div>
 | 
			
		||||
          <v-btn class="menuBtn" @click.stop="isMenuShow = !isMenuShow" icon>
 | 
			
		||||
            <v-icon>{{ menuIcon }}</v-icon>
 | 
			
		||||
          </v-btn>
 | 
			
		||||
        </v-card-title>
 | 
			
		||||
        <v-card-text>
 | 
			
		||||
          <v-row v-if="!first_loading">
 | 
			
		||||
            <v-col cols="12">
 | 
			
		||||
              <v-row>
 | 
			
		||||
                <v-col
 | 
			
		||||
                  v-for="freeDrink in free_drink_list_config_band"
 | 
			
		||||
                  :key="freeDrink.id"
 | 
			
		||||
                  cols="6"
 | 
			
		||||
                  xs="3"
 | 
			
		||||
                  sm="4"
 | 
			
		||||
                  class="drinkCol"
 | 
			
		||||
                >
 | 
			
		||||
                  <v-btn
 | 
			
		||||
                    class="drinkBtn"
 | 
			
		||||
                    block
 | 
			
		||||
                    :color="color_fix"
 | 
			
		||||
                    @click="addAmount(freeDrink, freeDrinkTypeBand)"
 | 
			
		||||
                    >{{ freeDrink.label }}</v-btn
 | 
			
		||||
                  >
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
              <v-row v-if="!first_loading" class="justify-end">
 | 
			
		||||
                <v-col cols="3">
 | 
			
		||||
                  <v-list-item>
 | 
			
		||||
                    <v-list-item-content class="text-center">
 | 
			
		||||
                      <v-list-item-action-text class="title">
 | 
			
		||||
                        {{
 | 
			
		||||
                          (free_drink_list_history_band_without_canceled_price/100).toFixed(2)
 | 
			
		||||
                        }} €
 | 
			
		||||
                      </v-list-item-action-text>
 | 
			
		||||
                    </v-list-item-content>
 | 
			
		||||
                  </v-list-item>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
            </v-col>
 | 
			
		||||
          </v-row>
 | 
			
		||||
        </v-card-text>
 | 
			
		||||
      </v-card>
 | 
			
		||||
    </v-container>
 | 
			
		||||
 | 
			
		||||
    <v-navigation-drawer v-model="isMenuShow" right app clipped>
 | 
			
		||||
      <v-list-item-group :key="componentRenderer">
 | 
			
		||||
        <v-list-item inactive>
 | 
			
		||||
          <v-list-item-title class="headline">Verlauf</v-list-item-title>
 | 
			
		||||
        </v-list-item>
 | 
			
		||||
        <v-divider />
 | 
			
		||||
        <div
 | 
			
		||||
          v-for="freeDrinkHistory in free_drink_list_history_bar"
 | 
			
		||||
          :key="freeDrinkHistory.id"
 | 
			
		||||
        >
 | 
			
		||||
          <v-list-item
 | 
			
		||||
            three-line
 | 
			
		||||
            inactive
 | 
			
		||||
            @click="canceledAmount(freeDrinkHistory)"
 | 
			
		||||
          >
 | 
			
		||||
            <v-list-item-content>
 | 
			
		||||
              <v-list-item-title>{{
 | 
			
		||||
                now(freeDrinkHistory.timestamp)
 | 
			
		||||
              }}</v-list-item-title>
 | 
			
		||||
              <v-list-item-subtitle
 | 
			
		||||
                >{{ freeDrinkHistory.free_drink_type.name }}:
 | 
			
		||||
                {{ freeDrinkHistory.free_drink_config.label }} wurde für
 | 
			
		||||
                {{
 | 
			
		||||
                  (freeDrinkHistory.free_drink_config.price / 100).toFixed(2)
 | 
			
		||||
                }}
 | 
			
		||||
                € hinzugefügt.
 | 
			
		||||
              </v-list-item-subtitle>
 | 
			
		||||
              <v-list-item-subtitle
 | 
			
		||||
                class="red--text"
 | 
			
		||||
                v-if="freeDrinkHistory.canceled"
 | 
			
		||||
                >STORNIERT!!!</v-list-item-subtitle
 | 
			
		||||
              >
 | 
			
		||||
              <v-list-item-action-text
 | 
			
		||||
                v-if="
 | 
			
		||||
                  isStronoEnabled(freeDrinkHistory.timestamp) &&
 | 
			
		||||
                    !freeDrinkHistory.canceled
 | 
			
		||||
                "
 | 
			
		||||
                >Klicken um zu Stornieren</v-list-item-action-text
 | 
			
		||||
              >
 | 
			
		||||
            </v-list-item-content>
 | 
			
		||||
          </v-list-item>
 | 
			
		||||
        </div>
 | 
			
		||||
      </v-list-item-group>
 | 
			
		||||
    </v-navigation-drawer>
 | 
			
		||||
 | 
			
		||||
    <v-dialog v-model="showConfirmCanceledDialog" max-width="290">
 | 
			
		||||
      <v-card>
 | 
			
		||||
        <v-card-title>Willst du wirklich??</v-card-title>
 | 
			
		||||
        <v-card-text v-if="canceledMessage">
 | 
			
		||||
          {{ canceledMessage.free_drink_type.name }}: Willst du wirklich ein
 | 
			
		||||
          {{ canceledMessage.free_drink_config.label }} im Wert von
 | 
			
		||||
          {{ (canceledMessage.free_drink_config.price / 100).toFixed(2) }} €
 | 
			
		||||
          stornieren?
 | 
			
		||||
        </v-card-text>
 | 
			
		||||
        <v-card-actions>
 | 
			
		||||
          <v-spacer />
 | 
			
		||||
          <v-btn text @click="cancelCanceled">Abbrechen</v-btn>
 | 
			
		||||
          <v-btn text @click="acceptCanceled">Stornieren</v-btn>
 | 
			
		||||
        </v-card-actions>
 | 
			
		||||
      </v-card>
 | 
			
		||||
    </v-dialog>
 | 
			
		||||
 | 
			
		||||
    <v-container v-if="first_loading">
 | 
			
		||||
      <AddAmountSkeleton />
 | 
			
		||||
    </v-container>
 | 
			
		||||
    <v-snackbar
 | 
			
		||||
        :color="
 | 
			
		||||
              snackbar_messages.length > 0
 | 
			
		||||
                ? snackbar_messages[0].error
 | 
			
		||||
                  ? 'error'
 | 
			
		||||
                  : 'success'
 | 
			
		||||
                : 'success'
 | 
			
		||||
            "
 | 
			
		||||
        bottom
 | 
			
		||||
        :timeout="0"
 | 
			
		||||
        :multi-line="true"
 | 
			
		||||
        :value="snackbar_messages.length > 0 ? snackbar_messages[0].visible : null"
 | 
			
		||||
        vertical
 | 
			
		||||
    >
 | 
			
		||||
      <v-list-item
 | 
			
		||||
          v-for="(message, index) in snackbar_messages"
 | 
			
		||||
          :key="index"
 | 
			
		||||
          :style="
 | 
			
		||||
                message.error
 | 
			
		||||
                  ? 'background-color: #FF5252;'
 | 
			
		||||
                  : 'background-color: #4CAF50;'
 | 
			
		||||
              "
 | 
			
		||||
          v-show="message.visible"
 | 
			
		||||
      >
 | 
			
		||||
        <v-list-item-content>
 | 
			
		||||
          <v-list-item-title style="color: white">
 | 
			
		||||
            {{ createMessage(message) }}
 | 
			
		||||
          </v-list-item-title>
 | 
			
		||||
        </v-list-item-content>
 | 
			
		||||
        <v-list-item-action v-if="message.error">
 | 
			
		||||
          <v-btn icon @click="message.visible = false">
 | 
			
		||||
            <v-icon color="white">
 | 
			
		||||
              mdi-close
 | 
			
		||||
            </v-icon>
 | 
			
		||||
          </v-btn>
 | 
			
		||||
        </v-list-item-action>
 | 
			
		||||
      </v-list-item>
 | 
			
		||||
    </v-snackbar>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
// eslint-disable-next-line no-unused-vars
 | 
			
		||||
import AddAmountSkeleton from '../user/Skeleton/AddAmountSkeleton'
 | 
			
		||||
import { mapActions, mapGetters } from 'vuex'
 | 
			
		||||
import { mdiPlus, mdiMenu } from '@mdi/js'
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'BarFreedrinks',
 | 
			
		||||
  components: { AddAmountSkeleton },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.timer = setInterval(() => (this.componentRenderer += 1), 1000)
 | 
			
		||||
    this.get_free_drink_list_config()
 | 
			
		||||
    this.get_free_drink_list_history()
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      plus: mdiPlus,
 | 
			
		||||
      menuIcon: mdiMenu,
 | 
			
		||||
      showConfirmCanceledDialog: false,
 | 
			
		||||
      canceledMessage: null,
 | 
			
		||||
      limit: 1000,
 | 
			
		||||
      amount: 0,
 | 
			
		||||
      customValue: null,
 | 
			
		||||
      color: 'green accent-4',
 | 
			
		||||
      color_fix: 'green accent-4',
 | 
			
		||||
      timer: '',
 | 
			
		||||
      componentRenderer: 0,
 | 
			
		||||
      isMenuShow: false,
 | 
			
		||||
      freeDrinkTypeJob: 1,
 | 
			
		||||
      freeDrinkTypeBand: 3
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions('freeDrinkList', [
 | 
			
		||||
      'get_free_drink_list_config',
 | 
			
		||||
      'get_free_drink_list_history',
 | 
			
		||||
      'set_free_drink_list_history',
 | 
			
		||||
      'update_free_drink_list_history'
 | 
			
		||||
    ]),
 | 
			
		||||
    output() {
 | 
			
		||||
      console.log(this.free_drink_list_config)
 | 
			
		||||
    },
 | 
			
		||||
    addAmount(freeDrink, free_drink_type_id) {
 | 
			
		||||
      this.set_free_drink_list_history({ ...freeDrink, free_drink_type_id })
 | 
			
		||||
      /*if (amount) {
 | 
			
		||||
        this.amount += amount
 | 
			
		||||
        this.generateMessage(amount)
 | 
			
		||||
      }
 | 
			
		||||
      this.checkLocked()*/
 | 
			
		||||
    },
 | 
			
		||||
    canceledAmount(historyElement) {
 | 
			
		||||
      if (!this.isStronoEnabled(historyElement.timestamp)) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.showConfirmCanceledDialog = true
 | 
			
		||||
      this.canceledMessage = historyElement
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    cancelCanceled() {
 | 
			
		||||
      this.showConfirmCanceledDialog = null
 | 
			
		||||
      this.canceledMessage = null
 | 
			
		||||
    },
 | 
			
		||||
    generateMessage(amount) {
 | 
			
		||||
      this.messages.push({
 | 
			
		||||
        date: new Date(),
 | 
			
		||||
        canceled: false,
 | 
			
		||||
        amount: amount,
 | 
			
		||||
        error: false,
 | 
			
		||||
        visible: true
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    createMessage(message) {
 | 
			
		||||
      var text = ''
 | 
			
		||||
      if (message.error) {
 | 
			
		||||
        text =
 | 
			
		||||
          'ERROR: ' + message.free_drink_type.name +': Konnte ' + message.label + ' für ' +
 | 
			
		||||
          (message.price / 100).toFixed(2) +
 | 
			
		||||
          '€ nicht hinzufügen.'
 | 
			
		||||
      } else if (message.canceled) {
 | 
			
		||||
        text = `${message.free_drink_type.name}: ${message.label} wurde für ${(message.price/100).toFixed(2)}€ storniert.`
 | 
			
		||||
      } else {
 | 
			
		||||
        text = message.free_drink_type.name + ': ' + message.label + ' wurde für ' + (message.price / 100).toFixed(2) + '€ hinzugefügt.'
 | 
			
		||||
      }
 | 
			
		||||
      return text
 | 
			
		||||
    },
 | 
			
		||||
    checkLocked() {
 | 
			
		||||
      this.locked = this.limit - this.amount <= 0
 | 
			
		||||
    },
 | 
			
		||||
    getColor() {
 | 
			
		||||
      return this.locked ? 'title red--text' : 'title'
 | 
			
		||||
    },
 | 
			
		||||
    acceptCanceled() {
 | 
			
		||||
      this.canceledMessage.canceled = true
 | 
			
		||||
      this.update_free_drink_list_history(this.canceledMessage)
 | 
			
		||||
      this.cancelCanceled()
 | 
			
		||||
    },
 | 
			
		||||
    acceptStorno() {
 | 
			
		||||
      this.stornoMessage.storno = true
 | 
			
		||||
      this.amount -= this.stornoMessage.amount
 | 
			
		||||
      console.log(this.amount, this.stornoMessage)
 | 
			
		||||
      this.cancelStorno()
 | 
			
		||||
    },
 | 
			
		||||
    stornoAmount(message) {
 | 
			
		||||
      if (!this.isStronoEnabled(message.date) || message.storno) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.showConfirmStornoDialog = true
 | 
			
		||||
      this.stornoMessage = message
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    cancelStorno() {
 | 
			
		||||
      this.showConfirmStornoDialog = null
 | 
			
		||||
      this.stornoMessage = null
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters('freeDrinkList', [
 | 
			
		||||
      'free_drink_list_config_job',
 | 
			
		||||
      'free_drink_list_config_band',
 | 
			
		||||
      'free_drink_list_history',
 | 
			
		||||
      'free_drink_list_history_job_credit',
 | 
			
		||||
      'free_drink_list_history_band',
 | 
			
		||||
      'free_drink_list_history_band_without_canceled',
 | 
			
		||||
      'loading',
 | 
			
		||||
      'snackbar_messages'
 | 
			
		||||
    ]),
 | 
			
		||||
    now() {
 | 
			
		||||
      return now => {
 | 
			
		||||
        var actual = new Date()
 | 
			
		||||
        var zero = new Date(0)
 | 
			
		||||
        var date = new Date(actual - now)
 | 
			
		||||
        if (date.getFullYear() === zero.getFullYear()) {
 | 
			
		||||
          if (date.getMonth() === zero.getMonth()) {
 | 
			
		||||
            if (date.getDate() === zero.getDate()) {
 | 
			
		||||
              if (date.getHours() === zero.getDate()) {
 | 
			
		||||
                if (date.getMinutes() < 1) {
 | 
			
		||||
                  return 'vor ' + date.getSeconds() + ' Sekunden'
 | 
			
		||||
                } else if (date.getMinutes() < 10) {
 | 
			
		||||
                  return 'vor ' + date.getMinutes() + ' Minuten'
 | 
			
		||||
                } else {
 | 
			
		||||
                  return (
 | 
			
		||||
                    (now.getHours() < 10 ? '0' : '') +
 | 
			
		||||
                    now.getHours() +
 | 
			
		||||
                    ':' +
 | 
			
		||||
                    (now.getMinutes() < 10 ? '0' : '') +
 | 
			
		||||
                    now.getMinutes()
 | 
			
		||||
                  )
 | 
			
		||||
                }
 | 
			
		||||
              } else {
 | 
			
		||||
                return (
 | 
			
		||||
                  (now.getHours() < 10 ? '0' : '') +
 | 
			
		||||
                  now.getHours() +
 | 
			
		||||
                  ':' +
 | 
			
		||||
                  (now.getMinutes() < 10 ? '0' : '') +
 | 
			
		||||
                  now.getMinutes()
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return (
 | 
			
		||||
          now.getDate() +
 | 
			
		||||
          '.' +
 | 
			
		||||
          now.getMonth() +
 | 
			
		||||
          '.' +
 | 
			
		||||
          now.getFullYear() +
 | 
			
		||||
          ' ' +
 | 
			
		||||
          (now.getHours() < 10 ? '0' : '') +
 | 
			
		||||
          now.getHours() +
 | 
			
		||||
          ':' +
 | 
			
		||||
          (now.getMinutes() < 10 ? '0' : '') +
 | 
			
		||||
          now.getMinutes()
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    isStronoEnabled() {
 | 
			
		||||
      return now => {
 | 
			
		||||
        var actual = new Date()
 | 
			
		||||
        return actual - now < 60000
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    locked() {
 | 
			
		||||
      return this.free_drink_list_history_job_credit >= 1000
 | 
			
		||||
    },
 | 
			
		||||
    first_loading() {
 | 
			
		||||
      return (
 | 
			
		||||
        this.loading &&
 | 
			
		||||
        this.free_drink_list_history.length == 0 &&
 | 
			
		||||
        this.free_drink_list_config_band.length == 0 &&
 | 
			
		||||
        this.free_drink_list_config_job.length == 0
 | 
			
		||||
      )
 | 
			
		||||
    },
 | 
			
		||||
    free_drink_list_history_band_without_canceled_price() {
 | 
			
		||||
      let sum = 0
 | 
			
		||||
      this.free_drink_list_history_band_without_canceled.forEach(item => {
 | 
			
		||||
        sum += item.free_drink_config.price
 | 
			
		||||
      })
 | 
			
		||||
      return sum
 | 
			
		||||
    },
 | 
			
		||||
    free_drink_list_history_bar() {
 | 
			
		||||
      return this.free_drink_list_history.filter(item => {
 | 
			
		||||
        return item.free_drink_type.id == 1 || item.free_drink_type.id == 3
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    clearInterval(this.timer)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.drinkBtn {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
.drinkCol {
 | 
			
		||||
  padding: 6px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.title {
 | 
			
		||||
  width: calc(100% - 135px);
 | 
			
		||||
  min-width: 150px;
 | 
			
		||||
  font-size: 1.25rem !important;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  line-height: 2rem;
 | 
			
		||||
  letter-spacing: 0.0125em !important;
 | 
			
		||||
  font-family: 'Roboto', sans-serif !important;
 | 
			
		||||
}
 | 
			
		||||
.menuBtn {
 | 
			
		||||
  right: 15px;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
}
 | 
			
		||||
.history-item {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <v-list>
 | 
			
		||||
    <v-list-item link :to="{name: 'geruecht'}">
 | 
			
		||||
      <v-list-item-icon>
 | 
			
		||||
        <v-icon>{{ glass_mug_variant }}</v-icon>
 | 
			
		||||
      </v-list-item-icon>
 | 
			
		||||
      <v-list-item-title>
 | 
			
		||||
        Geruecht
 | 
			
		||||
      </v-list-item-title>
 | 
			
		||||
    </v-list-item>
 | 
			
		||||
    <v-list-item link :to="{name: 'baruserFreedrinks'}">
 | 
			
		||||
      <v-list-item-icon>
 | 
			
		||||
        <v-icon>{{ beer }}</v-icon>
 | 
			
		||||
      </v-list-item-icon>
 | 
			
		||||
      <v-list-item-title>
 | 
			
		||||
        Freigetränke
 | 
			
		||||
      </v-list-item-title>
 | 
			
		||||
    </v-list-item>
 | 
			
		||||
  </v-list>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mdiGlassMugVariant,mdiBeer } from '@mdi/js'
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'BarNavigation',
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      glass_mug_variant: mdiGlassMugVariant,
 | 
			
		||||
      beer: mdiBeer
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,595 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <v-dialog v-model="checkValidate" max-width="290">
 | 
			
		||||
      <v-card>
 | 
			
		||||
        <v-card-title>
 | 
			
		||||
          Willst du wirklich??
 | 
			
		||||
        </v-card-title>
 | 
			
		||||
        <v-card-text v-if="stornoMessage">
 | 
			
		||||
          Willst du wirklich den Betrag
 | 
			
		||||
          {{ (stornoMessage.amount / 100).toFixed(2) }}€ von
 | 
			
		||||
          {{ stornoMessage.user.firstname }}
 | 
			
		||||
          {{ stornoMessage.user.lastname }} stornieren?
 | 
			
		||||
        </v-card-text>
 | 
			
		||||
        <v-card-actions>
 | 
			
		||||
          <v-spacer />
 | 
			
		||||
          <v-btn text @click="cancelStorno">Abbrechen</v-btn>
 | 
			
		||||
          <v-btn text @click="acceptStorno">Stornieren</v-btn>
 | 
			
		||||
        </v-card-actions>
 | 
			
		||||
      </v-card>
 | 
			
		||||
    </v-dialog>
 | 
			
		||||
    <v-dialog v-model="dialog" max-width="290">
 | 
			
		||||
      <v-card>
 | 
			
		||||
        <v-card-title class="headline"
 | 
			
		||||
          >Transaktion ist länger als 1 Minute her!</v-card-title
 | 
			
		||||
        >
 | 
			
		||||
        <v-card-text>
 | 
			
		||||
          Da die Transaktion länger als 1 Minuter her ist, kann eine Stornierung
 | 
			
		||||
          nicht durchgeführt werden. Wende dich bitte an den Finanzer.
 | 
			
		||||
        </v-card-text>
 | 
			
		||||
        <v-card-actions>
 | 
			
		||||
          <v-spacer />
 | 
			
		||||
          <v-btn text @click="dialog = false">
 | 
			
		||||
            Verstanden
 | 
			
		||||
          </v-btn>
 | 
			
		||||
        </v-card-actions>
 | 
			
		||||
      </v-card>
 | 
			
		||||
    </v-dialog>
 | 
			
		||||
    <v-dialog
 | 
			
		||||
      v-if="overLimitUser"
 | 
			
		||||
      v-model="overLimitUser"
 | 
			
		||||
      max-width="290"
 | 
			
		||||
      persistent
 | 
			
		||||
    >
 | 
			
		||||
      <v-card>
 | 
			
		||||
        <v-card-title>Warnung</v-card-title>
 | 
			
		||||
        <v-card-text>
 | 
			
		||||
          {{ overLimitUser.firstname }} {{ overLimitUser.lastname }} übersteigt
 | 
			
		||||
          das Anschreibelimit von
 | 
			
		||||
          {{ (overLimitUser.limit / 100).toFixed(2) }} €. Danach kann dieses
 | 
			
		||||
          Mitglied nichts mehr anschreiben. Will er das wirklich?
 | 
			
		||||
        </v-card-text>
 | 
			
		||||
        <v-card-actions>
 | 
			
		||||
          <v-spacer />
 | 
			
		||||
          <v-btn text @click="cancel()">Abbrechen</v-btn>
 | 
			
		||||
          <v-btn text @click="continueAdd(overLimitUser)">Anschreiben</v-btn>
 | 
			
		||||
        </v-card-actions>
 | 
			
		||||
      </v-card>
 | 
			
		||||
    </v-dialog>
 | 
			
		||||
    <v-dialog
 | 
			
		||||
      v-if="overOverLimit"
 | 
			
		||||
      v-model="overOverLimit"
 | 
			
		||||
      max-width="290"
 | 
			
		||||
      persistent
 | 
			
		||||
    >
 | 
			
		||||
      <v-card>
 | 
			
		||||
        <v-card-title>Anschreiben nicht möglich</v-card-title>
 | 
			
		||||
        <v-card-text>
 | 
			
		||||
          {{ overOverLimit.firstname }}
 | 
			
		||||
          {{ overOverLimit.lastname }} überschreitet das Anschreibelimit zuviel.
 | 
			
		||||
          Das Anschreiben wurde daher gestoppt und zurückgesetzt.
 | 
			
		||||
        </v-card-text>
 | 
			
		||||
        <v-card-actions>
 | 
			
		||||
          <v-btn text @click="overOverLimit = null">Verstanden</v-btn>
 | 
			
		||||
        </v-card-actions>
 | 
			
		||||
      </v-card>
 | 
			
		||||
    </v-dialog>
 | 
			
		||||
    <v-progress-linear v-if="loading && users.length !== 0" indeterminate />
 | 
			
		||||
    <v-container>
 | 
			
		||||
      <AddAmountSkeleton v-if="loading && users.length === 0" />
 | 
			
		||||
    </v-container>
 | 
			
		||||
    <v-navigation-drawer v-model="menu" right app clipped>
 | 
			
		||||
      <v-list-item-group :key="componentRenderer">
 | 
			
		||||
        <v-list-item inactive>
 | 
			
		||||
          <v-list-item-title class="headline">
 | 
			
		||||
            Verlauf
 | 
			
		||||
          </v-list-item-title>
 | 
			
		||||
        </v-list-item>
 | 
			
		||||
        <v-divider />
 | 
			
		||||
        <div
 | 
			
		||||
          v-for="message in messages"
 | 
			
		||||
          three-line
 | 
			
		||||
          :key="messages.indexOf(message)"
 | 
			
		||||
        >
 | 
			
		||||
          <v-list-item three-line inactive @click="storno(message)">
 | 
			
		||||
            <v-list-item-content>
 | 
			
		||||
              <v-progress-linear indeterminate v-if="message.loading" />
 | 
			
		||||
              <v-list-item-title>{{ now(message.date) }}</v-list-item-title>
 | 
			
		||||
              <v-list-item-subtitle>{{
 | 
			
		||||
                createMessage(message)
 | 
			
		||||
              }}</v-list-item-subtitle>
 | 
			
		||||
              <v-list-item-subtitle class="red--text" v-if="message.storno">
 | 
			
		||||
                STORNIERT!!!
 | 
			
		||||
              </v-list-item-subtitle>
 | 
			
		||||
              <v-list-item-subtitle class="red--text" v-else-if="message.error">
 | 
			
		||||
                ERROR!
 | 
			
		||||
              </v-list-item-subtitle>
 | 
			
		||||
              <v-list-item-action-text v-if="under5minutes(message.date)"
 | 
			
		||||
                >Klicken um zu Stornieren
 | 
			
		||||
              </v-list-item-action-text>
 | 
			
		||||
            </v-list-item-content>
 | 
			
		||||
          </v-list-item>
 | 
			
		||||
        </div>
 | 
			
		||||
      </v-list-item-group>
 | 
			
		||||
    </v-navigation-drawer>
 | 
			
		||||
    <div v-for="user in users" :key="users.indexOf(user)">
 | 
			
		||||
      <div v-if="isFiltered(user) && calcLastSeen(user)">
 | 
			
		||||
        <v-container>
 | 
			
		||||
          <v-card :loading="user.loading">
 | 
			
		||||
            <v-card-title>
 | 
			
		||||
              <v-list-item-title class="title"
 | 
			
		||||
                >{{ user.firstname }} {{ user.lastname }}</v-list-item-title
 | 
			
		||||
              >
 | 
			
		||||
            </v-card-title>
 | 
			
		||||
            <v-card-subtitle v-if="user.limit + user.amount > 0">
 | 
			
		||||
              Nur noch {{ ((user.limit + user.amount) / 100).toFixed(2) }} €
 | 
			
		||||
              übrig!!
 | 
			
		||||
            </v-card-subtitle>
 | 
			
		||||
            <v-card-text>
 | 
			
		||||
              <v-row v-if="!user.locked">
 | 
			
		||||
                <v-col cols="10">
 | 
			
		||||
                  <v-row>
 | 
			
		||||
                    <v-col cols="6" xs="5" sm="4">
 | 
			
		||||
                      <v-btn
 | 
			
		||||
                        class="creditBtn"
 | 
			
		||||
                        block
 | 
			
		||||
                        @click="addingAmount(user, 200)"
 | 
			
		||||
                        :color="color"
 | 
			
		||||
                        :disabled="user.locked"
 | 
			
		||||
                        >2 €
 | 
			
		||||
                      </v-btn>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col cols="6" xs="5" sm="4">
 | 
			
		||||
                      <v-btn
 | 
			
		||||
                        class="creditBtn"
 | 
			
		||||
                        block
 | 
			
		||||
                        @click="addingAmount(user, 100)"
 | 
			
		||||
                        :color="color"
 | 
			
		||||
                        :disabled="user.locked"
 | 
			
		||||
                        >1 €
 | 
			
		||||
                      </v-btn>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col cols="6" xs="5" sm="4">
 | 
			
		||||
                      <v-btn
 | 
			
		||||
                        class="creditBtn"
 | 
			
		||||
                        block
 | 
			
		||||
                        @click="addingAmount(user, 50)"
 | 
			
		||||
                        :color="color"
 | 
			
		||||
                        :disabled="user.locked"
 | 
			
		||||
                        >0,50 €
 | 
			
		||||
                      </v-btn>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col cols="6" xs="5" sm="4">
 | 
			
		||||
                      <v-btn
 | 
			
		||||
                        class="creditBtn"
 | 
			
		||||
                        block
 | 
			
		||||
                        @click="addingAmount(user, 40)"
 | 
			
		||||
                        :color="color"
 | 
			
		||||
                        :disabled="user.locked"
 | 
			
		||||
                        >0,40 €
 | 
			
		||||
                      </v-btn>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col cols="6" xs="5" sm="4">
 | 
			
		||||
                      <v-btn
 | 
			
		||||
                        class="creditBtn"
 | 
			
		||||
                        block
 | 
			
		||||
                        @click="addingAmount(user, 20)"
 | 
			
		||||
                        :color="color"
 | 
			
		||||
                        :disabled="user.locked"
 | 
			
		||||
                        >0,20 €
 | 
			
		||||
                      </v-btn>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col cols="6" xs="5" sm="4">
 | 
			
		||||
                      <v-btn
 | 
			
		||||
                        class="creditBtn"
 | 
			
		||||
                        block
 | 
			
		||||
                        @click="addingAmount(user, 10)"
 | 
			
		||||
                        :color="color"
 | 
			
		||||
                        :disabled="user.locked"
 | 
			
		||||
                        >0,10 €
 | 
			
		||||
                      </v-btn>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col cols="8">
 | 
			
		||||
                      <v-text-field
 | 
			
		||||
                        outlined
 | 
			
		||||
                        type="number"
 | 
			
		||||
                        v-model="user.value"
 | 
			
		||||
                        label="Benutzerdefinierter Betrag"
 | 
			
		||||
                        :disabled="user.locked"
 | 
			
		||||
                      ></v-text-field>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col cols="4">
 | 
			
		||||
                      <v-btn
 | 
			
		||||
                        fab
 | 
			
		||||
                        :color="color"
 | 
			
		||||
                        @click="addAmountMore(user)"
 | 
			
		||||
                        :disabled="user.locked"
 | 
			
		||||
                      >
 | 
			
		||||
                        <v-icon>{{ plus }}</v-icon>
 | 
			
		||||
                      </v-btn>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                  </v-row>
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col align-self="center">
 | 
			
		||||
                  <v-row>
 | 
			
		||||
                    <v-list-item>
 | 
			
		||||
                      <v-list-item-content class="text-center">
 | 
			
		||||
                        <v-list-item-action-text :class="getColor(user.type)"
 | 
			
		||||
                          >{{ (user.amount / 100).toFixed(2) }}
 | 
			
		||||
                          €
 | 
			
		||||
                        </v-list-item-action-text>
 | 
			
		||||
                        <v-list-item-action-text v-if="user.toSetAmount">
 | 
			
		||||
                          - {{ (user.toSetAmount / 100).toFixed(2) }}
 | 
			
		||||
                        </v-list-item-action-text>
 | 
			
		||||
                      </v-list-item-content>
 | 
			
		||||
                    </v-list-item>
 | 
			
		||||
                  </v-row>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
              <v-row>
 | 
			
		||||
                <v-col class="hidden-sm-and-down" cols="80">
 | 
			
		||||
                  <v-alert v-if="user.locked" type="error"
 | 
			
		||||
                    >{{ user.firstname }} darf nicht mehr anschreiben.
 | 
			
		||||
                    {{ user.firstname }} sollte sich lieber mal beim Finanzer
 | 
			
		||||
                    melden.
 | 
			
		||||
                  </v-alert>
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col align-self="center" v-if="user.locked">
 | 
			
		||||
                  <v-row>
 | 
			
		||||
                    <v-list-item>
 | 
			
		||||
                      <v-list-item-content class="text-center">
 | 
			
		||||
                        <v-list-item-action-text :class="getColor(user.type)"
 | 
			
		||||
                          >{{ (user.amount / 100).toFixed(2) }}
 | 
			
		||||
                          €
 | 
			
		||||
                        </v-list-item-action-text>
 | 
			
		||||
                      </v-list-item-content>
 | 
			
		||||
                    </v-list-item>
 | 
			
		||||
                  </v-row>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
            </v-card-text>
 | 
			
		||||
          </v-card>
 | 
			
		||||
          <v-snackbar
 | 
			
		||||
            :color="
 | 
			
		||||
              messages.length > 0
 | 
			
		||||
                ? messages[0].error
 | 
			
		||||
                  ? 'error'
 | 
			
		||||
                  : 'success'
 | 
			
		||||
                : 'success'
 | 
			
		||||
            "
 | 
			
		||||
            bottom
 | 
			
		||||
            :timeout="0"
 | 
			
		||||
            :multi-line="true"
 | 
			
		||||
            :value="messages.length > 0 ? messages[0].visible : test"
 | 
			
		||||
            vertical
 | 
			
		||||
          >
 | 
			
		||||
            <v-list-item
 | 
			
		||||
              v-for="message in messages"
 | 
			
		||||
              :key="messages.indexOf(message)"
 | 
			
		||||
              :style="
 | 
			
		||||
                message.error
 | 
			
		||||
                  ? 'background-color: #FF5252;'
 | 
			
		||||
                  : 'background-color: #4CAF50;'
 | 
			
		||||
              "
 | 
			
		||||
              v-show="message.visible"
 | 
			
		||||
            >
 | 
			
		||||
              <v-list-item-content>
 | 
			
		||||
                <v-list-item-title style="color: white">
 | 
			
		||||
                  {{ createMessage(message) }}
 | 
			
		||||
                </v-list-item-title>
 | 
			
		||||
              </v-list-item-content>
 | 
			
		||||
              <v-list-item-action v-if="message.error">
 | 
			
		||||
                <v-btn icon @click="message.visible = false">
 | 
			
		||||
                  <v-icon color="white">
 | 
			
		||||
                    mdi-close
 | 
			
		||||
                  </v-icon>
 | 
			
		||||
                </v-btn>
 | 
			
		||||
              </v-list-item-action>
 | 
			
		||||
            </v-list-item>
 | 
			
		||||
          </v-snackbar>
 | 
			
		||||
        </v-container>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapGetters, mapActions } from 'vuex'
 | 
			
		||||
import { mdiPlus } from '@mdi/js'
 | 
			
		||||
import AddAmountSkeleton from '../user/Skeleton/AddAmountSkeleton'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'CreditLists',
 | 
			
		||||
  components: { AddAmountSkeleton },
 | 
			
		||||
  props: {},
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      plus: mdiPlus,
 | 
			
		||||
      value: null,
 | 
			
		||||
      color: 'green accent-4',
 | 
			
		||||
      menu: true,
 | 
			
		||||
      dialog: false,
 | 
			
		||||
      componentRenderer: 0,
 | 
			
		||||
      timer: '',
 | 
			
		||||
      stornoMessage: null,
 | 
			
		||||
      checkValidate: false,
 | 
			
		||||
      test: null,
 | 
			
		||||
      overLimitUser: null,
 | 
			
		||||
      overOverLimit: null
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.menu = this.menu_from_store
 | 
			
		||||
    this.getUsers()
 | 
			
		||||
    this.timer = setInterval(this.forceRender, 1000)
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions({
 | 
			
		||||
      addAmount: 'barUsers/addAmount',
 | 
			
		||||
      getUsers: 'barUsers/getUsers',
 | 
			
		||||
      deactivate: 'barUsers/deactivateMenu',
 | 
			
		||||
      commitStorno: 'barUsers/storno'
 | 
			
		||||
    }),
 | 
			
		||||
    continueAdd(user) {
 | 
			
		||||
      this.overLimitUser = null
 | 
			
		||||
      user.checkedOverLimit = true
 | 
			
		||||
      if (user.value) {
 | 
			
		||||
        this.addAmount({
 | 
			
		||||
          username: user.username,
 | 
			
		||||
          amount: Math.round(Math.abs(user.value * 100)),
 | 
			
		||||
          user: user
 | 
			
		||||
        })
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          user.value = null
 | 
			
		||||
          user.toSetAmount = null
 | 
			
		||||
        }, 300)
 | 
			
		||||
      } else {
 | 
			
		||||
        user.timeout = setTimeout(() => {
 | 
			
		||||
          this.addAmount({
 | 
			
		||||
            username: user.username,
 | 
			
		||||
            amount: user.toSetAmount,
 | 
			
		||||
            user: user
 | 
			
		||||
          })
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            user.toSetAmount = null
 | 
			
		||||
          }, 300)
 | 
			
		||||
        }, 2000)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    cancel() {
 | 
			
		||||
      this.overLimitUser.toSetAmount = null
 | 
			
		||||
      this.overLimitUser.value = null
 | 
			
		||||
      this.overLimitUser = null
 | 
			
		||||
    },
 | 
			
		||||
    checkOverLimitIsValid(user) {
 | 
			
		||||
      console.log(user)
 | 
			
		||||
      if (user.toSetAmount && user.autoLock) {
 | 
			
		||||
        if (
 | 
			
		||||
          (user.amount - Number.parseInt(user.toSetAmount)) <
 | 
			
		||||
            -(user.limit + 500)
 | 
			
		||||
        ) {
 | 
			
		||||
          this.overOverLimit = user
 | 
			
		||||
          user.toSetAmount = null
 | 
			
		||||
          user.value = null
 | 
			
		||||
          return false
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return true
 | 
			
		||||
    },
 | 
			
		||||
    checkOverLimit(user) {
 | 
			
		||||
      console.log(user)
 | 
			
		||||
      if (user.toSetAmount) {
 | 
			
		||||
        if ((user.amount - user.toSetAmount) < -user.limit) {
 | 
			
		||||
          return user.checkedOverLimit ? false : true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return false
 | 
			
		||||
    },
 | 
			
		||||
    addingAmount(user, amount) {
 | 
			
		||||
      clearTimeout(user.timeout)
 | 
			
		||||
      user.toSetAmount = user.toSetAmount ? user.toSetAmount + amount : amount
 | 
			
		||||
      if (this.checkOverLimitIsValid(user)) {
 | 
			
		||||
        if (this.checkOverLimit(user) && user.autoLock) {
 | 
			
		||||
          this.overLimitUser = user
 | 
			
		||||
        } else {
 | 
			
		||||
          user.timeout = setTimeout(() => {
 | 
			
		||||
            this.addAmount({
 | 
			
		||||
              username: user.username,
 | 
			
		||||
              amount: user.toSetAmount,
 | 
			
		||||
              user: user
 | 
			
		||||
            })
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              user.toSetAmount = null
 | 
			
		||||
            }, 300)
 | 
			
		||||
          }, 2000)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    forceRender() {
 | 
			
		||||
      this.componentRenderer += 1
 | 
			
		||||
    },
 | 
			
		||||
    getColor(type) {
 | 
			
		||||
      return type === 'credit' ? 'title green--text' : 'title red--text'
 | 
			
		||||
    },
 | 
			
		||||
    isFiltered(user) {
 | 
			
		||||
      try {
 | 
			
		||||
        var filters = this.filter.split(' ')
 | 
			
		||||
        if (filters.length === 1) {
 | 
			
		||||
          if (
 | 
			
		||||
            user.firstname.toLowerCase().includes(filters[0].toLowerCase()) ||
 | 
			
		||||
            user.lastname.toLowerCase().includes(filters[0].toLowerCase())
 | 
			
		||||
          ) {
 | 
			
		||||
            return true
 | 
			
		||||
          }
 | 
			
		||||
        } else if (filters.length > 1) {
 | 
			
		||||
          if (
 | 
			
		||||
            user.firstname.toLowerCase().includes(filters[0].toLowerCase()) &&
 | 
			
		||||
            user.lastname.toLowerCase().includes(filters[1].toLowerCase())
 | 
			
		||||
          ) {
 | 
			
		||||
            return true
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        return true
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    addAmountMore(user) {
 | 
			
		||||
      user.toSetAmount = user.toSetAmount
 | 
			
		||||
        ? user.toSetAmount + Math.round(Math.abs(user.value * 100))
 | 
			
		||||
        : Math.round(Math.abs(user.value * 100))
 | 
			
		||||
      if (this.checkOverLimitIsValid(user)) {
 | 
			
		||||
        if (this.checkOverLimit(user) && user.autoLock) {
 | 
			
		||||
          this.overLimitUser = user
 | 
			
		||||
        } else {
 | 
			
		||||
          this.addAmount({
 | 
			
		||||
            username: user.username,
 | 
			
		||||
            amount: Math.round(Math.abs(user.value * 100)),
 | 
			
		||||
            user: user
 | 
			
		||||
          })
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            user.value = null
 | 
			
		||||
            user.toSetAmount = null
 | 
			
		||||
          }, 300)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    storno(message) {
 | 
			
		||||
      if (!message.error) {
 | 
			
		||||
        if (!this.under5minutes(message.date)) this.dialog = true
 | 
			
		||||
        else {
 | 
			
		||||
          this.checkValidate = true
 | 
			
		||||
          this.stornoMessage = message
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    acceptStorno() {
 | 
			
		||||
      this.commitStorno({
 | 
			
		||||
        username: this.stornoMessage.user.username,
 | 
			
		||||
        amount: this.stornoMessage.amount,
 | 
			
		||||
        date: this.stornoMessage.date
 | 
			
		||||
      })
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        this.cancelStorno()
 | 
			
		||||
      }, 300)
 | 
			
		||||
    },
 | 
			
		||||
    cancelStorno() {
 | 
			
		||||
      this.stornoMessage = null
 | 
			
		||||
      this.checkValidate = null
 | 
			
		||||
    },
 | 
			
		||||
    createMessage(message) {
 | 
			
		||||
      var text = ''
 | 
			
		||||
      if (message.error) {
 | 
			
		||||
        text =
 | 
			
		||||
          'ERROR: Konnte ' +
 | 
			
		||||
          (message.amount / 100).toFixed(2) +
 | 
			
		||||
          '€ nicht zu ' +
 | 
			
		||||
          message.user.firstname +
 | 
			
		||||
          ' ' +
 | 
			
		||||
          message.user.lastname +
 | 
			
		||||
          ' hinzufügen.'
 | 
			
		||||
      } else {
 | 
			
		||||
        text =
 | 
			
		||||
          '' +
 | 
			
		||||
          (message.amount / 100).toFixed(2) +
 | 
			
		||||
          '€ wurde zu ' +
 | 
			
		||||
          message.user.firstname +
 | 
			
		||||
          ' ' +
 | 
			
		||||
          message.user.lastname +
 | 
			
		||||
          ' hinzugefügt.'
 | 
			
		||||
      }
 | 
			
		||||
      return text
 | 
			
		||||
    },
 | 
			
		||||
    calcLastSeen(user) {
 | 
			
		||||
      if (user.last_seen) {
 | 
			
		||||
        let date = new Date()
 | 
			
		||||
        if ((date - user.last_seen) / 1000 / 60 / 60 < 72) {
 | 
			
		||||
          return true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters({
 | 
			
		||||
      users: 'barUsers/users',
 | 
			
		||||
      filter: 'barUsers/filter',
 | 
			
		||||
      loading: 'barUsers/usersLoading',
 | 
			
		||||
      messages: 'barUsers/messages',
 | 
			
		||||
      menu_from_store: 'barUsers/menu'
 | 
			
		||||
    }),
 | 
			
		||||
    under5minutes() {
 | 
			
		||||
      return now => {
 | 
			
		||||
        var actual = new Date()
 | 
			
		||||
        return actual - now < 60000
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    now() {
 | 
			
		||||
      return now => {
 | 
			
		||||
        var actual = new Date()
 | 
			
		||||
        var zero = new Date(0)
 | 
			
		||||
        var date = new Date(actual - now)
 | 
			
		||||
        if (date.getFullYear() === zero.getFullYear()) {
 | 
			
		||||
          if (date.getMonth() === zero.getMonth()) {
 | 
			
		||||
            if (date.getDate() === zero.getDate()) {
 | 
			
		||||
              if (date.getHours() === zero.getDate()) {
 | 
			
		||||
                if (date.getMinutes() < 1) {
 | 
			
		||||
                  return 'vor ' + date.getSeconds() + ' Sekunden'
 | 
			
		||||
                } else if (date.getMinutes() < 10) {
 | 
			
		||||
                  return 'vor ' + date.getMinutes() + ' Minuten'
 | 
			
		||||
                } else {
 | 
			
		||||
                  return (
 | 
			
		||||
                    (now.getHours() < 10 ? '0' : '') +
 | 
			
		||||
                    now.getHours() +
 | 
			
		||||
                    ':' +
 | 
			
		||||
                    (now.getMinutes() < 10 ? '0' : '') +
 | 
			
		||||
                    now.getMinutes()
 | 
			
		||||
                  )
 | 
			
		||||
                }
 | 
			
		||||
              } else {
 | 
			
		||||
                return (
 | 
			
		||||
                  (now.getHours() < 10 ? '0' : '') +
 | 
			
		||||
                  now.getHours() +
 | 
			
		||||
                  ':' +
 | 
			
		||||
                  (now.getMinutes() < 10 ? '0' : '') +
 | 
			
		||||
                  now.getMinutes()
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return (
 | 
			
		||||
          now.getDate() +
 | 
			
		||||
          '.' +
 | 
			
		||||
          now.getMonth() +
 | 
			
		||||
          '.' +
 | 
			
		||||
          now.getFullYear() +
 | 
			
		||||
          ' ' +
 | 
			
		||||
          (now.getHours() < 10 ? '0' : '') +
 | 
			
		||||
          now.getHours() +
 | 
			
		||||
          ':' +
 | 
			
		||||
          (now.getMinutes() < 10 ? '0' : '') +
 | 
			
		||||
          now.getMinutes()
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    menu(newValue) {
 | 
			
		||||
      if (!newValue) this.deactivate()
 | 
			
		||||
    },
 | 
			
		||||
    menu_from_store() {
 | 
			
		||||
      this.menu = this.menu_from_store
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    clearInterval(this.timer)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.creditBtn {
 | 
			
		||||
  margin: 2px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,133 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <v-toolbar>
 | 
			
		||||
      <v-spacer />
 | 
			
		||||
      <v-toolbar-items>
 | 
			
		||||
        <v-autocomplete
 | 
			
		||||
          outlined
 | 
			
		||||
          return-object
 | 
			
		||||
          v-model="user"
 | 
			
		||||
          style="margin-top: 3px"
 | 
			
		||||
          placeholder="Suche Person"
 | 
			
		||||
          :items="allUsers"
 | 
			
		||||
          item-text="fullName"
 | 
			
		||||
          full-width
 | 
			
		||||
          :loading="loading"
 | 
			
		||||
          :search-input.sync="filter"
 | 
			
		||||
          clearable
 | 
			
		||||
        >
 | 
			
		||||
          <template v-slot:prepend-inner>
 | 
			
		||||
            <v-icon>{{ search_person }}</v-icon>
 | 
			
		||||
          </template>
 | 
			
		||||
          <template v-slot:item="data">
 | 
			
		||||
            <v-list-item-icon v-if="getLocked(data.item)">
 | 
			
		||||
              <v-icon>mdi-alert</v-icon>
 | 
			
		||||
            </v-list-item-icon>
 | 
			
		||||
            <v-list-item-content>
 | 
			
		||||
              {{data.item.fullName}}
 | 
			
		||||
              <v-spacer/>
 | 
			
		||||
              {{(getCredit(data.item)/100).toFixed(2)}} €
 | 
			
		||||
            </v-list-item-content>
 | 
			
		||||
          </template>
 | 
			
		||||
        </v-autocomplete>
 | 
			
		||||
        <v-btn text @click="addUser">Hinzufügen</v-btn>
 | 
			
		||||
        <v-btn v-if="!locked" text @click="lock">Sperren</v-btn>
 | 
			
		||||
        <v-btn v-else text @click="overlay = true">Entsperren</v-btn>
 | 
			
		||||
        <v-btn @click="clickMenu" icon>
 | 
			
		||||
          <v-icon>{{ menuIcon }}</v-icon>
 | 
			
		||||
        </v-btn>
 | 
			
		||||
      </v-toolbar-items>
 | 
			
		||||
    </v-toolbar>
 | 
			
		||||
    <v-dialog v-model="overlay">
 | 
			
		||||
        <v-card>
 | 
			
		||||
          <v-card-title>Entsperre Baransicht</v-card-title>
 | 
			
		||||
          <v-card-text>
 | 
			
		||||
            <v-text-field outlined type="password" label="Passwort" v-model="password"></v-text-field>
 | 
			
		||||
          </v-card-text>
 | 
			
		||||
          <v-card-actions>
 | 
			
		||||
            <v-spacer/>
 | 
			
		||||
            <v-btn text @click="overlay = false">Abbrechen</v-btn>
 | 
			
		||||
            <v-btn text @click="doUnlock">Entsperren</v-btn>
 | 
			
		||||
          </v-card-actions>
 | 
			
		||||
        </v-card>
 | 
			
		||||
    </v-dialog>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapGetters, mapActions } from 'vuex'
 | 
			
		||||
import { mdiAccountSearch, mdiMenu, mdiAlert } from '@mdi/js'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'SearchBar',
 | 
			
		||||
  props: {},
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      user: null,
 | 
			
		||||
      filter: '',
 | 
			
		||||
      search_person: mdiAccountSearch,
 | 
			
		||||
      menuIcon: mdiMenu,
 | 
			
		||||
      alert: mdiAlert,
 | 
			
		||||
      overlay: false,
 | 
			
		||||
      password: ''
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.getAllUsers()
 | 
			
		||||
    this.getLocked()
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions({
 | 
			
		||||
      getAllUsers: 'barUsers/getAllUsers',
 | 
			
		||||
      addCreditList: 'barUsers/addCreditList',
 | 
			
		||||
      setFilter: 'barUsers/setFilter',
 | 
			
		||||
      activateMenu: 'barUsers/activateMenu',
 | 
			
		||||
      deactivateMenu: 'barUsers/deactivateMenu',
 | 
			
		||||
      lock: 'barUsers/setLocked',
 | 
			
		||||
      unlock: 'barUsers/unlock',
 | 
			
		||||
      getLocked: 'barUsers/getLocked'
 | 
			
		||||
    }),
 | 
			
		||||
    addUser() {
 | 
			
		||||
      this.addCreditList(this.user)
 | 
			
		||||
    },
 | 
			
		||||
    clickMenu() {
 | 
			
		||||
      if (this.menu) this.deactivateMenu()
 | 
			
		||||
      else this.activateMenu()
 | 
			
		||||
    },
 | 
			
		||||
    doUnlock() {
 | 
			
		||||
      this.unlock(this.password)
 | 
			
		||||
      this.password = ''
 | 
			
		||||
      this.overlay = false
 | 
			
		||||
    },
 | 
			
		||||
    getCredit(user) {
 | 
			
		||||
      let retUser = this.users.find(item => {
 | 
			
		||||
        return item.username === user.username
 | 
			
		||||
      })
 | 
			
		||||
      return retUser ? retUser.amount : 0
 | 
			
		||||
    },
 | 
			
		||||
    getLocked(user) {
 | 
			
		||||
      if(!user) return 
 | 
			
		||||
      let retUser = this.users.find(item => {
 | 
			
		||||
        return item.username === user.username
 | 
			
		||||
      })
 | 
			
		||||
      return retUser ? retUser.locked : false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters({
 | 
			
		||||
      allUsers: 'barUsers/allUsers',
 | 
			
		||||
      users: 'barUsers/users',
 | 
			
		||||
      loading: 'barUsers/allUsersLoading',
 | 
			
		||||
      menu: 'barUsers/menu',
 | 
			
		||||
      locked: 'barUsers/locked'
 | 
			
		||||
    })
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    filter(val) {
 | 
			
		||||
      this.setFilter(val)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,49 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <v-list>
 | 
			
		||||
      <v-list-item class="title" link :to="{name: 'overview'}">
 | 
			
		||||
        <v-list-item-icon>
 | 
			
		||||
          <v-icon>{{home}}</v-icon>
 | 
			
		||||
        </v-list-item-icon>
 | 
			
		||||
        <v-list-item-title>Gesamtübersicht</v-list-item-title>
 | 
			
		||||
      </v-list-item>
 | 
			
		||||
    </v-list>
 | 
			
		||||
    <v-divider />
 | 
			
		||||
    <v-list>
 | 
			
		||||
      <div v-for="user in users" v-bind:key="users.indexOf(user)">
 | 
			
		||||
        <v-list-item
 | 
			
		||||
          :to="{ name: 'activeUser', params: { id: user.username } }"
 | 
			
		||||
          link
 | 
			
		||||
        >
 | 
			
		||||
          <v-list-item-title
 | 
			
		||||
            >{{ user.lastname }}, {{ user.firstname }}</v-list-item-title
 | 
			
		||||
          >
 | 
			
		||||
        </v-list-item>
 | 
			
		||||
      </div>
 | 
			
		||||
      <v-list-item>
 | 
			
		||||
        <v-progress-circular indeterminate color="grey" v-if="loading" />
 | 
			
		||||
      </v-list-item>
 | 
			
		||||
    </v-list>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapGetters } from 'vuex'
 | 
			
		||||
import {mdiHome} from '@mdi/js'
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'FinanzerNavigation',
 | 
			
		||||
  data () {
 | 
			
		||||
    return {
 | 
			
		||||
      home: mdiHome
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters({
 | 
			
		||||
      users: 'finanzerUsers/users',
 | 
			
		||||
      loading: 'finanzerUsers/usersLoading'
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,400 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <v-content>
 | 
			
		||||
    <v-toolbar tile>
 | 
			
		||||
      <v-toolbar-title>Gesamtübersicht</v-toolbar-title>
 | 
			
		||||
      <v-spacer />
 | 
			
		||||
      <v-toolbar-items>
 | 
			
		||||
        <v-btn text icon @click="countYear(false)">
 | 
			
		||||
          <v-icon>{{keyboard_arrow_left}}</v-icon>
 | 
			
		||||
        </v-btn>
 | 
			
		||||
        <v-list-item>
 | 
			
		||||
          <v-list-item-title class="title">{{ year }}</v-list-item-title>
 | 
			
		||||
        </v-list-item>
 | 
			
		||||
        <v-btn text icon @click="countYear(true)" :disabled="isActualYear">
 | 
			
		||||
          <v-icon>{{keyboard_arrow_right}}</v-icon>
 | 
			
		||||
        </v-btn>
 | 
			
		||||
      </v-toolbar-items>
 | 
			
		||||
      <v-spacer />
 | 
			
		||||
      <v-toolbar-items>
 | 
			
		||||
        <v-btn text @click="sendMails">Emails senden</v-btn>
 | 
			
		||||
        <v-autocomplete
 | 
			
		||||
          outlined
 | 
			
		||||
          return-object
 | 
			
		||||
          v-model="user"
 | 
			
		||||
          style="margin-top: 3px"
 | 
			
		||||
          placeholder="Suche Person"
 | 
			
		||||
          :items="allUsers"
 | 
			
		||||
          item-text="fullName"
 | 
			
		||||
          full-width
 | 
			
		||||
          :loading="allUsersLoading"
 | 
			
		||||
          :search-input.sync="filter"
 | 
			
		||||
          @change="addToUser(user)"
 | 
			
		||||
        >
 | 
			
		||||
          <template v-slot:prepend-inner>
 | 
			
		||||
            <v-icon>{{search_person}}</v-icon>
 | 
			
		||||
          </template>
 | 
			
		||||
        </v-autocomplete>
 | 
			
		||||
      </v-toolbar-items>
 | 
			
		||||
    </v-toolbar>
 | 
			
		||||
    <v-expand-transition>
 | 
			
		||||
      <v-card style="margin-top: 3px" v-show="errorMails">
 | 
			
		||||
        <v-row>
 | 
			
		||||
          <v-spacer />
 | 
			
		||||
          <v-btn
 | 
			
		||||
            text
 | 
			
		||||
            icon
 | 
			
		||||
            style="margin-right: 5px"
 | 
			
		||||
            @click="errorExpand ? (errorExpand = false) : (errorExpand = true)"
 | 
			
		||||
          >
 | 
			
		||||
            <v-icon :class="isExpand(errorExpand)" dense>$expand</v-icon>
 | 
			
		||||
          </v-btn>
 | 
			
		||||
        </v-row>
 | 
			
		||||
        <v-expand-transition>
 | 
			
		||||
          <div v-show="errorExpand">
 | 
			
		||||
            <v-alert
 | 
			
		||||
              v-for="error in errorMails"
 | 
			
		||||
              :key="errorMails.indexOf(error)"
 | 
			
		||||
              dense
 | 
			
		||||
              :type="computeError(error.error)"
 | 
			
		||||
              >{{ errorMessage(error) }}</v-alert
 | 
			
		||||
            >
 | 
			
		||||
          </div>
 | 
			
		||||
        </v-expand-transition>
 | 
			
		||||
      </v-card>
 | 
			
		||||
    </v-expand-transition>
 | 
			
		||||
    <v-progress-linear v-if="loading && users.length !== 0" indeterminate />
 | 
			
		||||
    <TableSkeleton v-if="loading && users.length === 0" />
 | 
			
		||||
    <div v-for="user in users" :key="users.indexOf(user)">
 | 
			
		||||
      <v-card
 | 
			
		||||
        v-if="user.creditList[year] && isFiltered(user)"
 | 
			
		||||
        style="margin-top: 3px"
 | 
			
		||||
        :loading="user.loading"
 | 
			
		||||
      >
 | 
			
		||||
        <v-card-title>
 | 
			
		||||
          {{ user.lastname }}, {{ user.firstname }}
 | 
			
		||||
        </v-card-title>
 | 
			
		||||
        <Table v-bind:user="user" v-bind:year="year" />
 | 
			
		||||
        <v-container fluid>
 | 
			
		||||
          <v-row align="start" align-content="start">
 | 
			
		||||
            <v-col>
 | 
			
		||||
              <v-row>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-label>Vorjahr:</v-label>
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-chip
 | 
			
		||||
                    outlined
 | 
			
		||||
                    :text-color="getLastColor(user.creditList[year][1].last)"
 | 
			
		||||
                    >{{
 | 
			
		||||
                      (user.creditList[year][1].last / 100).toFixed(2)
 | 
			
		||||
                    }}</v-chip
 | 
			
		||||
                  >
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
              <v-row>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-label>Gesamt:</v-label>
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-chip
 | 
			
		||||
                    outlined
 | 
			
		||||
                    x-large
 | 
			
		||||
                    :text-color="
 | 
			
		||||
                      getLastColor(
 | 
			
		||||
                        getAllSum(
 | 
			
		||||
                          user.creditList[year][2].sum,
 | 
			
		||||
                          user.creditList[year][1].last
 | 
			
		||||
                        )
 | 
			
		||||
                      )
 | 
			
		||||
                    "
 | 
			
		||||
                  >
 | 
			
		||||
                    {{
 | 
			
		||||
                      (
 | 
			
		||||
                        getAllSum(
 | 
			
		||||
                          user.creditList[year][2].sum,
 | 
			
		||||
                          user.creditList[year][1].last
 | 
			
		||||
                        ) / 100
 | 
			
		||||
                      ).toFixed(2)
 | 
			
		||||
                    }}
 | 
			
		||||
                  </v-chip>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
            </v-col>
 | 
			
		||||
            <v-col align-self="center">
 | 
			
		||||
              <v-row>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-label>Status:</v-label>
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-chip outlined :text-color="getLockedColor(user.locked)">{{
 | 
			
		||||
                    user.locked ? 'Gesperrt' : 'nicht Gesperrt'
 | 
			
		||||
                  }}</v-chip>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
              <v-card outlined>
 | 
			
		||||
                <v-row>
 | 
			
		||||
                  <v-card-title class="subtitle-2"
 | 
			
		||||
                    >Geld transferieren</v-card-title
 | 
			
		||||
                  >
 | 
			
		||||
                  <v-spacer />
 | 
			
		||||
                  <v-btn
 | 
			
		||||
                    text
 | 
			
		||||
                    icon
 | 
			
		||||
                    style="margin-right: 5px"
 | 
			
		||||
                    @click="setExpand(user)"
 | 
			
		||||
                  >
 | 
			
		||||
                    <v-icon :class="isExpand(user.expand)" dense
 | 
			
		||||
                      >$expand</v-icon
 | 
			
		||||
                    >
 | 
			
		||||
                  </v-btn>
 | 
			
		||||
                </v-row>
 | 
			
		||||
                <v-expand-transition>
 | 
			
		||||
                  <v-card-text v-show="user.expand">
 | 
			
		||||
                    <v-form style="margin-left: 15px; margin-right: 15px">
 | 
			
		||||
                      <v-row>
 | 
			
		||||
                        <v-col>
 | 
			
		||||
                          <v-text-field
 | 
			
		||||
                            :rules="[isNumber]"
 | 
			
		||||
                            label="Betrag"
 | 
			
		||||
                            v-model="amount"
 | 
			
		||||
                          ></v-text-field>
 | 
			
		||||
                        </v-col>
 | 
			
		||||
                        <v-col>
 | 
			
		||||
                          <v-select
 | 
			
		||||
                            return-object
 | 
			
		||||
                            v-model="type"
 | 
			
		||||
                            label="Typ"
 | 
			
		||||
                            :items="[
 | 
			
		||||
                              { value: 'amount', text: 'Schulden' },
 | 
			
		||||
                              { value: 'credit', text: 'Guthaben' }
 | 
			
		||||
                            ]"
 | 
			
		||||
                            item-text="text"
 | 
			
		||||
                            item-value="value"
 | 
			
		||||
                          ></v-select>
 | 
			
		||||
                        </v-col>
 | 
			
		||||
                      </v-row>
 | 
			
		||||
                      <v-row>
 | 
			
		||||
                        <v-col>
 | 
			
		||||
                          <v-select
 | 
			
		||||
                            return-object
 | 
			
		||||
                            v-model="selectedYear"
 | 
			
		||||
                            label="Jahr"
 | 
			
		||||
                            :items="years"
 | 
			
		||||
                            item-text="text"
 | 
			
		||||
                            item-value="value"
 | 
			
		||||
                          ></v-select>
 | 
			
		||||
                        </v-col>
 | 
			
		||||
                        <v-col>
 | 
			
		||||
                          <v-select
 | 
			
		||||
                            return-object
 | 
			
		||||
                            v-model="selectedMonth"
 | 
			
		||||
                            label="Monat"
 | 
			
		||||
                            :items="months"
 | 
			
		||||
                            item-text="text"
 | 
			
		||||
                            item-value="value"
 | 
			
		||||
                          ></v-select>
 | 
			
		||||
                        </v-col>
 | 
			
		||||
                      </v-row>
 | 
			
		||||
                    </v-form>
 | 
			
		||||
                    <v-btn block @click="add(user)">Hinzufügen</v-btn>
 | 
			
		||||
                  </v-card-text>
 | 
			
		||||
                </v-expand-transition>
 | 
			
		||||
              </v-card>
 | 
			
		||||
            </v-col>
 | 
			
		||||
          </v-row>
 | 
			
		||||
        </v-container>
 | 
			
		||||
      </v-card>
 | 
			
		||||
    </div>
 | 
			
		||||
  </v-content>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import Table from './Table'
 | 
			
		||||
import { mapGetters, mapActions } from 'vuex'
 | 
			
		||||
import TableSkeleton from './Skeleton/TableSkeleton'
 | 
			
		||||
import {mdiChevronLeft, mdiChevronRight, mdiAccountSearch} from '@mdi/js'
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'Overview',
 | 
			
		||||
  components: { TableSkeleton, Table },
 | 
			
		||||
  props: {},
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      keyboard_arrow_left: mdiChevronLeft,
 | 
			
		||||
      keyboard_arrow_right: mdiChevronRight,
 | 
			
		||||
      search_person: mdiAccountSearch,
 | 
			
		||||
      errorExpand: false,
 | 
			
		||||
 | 
			
		||||
      filter: '',
 | 
			
		||||
      user: null,
 | 
			
		||||
 | 
			
		||||
      amount: null,
 | 
			
		||||
      isNumber: value => !isNaN(value) || 'Betrag muss eine Zahl sein.',
 | 
			
		||||
      type: { value: 'credit', text: 'Guthaben' },
 | 
			
		||||
      selectedYear: {
 | 
			
		||||
        value: new Date().getFullYear(),
 | 
			
		||||
        text: new Date().getFullYear()
 | 
			
		||||
      },
 | 
			
		||||
      selectedMonth: {
 | 
			
		||||
        value: new Date().getMonth() + 1,
 | 
			
		||||
        text: [
 | 
			
		||||
          'Januar',
 | 
			
		||||
          'Februar',
 | 
			
		||||
          'März',
 | 
			
		||||
          'April',
 | 
			
		||||
          'Mai',
 | 
			
		||||
          'Juni',
 | 
			
		||||
          'Juli',
 | 
			
		||||
          'August',
 | 
			
		||||
          'September',
 | 
			
		||||
          'Oktober',
 | 
			
		||||
          'November',
 | 
			
		||||
          'Dezember'
 | 
			
		||||
        ][new Date().getMonth()]
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created() {},
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions({
 | 
			
		||||
      createYears: 'finanzerUsers/createYears',
 | 
			
		||||
      addAmount: 'finanzerUsers/addAmount',
 | 
			
		||||
      addCredit: 'finanzerUsers/addCredit',
 | 
			
		||||
      countYear: 'finanzerUsers/countYear',
 | 
			
		||||
      sendMails: 'finanzerUsers/sendMails',
 | 
			
		||||
      addUser: 'finanzerUsers/addUser'
 | 
			
		||||
    }),
 | 
			
		||||
    async getData(promise) {
 | 
			
		||||
      return await promise
 | 
			
		||||
    },
 | 
			
		||||
    getLastColor(value) {
 | 
			
		||||
      return value < 0 ? 'red' : 'green'
 | 
			
		||||
    },
 | 
			
		||||
    getAllSum(sum, lastYear) {
 | 
			
		||||
      return lastYear + sum
 | 
			
		||||
    },
 | 
			
		||||
    getLockedColor(value) {
 | 
			
		||||
      return value ? 'red' : 'green'
 | 
			
		||||
    },
 | 
			
		||||
    computeError(error) {
 | 
			
		||||
      if (error) return 'error'
 | 
			
		||||
      else return 'success'
 | 
			
		||||
    },
 | 
			
		||||
    errorMessage(error) {
 | 
			
		||||
      if (error.error)
 | 
			
		||||
        return (
 | 
			
		||||
          'Konnte Email an ' +
 | 
			
		||||
          error.user.firstname +
 | 
			
		||||
          ' ' +
 | 
			
		||||
          error.user.lastname +
 | 
			
		||||
          ' nicht senden!'
 | 
			
		||||
        )
 | 
			
		||||
      else
 | 
			
		||||
        return (
 | 
			
		||||
          'Email wurde an ' +
 | 
			
		||||
          error.user.firstname +
 | 
			
		||||
          ' ' +
 | 
			
		||||
          error.user.lastname +
 | 
			
		||||
          ' versandt.'
 | 
			
		||||
        )
 | 
			
		||||
    },
 | 
			
		||||
    setExpand(user) {
 | 
			
		||||
      user.expand ? (user.expand = false) : (user.expand = true)
 | 
			
		||||
    },
 | 
			
		||||
    isExpand(value) {
 | 
			
		||||
      return value ? 'rotate' : ''
 | 
			
		||||
    },
 | 
			
		||||
    // eslint-disable-next-line no-unused-vars
 | 
			
		||||
    add(user) {
 | 
			
		||||
      if (this.type.value === 'amount') {
 | 
			
		||||
        this.addAmount({
 | 
			
		||||
          user: user,
 | 
			
		||||
          amount: this.amount,
 | 
			
		||||
          year: this.selectedYear.value,
 | 
			
		||||
          month: this.selectedMonth.value
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      if (this.type.value === 'credit') {
 | 
			
		||||
        this.addCredit({
 | 
			
		||||
          user: user,
 | 
			
		||||
          credit: this.amount,
 | 
			
		||||
          year: this.selectedYear.value,
 | 
			
		||||
          month: this.selectedMonth.value
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.createDefault(
 | 
			
		||||
        this.amount,
 | 
			
		||||
        this.type,
 | 
			
		||||
        this.selectedYear,
 | 
			
		||||
        this.selectedMonth
 | 
			
		||||
      )
 | 
			
		||||
    },
 | 
			
		||||
    isFiltered(user) {
 | 
			
		||||
      try {
 | 
			
		||||
        var filters = this.filter.split(' ')
 | 
			
		||||
        for (var filter in filters) {
 | 
			
		||||
          if (
 | 
			
		||||
                  user.firstname.toLowerCase().includes(filters[filter].toLowerCase()) ||
 | 
			
		||||
                  user.lastname.toLowerCase().includes(filters[filter].toLowerCase())
 | 
			
		||||
          ) {
 | 
			
		||||
            return true
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        return true
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    addToUser(user) {
 | 
			
		||||
      this.addUser(user)
 | 
			
		||||
      this.$router.push({name: 'activeUser', params: {id: user.username}})
 | 
			
		||||
    },
 | 
			
		||||
    createDefault() {
 | 
			
		||||
      this.amount = null
 | 
			
		||||
      this.type = { value: 'credit', text: 'Guthaben' }
 | 
			
		||||
      this.selectedYear = {
 | 
			
		||||
        value: new Date().getFullYear(),
 | 
			
		||||
        text: new Date().getFullYear()
 | 
			
		||||
      }
 | 
			
		||||
      this.selectedMonth = {
 | 
			
		||||
        value: new Date().getMonth() + 1,
 | 
			
		||||
        text: [
 | 
			
		||||
          'Januar',
 | 
			
		||||
          'Februar',
 | 
			
		||||
          'März',
 | 
			
		||||
          'April',
 | 
			
		||||
          'Mai',
 | 
			
		||||
          'Juni',
 | 
			
		||||
          'Juli',
 | 
			
		||||
          'August',
 | 
			
		||||
          'September',
 | 
			
		||||
          'Oktober',
 | 
			
		||||
          'November',
 | 
			
		||||
          'Dezember'
 | 
			
		||||
        ][new Date().getMonth()]
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    isActualYear() {
 | 
			
		||||
      return this.year === new Date().getFullYear()
 | 
			
		||||
    },
 | 
			
		||||
    ...mapGetters({
 | 
			
		||||
      users: 'finanzerUsers/users',
 | 
			
		||||
      allUsers: 'finanzerUsers/allUsers',
 | 
			
		||||
      errorMails: 'finanzerUsers/errorMails',
 | 
			
		||||
      year: 'finanzerUsers/year',
 | 
			
		||||
      years: 'finanzerUsers/years',
 | 
			
		||||
      months: 'finanzerUsers/months',
 | 
			
		||||
      loading: 'finanzerUsers/usersLoading',
 | 
			
		||||
      allUsersLoading: 'finanzerUsers/allUsersLoading'
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.rotate {
 | 
			
		||||
  transform: rotate(180deg);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,60 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <v-card style="margin-top: 3px">
 | 
			
		||||
      <v-card-title>
 | 
			
		||||
        <v-skeleton-loader type="heading" />
 | 
			
		||||
      </v-card-title>
 | 
			
		||||
      <v-container>
 | 
			
		||||
        <v-skeleton-loader type="table-thead"/>
 | 
			
		||||
        <v-skeleton-loader type="table-row-divider@3"/>
 | 
			
		||||
      </v-container>
 | 
			
		||||
      <v-container fluid>
 | 
			
		||||
        <v-row align="start" align-content="start">
 | 
			
		||||
          <v-col>
 | 
			
		||||
            <v-row>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-skeleton-loader type="chip" />
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-skeleton-loader type="chip" />
 | 
			
		||||
              </v-col>
 | 
			
		||||
            </v-row>
 | 
			
		||||
            <v-row>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-skeleton-loader type="chip"/>
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-skeleton-loader type="chip" />
 | 
			
		||||
              </v-col>
 | 
			
		||||
            </v-row>
 | 
			
		||||
          </v-col>
 | 
			
		||||
          <v-col align-self="center">
 | 
			
		||||
            <v-row>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-skeleton-loader type="chip" />
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-skeleton-loader type="chip" />
 | 
			
		||||
              </v-col>
 | 
			
		||||
            </v-row>
 | 
			
		||||
            <v-card outlined>
 | 
			
		||||
              <v-row>
 | 
			
		||||
                <v-card-title>
 | 
			
		||||
                  <v-skeleton-loader style="margin: 3px; margin-left: 10px" type="chip"/>
 | 
			
		||||
                </v-card-title>
 | 
			
		||||
              </v-row>
 | 
			
		||||
            </v-card>
 | 
			
		||||
          </v-col>
 | 
			
		||||
        </v-row>
 | 
			
		||||
      </v-container>
 | 
			
		||||
    </v-card>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'TableSkeleton'
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,82 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <v-toolbar tile>
 | 
			
		||||
      <v-toolbar-title>
 | 
			
		||||
        <v-row>
 | 
			
		||||
          <v-col>
 | 
			
		||||
            <v-skeleton-loader type="chip" />
 | 
			
		||||
          </v-col>
 | 
			
		||||
          <v-col>
 | 
			
		||||
            <v-skeleton-loader type="chip" />
 | 
			
		||||
          </v-col>
 | 
			
		||||
        </v-row>
 | 
			
		||||
      </v-toolbar-title>
 | 
			
		||||
      <v-spacer />
 | 
			
		||||
      <v-toolbar-items>
 | 
			
		||||
        <v-skeleton-loader type="button" />
 | 
			
		||||
      </v-toolbar-items>
 | 
			
		||||
    </v-toolbar>
 | 
			
		||||
    <v-card style="margin-top: 3px;">
 | 
			
		||||
      <v-card-title><v-skeleton-loader type="heading"/></v-card-title>
 | 
			
		||||
      <v-card-text>
 | 
			
		||||
        <v-form style="margin-left: 15px; margin-right: 15px">
 | 
			
		||||
          <v-row>
 | 
			
		||||
            <v-col>
 | 
			
		||||
              <v-skeleton-loader type="chip" />
 | 
			
		||||
            </v-col>
 | 
			
		||||
            <v-col>
 | 
			
		||||
              <v-skeleton-loader type="chip" />
 | 
			
		||||
            </v-col>
 | 
			
		||||
            <v-col>
 | 
			
		||||
              <v-skeleton-loader type="button" />
 | 
			
		||||
            </v-col>
 | 
			
		||||
          </v-row>
 | 
			
		||||
          <v-divider style="margin-bottom: 15px;" />
 | 
			
		||||
          <v-row>
 | 
			
		||||
            <v-col>
 | 
			
		||||
              <v-skeleton-loader type="chip" />
 | 
			
		||||
            </v-col>
 | 
			
		||||
            <v-col>
 | 
			
		||||
              <v-skeleton-loader type="chip" />
 | 
			
		||||
            </v-col>
 | 
			
		||||
          </v-row>
 | 
			
		||||
          <v-row>
 | 
			
		||||
            <v-skeleton-loader type="button" />
 | 
			
		||||
          </v-row>
 | 
			
		||||
        </v-form>
 | 
			
		||||
      </v-card-text>
 | 
			
		||||
    </v-card>
 | 
			
		||||
    <v-card style="margin-top: 3px;">
 | 
			
		||||
      <v-card-title><v-skeleton-loader type="chip"/></v-card-title>
 | 
			
		||||
      <v-card-text>
 | 
			
		||||
        <v-form style="margin-left: 15px; margin-right: 15px">
 | 
			
		||||
          <v-row>
 | 
			
		||||
            <v-col>
 | 
			
		||||
              <v-skeleton-loader type="chip" />
 | 
			
		||||
            </v-col>
 | 
			
		||||
            <v-col>
 | 
			
		||||
              <v-skeleton-loader type="chip" />
 | 
			
		||||
            </v-col>
 | 
			
		||||
          </v-row>
 | 
			
		||||
          <v-row>
 | 
			
		||||
            <v-col>
 | 
			
		||||
              <v-skeleton-loader type="chip" />
 | 
			
		||||
            </v-col>
 | 
			
		||||
            <v-col>
 | 
			
		||||
              <v-skeleton-loader type="chip" />
 | 
			
		||||
            </v-col>
 | 
			
		||||
          </v-row>
 | 
			
		||||
        </v-form>
 | 
			
		||||
        <v-skeleton-loader type="button" />
 | 
			
		||||
      </v-card-text>
 | 
			
		||||
    </v-card>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'UserSkeleton'
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,125 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <v-data-table dense :headers="headers" :items="user.creditList[year]" :hide-default-footer="true">
 | 
			
		||||
    <template v-slot:item.jan_amount="{ item }">
 | 
			
		||||
      <v-chip outlined :text-color="getColor(item, item.jan_amount)">
 | 
			
		||||
        {{(item.jan_amount /
 | 
			
		||||
        100).toFixed(2)}}
 | 
			
		||||
      </v-chip>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-slot:item.feb_amount="{ item }">
 | 
			
		||||
      <v-chip outlined :text-color="getColor(item, item.feb_amount)">
 | 
			
		||||
        {{(item.feb_amount /
 | 
			
		||||
        100).toFixed(2)}}
 | 
			
		||||
      </v-chip>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-slot:item.maer_amount="{ item }">
 | 
			
		||||
      <v-chip outlined :text-color="getColor(item, item.maer_amount)">
 | 
			
		||||
        {{(item.maer_amount /
 | 
			
		||||
        100).toFixed(2)}}
 | 
			
		||||
      </v-chip>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-slot:item.apr_amount="{ item }">
 | 
			
		||||
      <v-chip outlined :text-color="getColor(item, item.apr_amount)">
 | 
			
		||||
        {{(item.apr_amount /
 | 
			
		||||
        100).toFixed(2)}}
 | 
			
		||||
      </v-chip>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-slot:item.mai_amount="{ item }">
 | 
			
		||||
      <v-chip outlined :text-color="getColor(item, item.mai_amount)">
 | 
			
		||||
        {{(item.mai_amount /
 | 
			
		||||
        100).toFixed(2)}}
 | 
			
		||||
      </v-chip>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-slot:item.jun_amount="{ item }">
 | 
			
		||||
      <v-chip outlined :text-color="getColor(item, item.jun_amount)">
 | 
			
		||||
        {{(item.jun_amount /
 | 
			
		||||
        100).toFixed(2)}}
 | 
			
		||||
      </v-chip>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-slot:item.jul_amount="{ item }">
 | 
			
		||||
      <v-chip outlined :text-color="getColor(item, item.jul_amount)">
 | 
			
		||||
        {{(item.jul_amount /
 | 
			
		||||
        100).toFixed(2)}}
 | 
			
		||||
      </v-chip>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-slot:item.aug_amount="{ item }">
 | 
			
		||||
      <v-chip outlined :text-color="getColor(item, item.aug_amount)">
 | 
			
		||||
        {{(item.aug_amount /
 | 
			
		||||
        100).toFixed(2)}}
 | 
			
		||||
      </v-chip>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-slot:item.sep_amount="{ item }">
 | 
			
		||||
      <v-chip outlined :text-color="getColor(item, item.sep_amount)">
 | 
			
		||||
        {{(item.sep_amount /
 | 
			
		||||
        100).toFixed(2)}}
 | 
			
		||||
      </v-chip>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-slot:item.okt_amount="{ item }">
 | 
			
		||||
      <v-chip outlined :text-color="getColor(item, item.okt_amount)">
 | 
			
		||||
        {{(item.okt_amount /
 | 
			
		||||
        100).toFixed(2)}}
 | 
			
		||||
      </v-chip>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-slot:item.nov_amount="{ item }">
 | 
			
		||||
      <v-chip outlined :text-color="getColor(item, item.nov_amount)">
 | 
			
		||||
        {{(item.nov_amount /
 | 
			
		||||
        100).toFixed(2)}}
 | 
			
		||||
      </v-chip>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-slot:item.dez_amount="{ item }">
 | 
			
		||||
      <v-chip outlined :text-color="getColor(item, item.dez_amount)">
 | 
			
		||||
        {{(item.dez_amount /
 | 
			
		||||
        100).toFixed(2)}}
 | 
			
		||||
      </v-chip>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-slot:item.sum="{ item }">
 | 
			
		||||
      <v-chip outlined :text-color="getColor(item, item.sum)">{{(item.sum / 100).toFixed(2)}}</v-chip>
 | 
			
		||||
    </template>
 | 
			
		||||
  </v-data-table>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'Table',
 | 
			
		||||
  props: {
 | 
			
		||||
    user: Object,
 | 
			
		||||
    year: Number
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      headers: [
 | 
			
		||||
        {
 | 
			
		||||
          text: 'Schulden / Guthaben',
 | 
			
		||||
          align: 'left',
 | 
			
		||||
          sortable: false,
 | 
			
		||||
          value: 'type'
 | 
			
		||||
        },
 | 
			
		||||
        { text: 'Januar in EUR', value: 'jan_amount' },
 | 
			
		||||
        { text: 'Februar in EUR', value: 'feb_amount' },
 | 
			
		||||
        { text: 'März in EUR', value: 'maer_amount' },
 | 
			
		||||
        { text: 'April in EUR', value: 'apr_amount' },
 | 
			
		||||
        { text: 'Mai in EUR', value: 'mai_amount' },
 | 
			
		||||
        { text: 'Juni in EUR', value: 'jun_amount' },
 | 
			
		||||
        { text: 'Juli in EUR', value: 'jul_amount' },
 | 
			
		||||
        { text: 'August in EUR', value: 'aug_amount' },
 | 
			
		||||
        { text: 'September in EUR', value: 'sep_amount' },
 | 
			
		||||
        { text: 'Oktober in EUR', value: 'okt_amount' },
 | 
			
		||||
        { text: 'November in EUR', value: 'nov_amount' },
 | 
			
		||||
        { text: 'Dezember in EUR', value: 'dez_amount' },
 | 
			
		||||
        { text: 'Summe in EUR', value: 'sum' }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    getColor(item, value) {
 | 
			
		||||
      if (item.type === 'Summe') {
 | 
			
		||||
        return value < 0 ? 'red' : 'green'
 | 
			
		||||
      }
 | 
			
		||||
      return item.type === 'Guthaben' ? 'green' : 'red'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,363 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <v-content v-if="loading" >
 | 
			
		||||
      <UserSkeleton />
 | 
			
		||||
    </v-content>
 | 
			
		||||
    <v-content v-if="activeUser">
 | 
			
		||||
      <v-toolbar tile>
 | 
			
		||||
        <v-toolbar-title
 | 
			
		||||
          >{{ activeUser.lastname }},
 | 
			
		||||
          {{ activeUser.firstname }}</v-toolbar-title
 | 
			
		||||
        >
 | 
			
		||||
        <v-spacer />
 | 
			
		||||
        <v-toolbar-items>
 | 
			
		||||
          <v-btn @click="sendMail({ username: activeUser.username })" text
 | 
			
		||||
            >Email senden</v-btn
 | 
			
		||||
          >
 | 
			
		||||
        </v-toolbar-items>
 | 
			
		||||
      </v-toolbar>
 | 
			
		||||
      <v-progress-linear v-if="activeUser.loading" indeterminate />
 | 
			
		||||
      <v-expand-transition>
 | 
			
		||||
        <v-card style="margin-top: 3px" v-show="errorMail">
 | 
			
		||||
          <v-alert dense :type="computeError(errorMail)">
 | 
			
		||||
            {{ errorMessage(errorMail) }}
 | 
			
		||||
          </v-alert>
 | 
			
		||||
        </v-card>
 | 
			
		||||
      </v-expand-transition>
 | 
			
		||||
      <v-card style="margin-top: 3px;">
 | 
			
		||||
        <v-card-title>Konfiguration</v-card-title>
 | 
			
		||||
        <v-card-text>
 | 
			
		||||
          <v-form style="margin-left: 15px; margin-right: 15px">
 | 
			
		||||
            <v-row>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-label>Status:</v-label>
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-chip outlined :text-color="getLockedColor(activeUser.locked)"
 | 
			
		||||
                  >{{ activeUser.locked ? 'Gesperrt' : 'nicht Gesperrt' }}
 | 
			
		||||
                </v-chip>
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-btn
 | 
			
		||||
                  @click="
 | 
			
		||||
                    doLock({ user: activeUser, locked: !activeUser.locked })
 | 
			
		||||
                  "
 | 
			
		||||
                  >{{ activeUser.locked ? 'Entperren' : 'Sperren' }}
 | 
			
		||||
                </v-btn>
 | 
			
		||||
              </v-col>
 | 
			
		||||
            </v-row>
 | 
			
		||||
            <v-divider style="margin-bottom: 15px;" />
 | 
			
		||||
            <v-row>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-text-field
 | 
			
		||||
                  :rules="[isNumber]"
 | 
			
		||||
                  label="Betrag des Sperrlimits in € (EURO)"
 | 
			
		||||
                  v-model="limit"
 | 
			
		||||
                ></v-text-field>
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-select
 | 
			
		||||
                  return-object
 | 
			
		||||
                  v-model="autoLock"
 | 
			
		||||
                  label="Automatische Sperre"
 | 
			
		||||
                  :items="[
 | 
			
		||||
                    { value: true, text: 'Aktiviert' },
 | 
			
		||||
                    { value: false, text: 'Deaktiviert' }
 | 
			
		||||
                  ]"
 | 
			
		||||
                  item-text="text"
 | 
			
		||||
                  item-value="value"
 | 
			
		||||
                />
 | 
			
		||||
              </v-col>
 | 
			
		||||
            </v-row>
 | 
			
		||||
            <v-row>
 | 
			
		||||
              <v-btn
 | 
			
		||||
                block
 | 
			
		||||
                @click="
 | 
			
		||||
                  saveConfig({
 | 
			
		||||
                    user: activeUser,
 | 
			
		||||
                    limit: limit,
 | 
			
		||||
                    autoLock: autoLock.value
 | 
			
		||||
                  })
 | 
			
		||||
                "
 | 
			
		||||
                >Speichern
 | 
			
		||||
              </v-btn>
 | 
			
		||||
            </v-row>
 | 
			
		||||
          </v-form>
 | 
			
		||||
        </v-card-text>
 | 
			
		||||
      </v-card>
 | 
			
		||||
      <v-card style="margin-top: 3px;">
 | 
			
		||||
        <v-card-title>Geld transferieren</v-card-title>
 | 
			
		||||
        <v-card-text>
 | 
			
		||||
          <v-form style="margin-left: 15px; margin-right: 15px">
 | 
			
		||||
            <v-row>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-text-field
 | 
			
		||||
                  :rules="[isNumber]"
 | 
			
		||||
                  label="Betrag"
 | 
			
		||||
                  v-model="amount"
 | 
			
		||||
                ></v-text-field>
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-select
 | 
			
		||||
                  return-object
 | 
			
		||||
                  v-model="type"
 | 
			
		||||
                  label="Typ"
 | 
			
		||||
                  :items="[
 | 
			
		||||
                    { value: 'amount', text: 'Schulden' },
 | 
			
		||||
                    { value: 'credit', text: 'Guthaben' }
 | 
			
		||||
                  ]"
 | 
			
		||||
                  item-text="text"
 | 
			
		||||
                  item-value="value"
 | 
			
		||||
                ></v-select>
 | 
			
		||||
              </v-col>
 | 
			
		||||
            </v-row>
 | 
			
		||||
            <v-row>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-select
 | 
			
		||||
                  return-object
 | 
			
		||||
                  v-model="selectedYear"
 | 
			
		||||
                  label="Jahr"
 | 
			
		||||
                  :items="selectYears"
 | 
			
		||||
                  item-text="text"
 | 
			
		||||
                  item-value="value"
 | 
			
		||||
                ></v-select>
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-select
 | 
			
		||||
                  return-object
 | 
			
		||||
                  v-model="selectedMonth"
 | 
			
		||||
                  label="Monat"
 | 
			
		||||
                  :items="months"
 | 
			
		||||
                  item-text="text"
 | 
			
		||||
                  item-value="value"
 | 
			
		||||
                ></v-select>
 | 
			
		||||
              </v-col>
 | 
			
		||||
            </v-row>
 | 
			
		||||
          </v-form>
 | 
			
		||||
          <v-btn block @click="add">Hinzufügen</v-btn>
 | 
			
		||||
        </v-card-text>
 | 
			
		||||
      </v-card>
 | 
			
		||||
      <div v-for="year in years" :key="years.indexOf(year)">
 | 
			
		||||
        <v-card style="margin-top: 3px;">
 | 
			
		||||
          <v-card-title>{{ year }}</v-card-title>
 | 
			
		||||
          <Table v-bind:user="activeUser" v-bind:year="year" />
 | 
			
		||||
          <v-container fluid>
 | 
			
		||||
            <v-col>
 | 
			
		||||
              <v-row>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-label>Vorjahr:</v-label>
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-chip
 | 
			
		||||
                    outlined
 | 
			
		||||
                    :text-color="
 | 
			
		||||
                      getLastColor(activeUser.creditList[year][1].last)
 | 
			
		||||
                    "
 | 
			
		||||
                  >
 | 
			
		||||
                    {{ (activeUser.creditList[year][1].last / 100).toFixed(2) }}
 | 
			
		||||
                  </v-chip>
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-label>Gesamt:</v-label>
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-chip
 | 
			
		||||
                    outlined
 | 
			
		||||
                    x-large
 | 
			
		||||
                    :text-color="
 | 
			
		||||
                      getLastColor(
 | 
			
		||||
                        getAllSum(
 | 
			
		||||
                          activeUser.creditList[year][2].sum,
 | 
			
		||||
                          activeUser.creditList[year][1].last
 | 
			
		||||
                        )
 | 
			
		||||
                      )
 | 
			
		||||
                    "
 | 
			
		||||
                  >
 | 
			
		||||
                    {{
 | 
			
		||||
                      (
 | 
			
		||||
                        getAllSum(
 | 
			
		||||
                          activeUser.creditList[year][2].sum,
 | 
			
		||||
                          activeUser.creditList[year][1].last
 | 
			
		||||
                        ) / 100
 | 
			
		||||
                      ).toFixed(2)
 | 
			
		||||
                    }}
 | 
			
		||||
                  </v-chip>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
            </v-col>
 | 
			
		||||
          </v-container>
 | 
			
		||||
        </v-card>
 | 
			
		||||
      </div>
 | 
			
		||||
    </v-content>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import Table from './Table'
 | 
			
		||||
import { mapGetters, mapActions } from 'vuex'
 | 
			
		||||
import UserSkeleton from "./Skeleton/UserSkeleton";
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'User',
 | 
			
		||||
  props: {
 | 
			
		||||
    id: String
 | 
			
		||||
  },
 | 
			
		||||
  components: {UserSkeleton, Table },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      isNumber: value => !isNaN(value) || 'Betrag muss eine Zahl sein.',
 | 
			
		||||
      limit: null,
 | 
			
		||||
      autoLock: null,
 | 
			
		||||
      amount: null,
 | 
			
		||||
      type: { value: 'credit', text: 'Guthaben' },
 | 
			
		||||
      selectedYear: {
 | 
			
		||||
        value: new Date().getFullYear(),
 | 
			
		||||
        text: new Date().getFullYear()
 | 
			
		||||
      },
 | 
			
		||||
      selectedMonth: {
 | 
			
		||||
        value: new Date().getMonth() + 1,
 | 
			
		||||
        text: [
 | 
			
		||||
          'Januar',
 | 
			
		||||
          'Februar',
 | 
			
		||||
          'März',
 | 
			
		||||
          'April',
 | 
			
		||||
          'Mai',
 | 
			
		||||
          'Juni',
 | 
			
		||||
          'Juli',
 | 
			
		||||
          'August',
 | 
			
		||||
          'September',
 | 
			
		||||
          'Oktober',
 | 
			
		||||
          'November',
 | 
			
		||||
          'Dezember'
 | 
			
		||||
        ][new Date().getMonth()]
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.setActiveUser(this.$route.params.id)
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions({
 | 
			
		||||
      addAmount: 'finanzerUsers/addAmount',
 | 
			
		||||
      addCredit: 'finanzerUsers/addCredit',
 | 
			
		||||
      sendMail: 'finanzerUsers/sendMail',
 | 
			
		||||
      doLock: 'finanzerUsers/doLock',
 | 
			
		||||
      saveConfig: 'finanzerUsers/saveConfig',
 | 
			
		||||
      setActiveUser: 'finanzerUsers/setActiveUser'
 | 
			
		||||
    }),
 | 
			
		||||
    getLastColor(value) {
 | 
			
		||||
      return value < 0 ? 'red' : 'green'
 | 
			
		||||
    },
 | 
			
		||||
    getAllSum(sum, lastYear) {
 | 
			
		||||
      return lastYear + sum
 | 
			
		||||
    },
 | 
			
		||||
    getLockedColor(value) {
 | 
			
		||||
      return value ? 'red' : 'green'
 | 
			
		||||
    },
 | 
			
		||||
    add() {
 | 
			
		||||
      if (this.type.value === 'amount') {
 | 
			
		||||
        this.addAmount({
 | 
			
		||||
          user: this.activeUser,
 | 
			
		||||
          amount: this.amount,
 | 
			
		||||
          year: this.selectedYear.value,
 | 
			
		||||
          month: this.selectedMonth.value
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      if (this.type.value === 'credit') {
 | 
			
		||||
        this.addCredit({
 | 
			
		||||
          user: this.activeUser,
 | 
			
		||||
          credit: this.amount,
 | 
			
		||||
          year: this.selectedYear.value,
 | 
			
		||||
          month: this.selectedMonth.value
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.createDefault()
 | 
			
		||||
    },
 | 
			
		||||
    createDefault() {
 | 
			
		||||
      // eslint-disable-next-line no-unused-vars
 | 
			
		||||
      let year = new Date().getFullYear()
 | 
			
		||||
      // eslint-disable-next-line no-unused-vars
 | 
			
		||||
      let month = new Date().getMonth()
 | 
			
		||||
      this.amount = null
 | 
			
		||||
      this.type = { value: 'credit', text: 'Guthaben' }
 | 
			
		||||
      this.selectedYear = {
 | 
			
		||||
        value: new Date().getFullYear(),
 | 
			
		||||
        text: new Date().getFullYear()
 | 
			
		||||
      }
 | 
			
		||||
      this.selectedMonth = {
 | 
			
		||||
        value: new Date().getMonth() + 1,
 | 
			
		||||
        text: [
 | 
			
		||||
          'Januar',
 | 
			
		||||
          'Februar',
 | 
			
		||||
          'März',
 | 
			
		||||
          'April',
 | 
			
		||||
          'Mai',
 | 
			
		||||
          'Juni',
 | 
			
		||||
          'Juli',
 | 
			
		||||
          'August',
 | 
			
		||||
          'September',
 | 
			
		||||
          'Oktober',
 | 
			
		||||
          'November',
 | 
			
		||||
          'Dezember'
 | 
			
		||||
        ][new Date().getMonth()]
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    computeError(error) {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        if (error.error) return 'error'
 | 
			
		||||
        else return 'success'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    errorMessage(error) {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        if (error.error)
 | 
			
		||||
          return (
 | 
			
		||||
            'Konnte Email an ' +
 | 
			
		||||
            error.user.firstname +
 | 
			
		||||
            ' ' +
 | 
			
		||||
            error.user.lastname +
 | 
			
		||||
            ' nicht senden!'
 | 
			
		||||
          )
 | 
			
		||||
        else
 | 
			
		||||
          return (
 | 
			
		||||
            'Email wurde an ' +
 | 
			
		||||
            error.user.firstname +
 | 
			
		||||
            ' ' +
 | 
			
		||||
            error.user.lastname +
 | 
			
		||||
            ' versandt.'
 | 
			
		||||
          )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    years() {
 | 
			
		||||
      let years = []
 | 
			
		||||
      for (let year in this.activeUser.creditList) {
 | 
			
		||||
        years.unshift(parseInt(year))
 | 
			
		||||
      }
 | 
			
		||||
      return years
 | 
			
		||||
    },
 | 
			
		||||
    ...mapGetters({
 | 
			
		||||
      activeUser: 'finanzerUsers/activeUser',
 | 
			
		||||
      errorMail: 'finanzerUsers/errorMail',
 | 
			
		||||
      months: 'finanzerUsers/months',
 | 
			
		||||
      selectYears: 'finanzerUsers/selectYears',
 | 
			
		||||
      loading: 'finanzerUsers/addUserLoading'
 | 
			
		||||
    })
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    activeUser(newVal) {
 | 
			
		||||
      this.limit = (newVal.limit / 100).toFixed(2)
 | 
			
		||||
      this.autoLock = {
 | 
			
		||||
        value: newVal.autoLock,
 | 
			
		||||
        text: newVal.autoLock ? 'Aktiviert' : 'Deaktiviert'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    id(newVal) {
 | 
			
		||||
      this.setActiveUser(newVal)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
<template>
 | 
			
		||||
    <v-list>
 | 
			
		||||
        <v-list-item class="title" link :to="{name: 'gastroPricelist'}">
 | 
			
		||||
            <v-list-item-icon>
 | 
			
		||||
                <v-icon>{{list}}</v-icon>
 | 
			
		||||
            </v-list-item-icon>
 | 
			
		||||
            <v-list-item-title>
 | 
			
		||||
                Preisliste
 | 
			
		||||
            </v-list-item-title>
 | 
			
		||||
        </v-list-item>
 | 
			
		||||
    </v-list>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
    import { mdiFileMultiple } from '@mdi/js'
 | 
			
		||||
    export default {
 | 
			
		||||
        name: "GastroNavigation",
 | 
			
		||||
        data() {
 | 
			
		||||
            return {
 | 
			
		||||
                list: mdiFileMultiple
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,23 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <q-circular-progress
 | 
			
		||||
    indeterminate
 | 
			
		||||
    show-value
 | 
			
		||||
    font-size="10px"
 | 
			
		||||
    class="q-ma-md"
 | 
			
		||||
    size="80px"
 | 
			
		||||
    :thickness="0.15"
 | 
			
		||||
    color="primary"
 | 
			
		||||
    track-color="grey-3"
 | 
			
		||||
  >
 | 
			
		||||
    <q-avatar size="60px">
 | 
			
		||||
      <img src="flaschengeist-logo.svg" />
 | 
			
		||||
    </q-avatar>
 | 
			
		||||
  </q-circular-progress>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'CircularProgress',
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,23 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <q-circular-progress
 | 
			
		||||
    indeterminate
 | 
			
		||||
    show-value
 | 
			
		||||
    font-size="10px"
 | 
			
		||||
    class="q-ma-md"
 | 
			
		||||
    size="80px"
 | 
			
		||||
    :thickness="0.15"
 | 
			
		||||
    color="primary"
 | 
			
		||||
    track-color="grey-3"
 | 
			
		||||
  >
 | 
			
		||||
    <q-avatar size="60px">
 | 
			
		||||
      <img src="flaschengeist-logo.svg" />
 | 
			
		||||
    </q-avatar>
 | 
			
		||||
  </q-circular-progress>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'DarkCircularProgress',
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,14 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <router-view />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'EmptyParent',
 | 
			
		||||
  setup() {
 | 
			
		||||
    return {};
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,61 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <q-expansion-item
 | 
			
		||||
    v-if="isGranted(entry)"
 | 
			
		||||
    clickable
 | 
			
		||||
    :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-btn v-close-popup label="Verknüpfung erstellen" dense @click="addShortCut(child)" />
 | 
			
		||||
          </q-menu>
 | 
			
		||||
          <q-item-section avatar>
 | 
			
		||||
            <q-icon :name="child.icon" />
 | 
			
		||||
          </q-item-section>
 | 
			
		||||
          <q-item-section>
 | 
			
		||||
            <q-item-label>
 | 
			
		||||
              {{ getTitle(child) }}
 | 
			
		||||
            </q-item-label>
 | 
			
		||||
          </q-item-section>
 | 
			
		||||
        </q-item>
 | 
			
		||||
      </div>
 | 
			
		||||
    </q-list>
 | 
			
		||||
  </q-expansion-item>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, PropType } from 'vue';
 | 
			
		||||
import { hasPermissions } from '@flaschengeist/api';
 | 
			
		||||
import { FG_Plugin } from '@flaschengeist/types';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'EssentialExpansionLink',
 | 
			
		||||
  components: {},
 | 
			
		||||
  props: {
 | 
			
		||||
    entry: {
 | 
			
		||||
      type: Object as PropType<FG_Plugin.MenuLink>,
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  emits: {
 | 
			
		||||
    addShortCut: (val: FG_Plugin.MenuLink) => val.link,
 | 
			
		||||
  },
 | 
			
		||||
  setup(_, { emit }) {
 | 
			
		||||
    function isGranted(val: FG_Plugin.MenuLink) {
 | 
			
		||||
      return hasPermissions(val.permissions || []);
 | 
			
		||||
    }
 | 
			
		||||
    function getTitle(entry: FG_Plugin.MenuLink) {
 | 
			
		||||
      return typeof entry.title === 'function' ? entry.title() : entry.title;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function addShortCut(val: FG_Plugin.MenuLink) {
 | 
			
		||||
      emit('addShortCut', val);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { isGranted, getTitle, addShortCut };
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,35 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <q-item v-if="isGranted" clickable tag="a" target="self" :to="{ name: entry.link }">
 | 
			
		||||
    <q-item-section v-if="entry.icon" avatar>
 | 
			
		||||
      <q-icon :name="entry.icon" />
 | 
			
		||||
    </q-item-section>
 | 
			
		||||
 | 
			
		||||
    <q-item-section>
 | 
			
		||||
      <q-item-label>{{ title }}</q-item-label>
 | 
			
		||||
    </q-item-section>
 | 
			
		||||
  </q-item>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineComponent, PropType } from 'vue';
 | 
			
		||||
import { hasPermissions } from '@flaschengeist/api';
 | 
			
		||||
import { FG_Plugin } from '@flaschengeist/types';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'EssentialLink',
 | 
			
		||||
  props: {
 | 
			
		||||
    entry: {
 | 
			
		||||
      type: Object as PropType<FG_Plugin.MenuLink>,
 | 
			
		||||
      required: true,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  setup(props) {
 | 
			
		||||
    const isGranted = computed(() => hasPermissions(props.entry.permissions || []));
 | 
			
		||||
    const title = computed(() =>
 | 
			
		||||
      typeof props.entry.title === 'function' ? props.entry.title() : props.entry.title
 | 
			
		||||
    );
 | 
			
		||||
    return { isGranted, title };
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,37 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <q-btn v-if="isGranted" flat dense :icon="shortcut.icon" :to="{ name: shortcut.link }" round>
 | 
			
		||||
    <q-menu v-if="context" context-menu>
 | 
			
		||||
      <q-btn v-close-popup label="Verknüpfung entfernen" @click="deleteShortcut" />
 | 
			
		||||
    </q-menu>
 | 
			
		||||
  </q-btn>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineComponent, PropType } from 'vue';
 | 
			
		||||
import { hasPermissions } from '@flaschengeist/api';
 | 
			
		||||
import { FG_Plugin } from '@flaschengeist/types';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
  name: 'ShortcutLink',
 | 
			
		||||
  props: {
 | 
			
		||||
    shortcut: {
 | 
			
		||||
      required: true,
 | 
			
		||||
      type: Object as PropType<FG_Plugin.Shortcut | FG_Plugin.MenuLink>,
 | 
			
		||||
    },
 | 
			
		||||
    context: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: false,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  emits: {
 | 
			
		||||
    deleteShortcut: (val: FG_Plugin.MenuLink | FG_Plugin.Shortcut) => val.link,
 | 
			
		||||
  },
 | 
			
		||||
  setup(props, { emit }) {
 | 
			
		||||
    const isGranted = computed(() => hasPermissions(props.shortcut.permissions || []));
 | 
			
		||||
    function deleteShortcut() {
 | 
			
		||||
      emit('deleteShortcut', props.shortcut);
 | 
			
		||||
    }
 | 
			
		||||
    return { isGranted, deleteShortcut };
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,497 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <v-data-table
 | 
			
		||||
      :headers="headers"
 | 
			
		||||
      :items="priceList"
 | 
			
		||||
      :search="search"
 | 
			
		||||
      :loading="priceListLoading || typesLoading"
 | 
			
		||||
    >
 | 
			
		||||
      <template v-slot:top>
 | 
			
		||||
        <v-toolbar flat color="white">
 | 
			
		||||
          <v-toolbar-title>Preisliste</v-toolbar-title>
 | 
			
		||||
          <v-spacer></v-spacer>
 | 
			
		||||
          <v-text-field
 | 
			
		||||
            v-model="search"
 | 
			
		||||
            label="Suche Getränk"
 | 
			
		||||
            single-line
 | 
			
		||||
            hide-details
 | 
			
		||||
          >
 | 
			
		||||
            <template v-slot:append>
 | 
			
		||||
              <v-icon>{{ searchIcon }}</v-icon>
 | 
			
		||||
            </template>
 | 
			
		||||
          </v-text-field>
 | 
			
		||||
          <v-dialog v-model="dialog" v-if="isGastro && isGastroPage">
 | 
			
		||||
            <template v-slot:activator="{ on }">
 | 
			
		||||
              <v-btn
 | 
			
		||||
                fab
 | 
			
		||||
                x-small
 | 
			
		||||
                color="primary"
 | 
			
		||||
                class="mb-2"
 | 
			
		||||
                v-on="on"
 | 
			
		||||
                style="margin: 5px"
 | 
			
		||||
              >
 | 
			
		||||
                <v-icon>{{ plus }}</v-icon>
 | 
			
		||||
              </v-btn>
 | 
			
		||||
            </template>
 | 
			
		||||
            <v-card>
 | 
			
		||||
              <v-card-title>
 | 
			
		||||
                <span class="headline">{{ formTitle }}</span>
 | 
			
		||||
              </v-card-title>
 | 
			
		||||
 | 
			
		||||
              <v-card-text>
 | 
			
		||||
                <v-container>
 | 
			
		||||
                  <v-row>
 | 
			
		||||
                    <v-col cols="12" sm="6" md="4">
 | 
			
		||||
                      <v-text-field
 | 
			
		||||
                        v-model="editedItem.name"
 | 
			
		||||
                        label="Name"
 | 
			
		||||
                        outlined
 | 
			
		||||
                      ></v-text-field>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col cols="12" sm="6" md="4">
 | 
			
		||||
                      <v-autocomplete
 | 
			
		||||
                        return-object
 | 
			
		||||
                        v-model="editedItem.type"
 | 
			
		||||
                        label="Kategorie"
 | 
			
		||||
                        item-text="name"
 | 
			
		||||
                        item-value="id"
 | 
			
		||||
                        :items="types"
 | 
			
		||||
                        outlined
 | 
			
		||||
                        :search-input.sync="searchType"
 | 
			
		||||
                        no-data-text="Kategorie nicht vorhanden."
 | 
			
		||||
                      >
 | 
			
		||||
                        <template v-slot:append-item>
 | 
			
		||||
                          <v-list-item v-if="!inType(searchType)">
 | 
			
		||||
                            <v-list-item-title>
 | 
			
		||||
                              {{ searchType }}
 | 
			
		||||
                            </v-list-item-title>
 | 
			
		||||
                            <v-btn
 | 
			
		||||
                              dark
 | 
			
		||||
                              x-small
 | 
			
		||||
                              color="blue darken-1"
 | 
			
		||||
                              @click="addType()"
 | 
			
		||||
                              :disabled="inType(searchType)"
 | 
			
		||||
                              fab
 | 
			
		||||
                            >
 | 
			
		||||
                              <v-icon>
 | 
			
		||||
                                {{ plus }}
 | 
			
		||||
                              </v-icon>
 | 
			
		||||
                            </v-btn>
 | 
			
		||||
                          </v-list-item>
 | 
			
		||||
                        </template>
 | 
			
		||||
                      </v-autocomplete>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col cols="12" sm="6" md="4">
 | 
			
		||||
                      <v-text-field
 | 
			
		||||
                        v-model="editedItem.price"
 | 
			
		||||
                        label="Preis in €"
 | 
			
		||||
                        outlined
 | 
			
		||||
                      ></v-text-field>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col cols="12" sm="6" md="4">
 | 
			
		||||
                      <v-text-field
 | 
			
		||||
                        v-model="editedItem.price_big"
 | 
			
		||||
                        label="Preis groß in €"
 | 
			
		||||
                        outlined
 | 
			
		||||
                      ></v-text-field>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col cols="12" sm="6" md="4">
 | 
			
		||||
                      <v-text-field
 | 
			
		||||
                        v-model="editedItem.price_club"
 | 
			
		||||
                        label="Preis Club in €"
 | 
			
		||||
                        outlined
 | 
			
		||||
                      ></v-text-field>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col cols="12" sm="6" md="4">
 | 
			
		||||
                      <v-text-field
 | 
			
		||||
                        v-model="editedItem.price_club_big"
 | 
			
		||||
                        label="Preis Club groß in €"
 | 
			
		||||
                        outlined
 | 
			
		||||
                      ></v-text-field>
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col col="12" sm="6" md="4">
 | 
			
		||||
                      <v-text-field
 | 
			
		||||
                        v-model="editedItem.premium"
 | 
			
		||||
                        label="Aufpreis in €"
 | 
			
		||||
                        outlined
 | 
			
		||||
                      />
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col cols="12" sm="6" md="4">
 | 
			
		||||
                      <v-text-field
 | 
			
		||||
                        v-model="editedItem.premium_club"
 | 
			
		||||
                        label="Aufpreis Club in €"
 | 
			
		||||
                        outlined
 | 
			
		||||
                      />
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                    <v-col cols="12" sm="6" md="4">
 | 
			
		||||
                      <v-text-field
 | 
			
		||||
                        v-model="editedItem.price_extern_club"
 | 
			
		||||
                        label="Preis extern Club in €"
 | 
			
		||||
                        outlined
 | 
			
		||||
                      />
 | 
			
		||||
                    </v-col>
 | 
			
		||||
                  </v-row>
 | 
			
		||||
                </v-container>
 | 
			
		||||
              </v-card-text>
 | 
			
		||||
 | 
			
		||||
              <v-card-actions>
 | 
			
		||||
                <v-spacer></v-spacer>
 | 
			
		||||
                <v-btn color="blue darken-1" text @click="close"
 | 
			
		||||
                  >Abbrechen</v-btn
 | 
			
		||||
                >
 | 
			
		||||
                <v-btn color="blue darken-1" text @click="save"
 | 
			
		||||
                  >Speichern</v-btn
 | 
			
		||||
                >
 | 
			
		||||
              </v-card-actions>
 | 
			
		||||
            </v-card>
 | 
			
		||||
          </v-dialog>
 | 
			
		||||
        </v-toolbar>
 | 
			
		||||
      </template>
 | 
			
		||||
      <template v-slot:item.type="{ item }">
 | 
			
		||||
        {{ computeType(item.type) }}
 | 
			
		||||
      </template>
 | 
			
		||||
      <template v-slot:item.price="{ item }">
 | 
			
		||||
        {{ item.price ? (item.price / 100).toFixed(2) : '' }}
 | 
			
		||||
      </template>
 | 
			
		||||
      <template v-slot:item.price_big="{ item }">
 | 
			
		||||
        {{ item.price_big ? (item.price_big / 100).toFixed(2) : '' }}
 | 
			
		||||
      </template>
 | 
			
		||||
      <template v-slot:item.price_club="{ item }">
 | 
			
		||||
        {{
 | 
			
		||||
          item.name.toLowerCase() == 'long island ice tea'.toLowerCase()
 | 
			
		||||
            ? 'Ein Klubmitglied bestellt keinen Long Island Icetea'
 | 
			
		||||
            : item.price_club
 | 
			
		||||
            ? (item.price_club / 100).toFixed(2)
 | 
			
		||||
            : ''
 | 
			
		||||
        }}
 | 
			
		||||
      </template>
 | 
			
		||||
      <template v-slot:item.price_club_big="{ item }">
 | 
			
		||||
        {{ item.price_club_big ? (item.price_club_big / 100).toFixed(2) : '' }}
 | 
			
		||||
      </template>
 | 
			
		||||
      <template v-slot:item.premium="{ item }">
 | 
			
		||||
        {{ item.premium ? (item.premium / 100).toFixed(2) : '' }}
 | 
			
		||||
      </template>
 | 
			
		||||
      <template v-slot:item.premium_club="{ item }">
 | 
			
		||||
        {{ item.premium_club ? (item.premium_club / 100).toFixed(2) : '' }}
 | 
			
		||||
      </template>
 | 
			
		||||
      <template v-slot:item.price_extern_club="{ item }">
 | 
			
		||||
        {{
 | 
			
		||||
          item.price_extern_club
 | 
			
		||||
            ? (item.price_extern_club / 100).toFixed(2)
 | 
			
		||||
            : ''
 | 
			
		||||
        }}
 | 
			
		||||
      </template>
 | 
			
		||||
      <template v-slot:item.action="{ item }">
 | 
			
		||||
        <v-icon small class="mr-2" @click="editItem(item)">
 | 
			
		||||
          {{ editIcon }}
 | 
			
		||||
        </v-icon>
 | 
			
		||||
        <v-icon small @click="deleteItem(item)">
 | 
			
		||||
          {{ deleteIcon }}
 | 
			
		||||
        </v-icon>
 | 
			
		||||
      </template>
 | 
			
		||||
    </v-data-table>
 | 
			
		||||
    <v-card tile v-if="isGastro && isGastroPage" :loading="typesLoading">
 | 
			
		||||
      <v-card-title>
 | 
			
		||||
        Kategorien
 | 
			
		||||
        <v-spacer />
 | 
			
		||||
        <v-btn fab x-small @click="dialogType = true" color="primary">
 | 
			
		||||
          <v-icon>{{ plus }}</v-icon>
 | 
			
		||||
        </v-btn>
 | 
			
		||||
      </v-card-title>
 | 
			
		||||
      <v-card-text>
 | 
			
		||||
        <div tile v-for="type in types" :key="type.id">
 | 
			
		||||
          <v-card tile>
 | 
			
		||||
            <v-card-text class="black--text">
 | 
			
		||||
              <v-row class="ml-3 mr-3">
 | 
			
		||||
                {{ type.name }}
 | 
			
		||||
                <v-spacer />
 | 
			
		||||
                <v-btn icon @click="editType(type)">
 | 
			
		||||
                  <v-icon>
 | 
			
		||||
                    {{ editIcon }}
 | 
			
		||||
                  </v-icon>
 | 
			
		||||
                </v-btn>
 | 
			
		||||
                <v-btn icon @click="deleteType(type)">
 | 
			
		||||
                  <v-icon>
 | 
			
		||||
                    {{ deleteIcon }}
 | 
			
		||||
                  </v-icon>
 | 
			
		||||
                </v-btn>
 | 
			
		||||
              </v-row>
 | 
			
		||||
            </v-card-text>
 | 
			
		||||
          </v-card>
 | 
			
		||||
        </div>
 | 
			
		||||
      </v-card-text>
 | 
			
		||||
      <v-dialog v-model="dialogType">
 | 
			
		||||
        <v-card>
 | 
			
		||||
          <v-card-title>
 | 
			
		||||
            {{ dialogTypeTitle }}
 | 
			
		||||
          </v-card-title>
 | 
			
		||||
          <v-card-text>
 | 
			
		||||
            <v-container>
 | 
			
		||||
              <v-text-field
 | 
			
		||||
                v-model="editedType.name"
 | 
			
		||||
                outlined
 | 
			
		||||
                label="Name der Kategorie"
 | 
			
		||||
              />
 | 
			
		||||
            </v-container>
 | 
			
		||||
          </v-card-text>
 | 
			
		||||
          <v-card-actions>
 | 
			
		||||
            <v-spacer />
 | 
			
		||||
            <v-btn text color="blue darken-1" @click="closeType()">
 | 
			
		||||
              Abbrechen
 | 
			
		||||
            </v-btn>
 | 
			
		||||
            <v-btn
 | 
			
		||||
              text
 | 
			
		||||
              color="blue darken-1"
 | 
			
		||||
              @click="saveType()"
 | 
			
		||||
              :disabled="inType(editedType.name)"
 | 
			
		||||
            >
 | 
			
		||||
              Speichern
 | 
			
		||||
            </v-btn>
 | 
			
		||||
          </v-card-actions>
 | 
			
		||||
        </v-card>
 | 
			
		||||
      </v-dialog>
 | 
			
		||||
    </v-card>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapGetters, mapActions } from 'vuex'
 | 
			
		||||
import { mdiMagnify, mdiPlus, mdiPencil, mdiDelete } from '@mdi/js'
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'PriceList',
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      editIcon: mdiPencil,
 | 
			
		||||
      deleteIcon: mdiDelete,
 | 
			
		||||
      searchIcon: mdiMagnify,
 | 
			
		||||
      plus: mdiPlus,
 | 
			
		||||
      searchType: null,
 | 
			
		||||
      search: null,
 | 
			
		||||
      dialog: null,
 | 
			
		||||
      dialogType: null,
 | 
			
		||||
      editedIndex: -1,
 | 
			
		||||
      headers: [
 | 
			
		||||
        {
 | 
			
		||||
          text: 'Name',
 | 
			
		||||
          value: 'name'
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          text: 'Kategorie',
 | 
			
		||||
          value: 'type'
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          text: 'Preis in €',
 | 
			
		||||
          value: 'price'
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          text: 'Preis groß in €',
 | 
			
		||||
          value: 'price_big'
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          text: 'Preis Club in €',
 | 
			
		||||
          value: 'price_club'
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          text: 'Preis groß Club in €',
 | 
			
		||||
          value: 'price_club_big'
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          text: 'Aufpreis in €',
 | 
			
		||||
          value: 'premium'
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          text: 'Aufpreis Club in €',
 | 
			
		||||
          value: 'premium_club'
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          text: 'Preis Club extern in €',
 | 
			
		||||
          value: 'price_extern_club'
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      editedItem: {
 | 
			
		||||
        name: null,
 | 
			
		||||
        type: { id: -1, name: null },
 | 
			
		||||
        price: null,
 | 
			
		||||
        price_big: null,
 | 
			
		||||
        price_club: null,
 | 
			
		||||
        price_club_big: null,
 | 
			
		||||
        premium: null,
 | 
			
		||||
        premium_club: null,
 | 
			
		||||
        price_extern_club: null
 | 
			
		||||
      },
 | 
			
		||||
      defaultItem: {
 | 
			
		||||
        name: null,
 | 
			
		||||
        type: { id: -1, name: null },
 | 
			
		||||
        price: null,
 | 
			
		||||
        price_big: null,
 | 
			
		||||
        price_club: null,
 | 
			
		||||
        price_club_big: null,
 | 
			
		||||
        premium: null,
 | 
			
		||||
        premium_club: null,
 | 
			
		||||
        price_extern_club: null
 | 
			
		||||
      },
 | 
			
		||||
      editedType: {
 | 
			
		||||
        id: -1,
 | 
			
		||||
        name: null
 | 
			
		||||
      },
 | 
			
		||||
      defaultType: {
 | 
			
		||||
        id: -1,
 | 
			
		||||
        name: null
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions({
 | 
			
		||||
      getPriceList: 'priceList/getPriceList',
 | 
			
		||||
      getTypes: 'priceList/getTypes',
 | 
			
		||||
      setDrink: 'priceList/setDrink',
 | 
			
		||||
      updateDrink: 'priceList/updateDrink',
 | 
			
		||||
      deleteDrink: 'priceList/deleteDrink',
 | 
			
		||||
      setDrinkType: 'priceList/setDrinkType',
 | 
			
		||||
      updateDrinkType: 'priceList/updateDrinkType',
 | 
			
		||||
      deleteDrinkType: 'priceList/deleteDrinkType'
 | 
			
		||||
    }),
 | 
			
		||||
    editType(item) {
 | 
			
		||||
      this.editedType = Object.assign({}, item)
 | 
			
		||||
      this.dialogType = true
 | 
			
		||||
    },
 | 
			
		||||
    closeType() {
 | 
			
		||||
      this.dialogType = false
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        this.editedType = Object.assign({}, this.defaultType)
 | 
			
		||||
      }, 300)
 | 
			
		||||
    },
 | 
			
		||||
    saveType() {
 | 
			
		||||
      this.editedType.id === -1
 | 
			
		||||
        ? this.setDrinkType(this.editedType)
 | 
			
		||||
        : this.updateDrinkType(this.editedType)
 | 
			
		||||
      this.closeType()
 | 
			
		||||
    },
 | 
			
		||||
    deleteType(item) {
 | 
			
		||||
      confirm('Bist du sicher, dass du diese Kategorie entfernen willst?') &&
 | 
			
		||||
        this.deleteDrinkType({ id: item.id })
 | 
			
		||||
    },
 | 
			
		||||
    addType() {
 | 
			
		||||
      this.setDrinkType({ name: this.searchType })
 | 
			
		||||
    },
 | 
			
		||||
    close() {
 | 
			
		||||
      this.dialog = false
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        this.editedItem = Object.assign({}, this.defaultItem)
 | 
			
		||||
        this.editedIndex = -1
 | 
			
		||||
      }, 300)
 | 
			
		||||
    },
 | 
			
		||||
    editItem(item) {
 | 
			
		||||
      this.editedIndex = item.id
 | 
			
		||||
      this.editedItem = Object.assign({}, item)
 | 
			
		||||
      for (let i in this.editedItem) {
 | 
			
		||||
        this.editedItem[i] = isNaN(this.editedItem[i])
 | 
			
		||||
          ? this.editedItem[i]
 | 
			
		||||
          : this.editedItem[i] == null || this.editedItem[i] == 0
 | 
			
		||||
          ? null
 | 
			
		||||
          : (this.editedItem[i] / 100).toFixed(2)
 | 
			
		||||
      }
 | 
			
		||||
      this.editedItem.type = Object.assign(
 | 
			
		||||
        {},
 | 
			
		||||
        this.types.find(a => a.id == item.type)
 | 
			
		||||
      )
 | 
			
		||||
      this.dialog = true
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    deleteItem(item) {
 | 
			
		||||
      confirm('Bist du sicher, dass die dieses Getränk entfernen wills?') &&
 | 
			
		||||
        this.deleteDrink({ id: item.id })
 | 
			
		||||
    },
 | 
			
		||||
    save() {
 | 
			
		||||
      var drink = {
 | 
			
		||||
        id: this.editedIndex,
 | 
			
		||||
        name: this.editedItem.name,
 | 
			
		||||
        type: this.editedItem.type.id,
 | 
			
		||||
        price: !isNaN(this.editedItem.price)
 | 
			
		||||
          ? this.editedItem.price * 100
 | 
			
		||||
          : null,
 | 
			
		||||
        price_big: !isNaN(this.editedItem.price_big)
 | 
			
		||||
          ? this.editedItem.price_big * 100
 | 
			
		||||
          : null,
 | 
			
		||||
        price_club: !isNaN(this.editedItem.price_club)
 | 
			
		||||
          ? this.editedItem.price_club * 100
 | 
			
		||||
          : null,
 | 
			
		||||
        price_club_big: !isNaN(this.editedItem.price_club_big)
 | 
			
		||||
          ? this.editedItem.price_club_big * 100
 | 
			
		||||
          : null,
 | 
			
		||||
        premium: !isNaN(this.editedItem.premium)
 | 
			
		||||
          ? this.editedItem.premium * 100
 | 
			
		||||
          : null,
 | 
			
		||||
        premium_club: !isNaN(this.editedItem.premium_club)
 | 
			
		||||
          ? this.editedItem.premium_club * 100
 | 
			
		||||
          : null,
 | 
			
		||||
        price_extern_club: !isNaN(this.editedItem.price_extern_club)
 | 
			
		||||
          ? this.editedItem.price_extern_club * 100
 | 
			
		||||
          : null
 | 
			
		||||
      }
 | 
			
		||||
      drink.id === -1 ? this.setDrink(drink) : this.updateDrink(drink)
 | 
			
		||||
      this.editedItem = Object.assign({}, this.defaultItem)
 | 
			
		||||
      this.close()
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters({
 | 
			
		||||
      priceList: 'priceList/priceList',
 | 
			
		||||
      types: 'priceList/types',
 | 
			
		||||
      priceListLoading: 'priceList/priceListLoading',
 | 
			
		||||
      typesLoading: 'priceList/typesLoading',
 | 
			
		||||
      isGastro: 'isGastro'
 | 
			
		||||
    }),
 | 
			
		||||
    isGastroPage() {
 | 
			
		||||
      return this.$route.name === 'gastroPricelist'
 | 
			
		||||
    },
 | 
			
		||||
    formTitle() {
 | 
			
		||||
      return this.editedIndex === -1 ? 'Neues Getränk' : 'Bearbeite Getränk'
 | 
			
		||||
    },
 | 
			
		||||
    inType() {
 | 
			
		||||
      return text => {
 | 
			
		||||
        return !!this.types.find(a => {
 | 
			
		||||
          if (a.name === null || text === null) {
 | 
			
		||||
            return true
 | 
			
		||||
          } else {
 | 
			
		||||
            return a.name.toLowerCase() === text.toLowerCase()
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    dialogTypeTitle() {
 | 
			
		||||
      return this.editedType.id === -1
 | 
			
		||||
        ? 'Neue Kategorie'
 | 
			
		||||
        : 'Bearbeite Kategorie'
 | 
			
		||||
    },
 | 
			
		||||
    computeType() {
 | 
			
		||||
      return id => {
 | 
			
		||||
        const type = this.types.find(a => {
 | 
			
		||||
          return a.id === id
 | 
			
		||||
        })
 | 
			
		||||
        return type ? type.name : null
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.getPriceList()
 | 
			
		||||
    this.getTypes()
 | 
			
		||||
    if (this.isGastro && this.isGastroPage) {
 | 
			
		||||
      this.headers.push({
 | 
			
		||||
        text: 'Aktion',
 | 
			
		||||
        value: 'action',
 | 
			
		||||
        sortable: false,
 | 
			
		||||
        filterable: false
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    console.log(this.$route)
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    dialog(val) {
 | 
			
		||||
      val || this.close()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,497 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <v-container>
 | 
			
		||||
    <v-dialog v-model="checkValidate" max-width="290">
 | 
			
		||||
      <v-card>
 | 
			
		||||
        <v-card-title>
 | 
			
		||||
          Willst du wirklich??
 | 
			
		||||
        </v-card-title>
 | 
			
		||||
        <v-card-text v-if="stornoMessage">
 | 
			
		||||
          Willst du wirklich den Betrag
 | 
			
		||||
          {{ (stornoMessage.amount / 100).toFixed(2) }}€ von
 | 
			
		||||
          {{ stornoMessage.user.firstname }}
 | 
			
		||||
          {{ stornoMessage.user.lastname }} stornieren?
 | 
			
		||||
        </v-card-text>
 | 
			
		||||
        <v-card-actions>
 | 
			
		||||
          <v-spacer />
 | 
			
		||||
          <v-btn text @click="cancelStorno">Abbrechen</v-btn>
 | 
			
		||||
          <v-btn text @click="acceptStorno">Stornieren</v-btn>
 | 
			
		||||
        </v-card-actions>
 | 
			
		||||
      </v-card>
 | 
			
		||||
    </v-dialog>
 | 
			
		||||
    <v-dialog v-model="dialog" max-width="290">
 | 
			
		||||
      <v-card>
 | 
			
		||||
        <v-card-title class="headline"
 | 
			
		||||
          >Transaktion ist länger als 15 Sekunden her!</v-card-title
 | 
			
		||||
        >
 | 
			
		||||
        <v-card-text>
 | 
			
		||||
          Da die Transaktion länger als 15 Sekunden her ist, kann eine
 | 
			
		||||
          Stornierung nicht durchgeführt werden. Wende dich bitte an den
 | 
			
		||||
          Finanzer.
 | 
			
		||||
        </v-card-text>
 | 
			
		||||
        <v-card-actions>
 | 
			
		||||
          <v-spacer />
 | 
			
		||||
          <v-btn text @click="dialog = false">
 | 
			
		||||
            Verstanden
 | 
			
		||||
          </v-btn>
 | 
			
		||||
        </v-card-actions>
 | 
			
		||||
      </v-card>
 | 
			
		||||
    </v-dialog>
 | 
			
		||||
    <v-dialog
 | 
			
		||||
            v-if="overLimitUser"
 | 
			
		||||
            v-model="overLimitUser"
 | 
			
		||||
            max-width="290"
 | 
			
		||||
            persistent
 | 
			
		||||
    >
 | 
			
		||||
      <v-card>
 | 
			
		||||
        <v-card-title>Warnung</v-card-title>
 | 
			
		||||
        <v-card-text>
 | 
			
		||||
          {{ overLimitUser.firstname }} {{ overLimitUser.lastname }} übersteigt
 | 
			
		||||
          das Anschreibelimit von
 | 
			
		||||
          {{ (overLimitUser.limit / 100).toFixed(2) }} €. Danach kann dieses
 | 
			
		||||
          Mitglied nichts mehr anschreiben. Will er das wirklich?
 | 
			
		||||
        </v-card-text>
 | 
			
		||||
        <v-card-actions>
 | 
			
		||||
          <v-spacer />
 | 
			
		||||
          <v-btn text @click="cancel()">Abbrechen</v-btn>
 | 
			
		||||
          <v-btn text @click="continueAdd(overLimitUser)">Anschreiben</v-btn>
 | 
			
		||||
        </v-card-actions>
 | 
			
		||||
      </v-card>
 | 
			
		||||
    </v-dialog>
 | 
			
		||||
    <v-dialog v-if="overOverLimit" v-model="overOverLimit" max-width="290" persistent>
 | 
			
		||||
      <v-card>
 | 
			
		||||
        <v-card-title>Anschreiben nicht möglich</v-card-title>
 | 
			
		||||
        <v-card-text>
 | 
			
		||||
          {{ overOverLimit.firstname }}
 | 
			
		||||
          {{ overOverLimit.lastname }} überschreitet das Anschreibelimit zuviel.
 | 
			
		||||
          Das Anschreiben wurde daher gestoppt und zurückgesetzt.
 | 
			
		||||
        </v-card-text>
 | 
			
		||||
        <v-card-actions>
 | 
			
		||||
          <v-btn text @click="overOverLimit = null">Verstanden</v-btn>
 | 
			
		||||
        </v-card-actions>
 | 
			
		||||
      </v-card>
 | 
			
		||||
    </v-dialog>
 | 
			
		||||
    <AddAmountSkeleton v-if="loading" />
 | 
			
		||||
    <v-navigation-drawer v-model="menu" right app clipped>
 | 
			
		||||
      <v-list-item-group :key="componentRenderer">
 | 
			
		||||
        <v-list-item inactive>
 | 
			
		||||
          <v-list-item-title class="headline">
 | 
			
		||||
            Verlauf
 | 
			
		||||
          </v-list-item-title>
 | 
			
		||||
        </v-list-item>
 | 
			
		||||
        <v-divider />
 | 
			
		||||
        <div
 | 
			
		||||
          v-for="message in messages"
 | 
			
		||||
          three-line
 | 
			
		||||
          :key="messages.indexOf(message)"
 | 
			
		||||
        >
 | 
			
		||||
          <div v-if="message">
 | 
			
		||||
            <v-list-item three-line inactive @click="storno(message)">
 | 
			
		||||
              <v-list-item-content>
 | 
			
		||||
                <v-progress-linear indeterminate v-if="message.loading" />
 | 
			
		||||
                <v-list-item-title>{{ now(message.date) }}</v-list-item-title>
 | 
			
		||||
                <v-list-item-subtitle>
 | 
			
		||||
                  {{ createSum(message) }} {{ createMessage(message) }}
 | 
			
		||||
                </v-list-item-subtitle>
 | 
			
		||||
                <v-list-item-subtitle class="red--text" v-if="message.storno"
 | 
			
		||||
                  >STORNIERT!!!
 | 
			
		||||
                </v-list-item-subtitle>
 | 
			
		||||
                <v-list-item-subtitle class="red--text" v-else-if="message.error">
 | 
			
		||||
                  ERROR!
 | 
			
		||||
                </v-list-item-subtitle>
 | 
			
		||||
                <v-list-item-action-text v-if="under5minutes(message.date) && !message.error"
 | 
			
		||||
                  >Klicken um zu Stornieren
 | 
			
		||||
                </v-list-item-action-text>
 | 
			
		||||
              </v-list-item-content>
 | 
			
		||||
            </v-list-item>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </v-list-item-group>
 | 
			
		||||
    </v-navigation-drawer>
 | 
			
		||||
    <v-card v-if="!loading" :loading="addLoading">
 | 
			
		||||
      <v-card-title>
 | 
			
		||||
        {{ user.firstname }} {{ user.lastname }}
 | 
			
		||||
        <v-spacer />
 | 
			
		||||
        <v-btn @click="menu = !menu" icon>
 | 
			
		||||
          <v-icon>{{ menuIcon }}</v-icon>
 | 
			
		||||
        </v-btn>
 | 
			
		||||
      </v-card-title>
 | 
			
		||||
      <v-card-subtitle v-if="user.limit + getAllSum() > 0">
 | 
			
		||||
        Nur noch {{ ((user.limit + getAllSum()) / 100).toFixed(2) }} €
 | 
			
		||||
        übrig!!
 | 
			
		||||
      </v-card-subtitle>
 | 
			
		||||
      <v-card-text>
 | 
			
		||||
        <v-row>
 | 
			
		||||
          <v-col cols="10">
 | 
			
		||||
            <v-row>
 | 
			
		||||
              <v-col cols="6" sm="4">
 | 
			
		||||
                <v-btn
 | 
			
		||||
                  class="creditBtn"
 | 
			
		||||
                  block
 | 
			
		||||
                  @click="addingAmount(200)"
 | 
			
		||||
                  :color="color"
 | 
			
		||||
                  :disabled="user.locked"
 | 
			
		||||
                  >2 €</v-btn
 | 
			
		||||
                >
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col cols="6" sm="4">
 | 
			
		||||
                <v-btn
 | 
			
		||||
                  class="creditBtn"
 | 
			
		||||
                  block
 | 
			
		||||
                  @click="addingAmount(100)"
 | 
			
		||||
                  :color="color"
 | 
			
		||||
                  :disabled="user.locked"
 | 
			
		||||
                  >1 €</v-btn
 | 
			
		||||
                >
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col cols="6" sm="4">
 | 
			
		||||
                <v-btn
 | 
			
		||||
                  class="creditBtn"
 | 
			
		||||
                  block
 | 
			
		||||
                  @click="addingAmount(50)"
 | 
			
		||||
                  :color="color"
 | 
			
		||||
                  :disabled="user.locked"
 | 
			
		||||
                  >0,50 €</v-btn
 | 
			
		||||
                >
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col cols="6" sm="4">
 | 
			
		||||
                <v-btn
 | 
			
		||||
                  class="creditBtn"
 | 
			
		||||
                  block
 | 
			
		||||
                  @click="addingAmount(40)"
 | 
			
		||||
                  :color="color"
 | 
			
		||||
                  :disabled="user.locked"
 | 
			
		||||
                  >0,40 €</v-btn
 | 
			
		||||
                >
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col cols="6" sm="4">
 | 
			
		||||
                <v-btn
 | 
			
		||||
                  class="creditBtn"
 | 
			
		||||
                  block
 | 
			
		||||
                  @click="addingAmount(20)"
 | 
			
		||||
                  :color="color"
 | 
			
		||||
                  :disabled="user.locked"
 | 
			
		||||
                  >0,20 €</v-btn
 | 
			
		||||
                >
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col cols="6" sm="4">
 | 
			
		||||
                <v-btn
 | 
			
		||||
                  class="creditBtn"
 | 
			
		||||
                  block
 | 
			
		||||
                  @click="addingAmount(10)"
 | 
			
		||||
                  :color="color"
 | 
			
		||||
                  :disabled="user.locked"
 | 
			
		||||
                  >0,10 €</v-btn
 | 
			
		||||
                >
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col cols="8">
 | 
			
		||||
                <v-text-field
 | 
			
		||||
                  outlined
 | 
			
		||||
                  type="number"
 | 
			
		||||
                  v-model="value"
 | 
			
		||||
                  label="Benutzerdefinierter Betrag"
 | 
			
		||||
                  :disabled="user.locked"
 | 
			
		||||
                ></v-text-field>
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col cols="4">
 | 
			
		||||
                <v-btn
 | 
			
		||||
                  fab
 | 
			
		||||
                  :color="color"
 | 
			
		||||
                  @click="addAmountMore()"
 | 
			
		||||
                  :disabled="user.locked"
 | 
			
		||||
                >
 | 
			
		||||
                  <v-icon>{{ plus }}</v-icon>
 | 
			
		||||
                </v-btn>
 | 
			
		||||
              </v-col>
 | 
			
		||||
            </v-row>
 | 
			
		||||
          </v-col>
 | 
			
		||||
          <v-col align-self="center">
 | 
			
		||||
            <v-row>
 | 
			
		||||
              <v-list-item>
 | 
			
		||||
                <v-list-item-content class="text-center">
 | 
			
		||||
                  <v-list-item-action-text :class="getColor(getAllSum())"
 | 
			
		||||
                    >{{ (getAllSum() / 100).toFixed(2) }}
 | 
			
		||||
                    €
 | 
			
		||||
                  </v-list-item-action-text>
 | 
			
		||||
                  <v-list-item-action-text v-if="toSetAmount">
 | 
			
		||||
                    - {{ (toSetAmount / 100).toFixed(2) }}
 | 
			
		||||
                  </v-list-item-action-text>
 | 
			
		||||
                </v-list-item-content>
 | 
			
		||||
              </v-list-item>
 | 
			
		||||
            </v-row>
 | 
			
		||||
          </v-col>
 | 
			
		||||
        </v-row>
 | 
			
		||||
        <v-alert v-if="user.locked" type="error"
 | 
			
		||||
          >{{ user.firstname }} darf nicht mehr anschreiben.
 | 
			
		||||
          {{ user.firstname }} sollte sich lieber mal beim Finanzer
 | 
			
		||||
          melden.</v-alert
 | 
			
		||||
        >
 | 
			
		||||
      </v-card-text>
 | 
			
		||||
    </v-card>
 | 
			
		||||
    <v-snackbar
 | 
			
		||||
      v-for="message in messages"
 | 
			
		||||
      :key="messages.indexOf(message)"
 | 
			
		||||
      :color="message.error ? 'error' : 'success'"
 | 
			
		||||
      bottom
 | 
			
		||||
      :timeout="0"
 | 
			
		||||
      :multi-line="true"
 | 
			
		||||
      v-model="message.visible"
 | 
			
		||||
      vertical
 | 
			
		||||
    >
 | 
			
		||||
      <div class="title">
 | 
			
		||||
        <p style="font-size: 5em; margin: 20px">{{ createSum(message) }}</p>
 | 
			
		||||
        {{ createMessage(message) }}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div>
 | 
			
		||||
        {{ now(message.date) }}
 | 
			
		||||
      </div>
 | 
			
		||||
      <v-btn color="white" icon @click="message.visible = false">
 | 
			
		||||
        <v-icon>
 | 
			
		||||
          {{ close }}
 | 
			
		||||
        </v-icon>
 | 
			
		||||
      </v-btn>
 | 
			
		||||
    </v-snackbar>
 | 
			
		||||
    <ConnectionError/>
 | 
			
		||||
  </v-container>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapGetters, mapActions } from 'vuex'
 | 
			
		||||
// eslint-disable-next-line no-unused-vars
 | 
			
		||||
import { mdiMenu, mdiPlus, mdiClose } from '@mdi/js'
 | 
			
		||||
import AddAmountSkeleton from './Skeleton/AddAmountSkeleton'
 | 
			
		||||
import ConnectionError from "@/components/ConnectionError";
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'AddAmount',
 | 
			
		||||
  components: {ConnectionError, AddAmountSkeleton },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      color: 'green accent-4',
 | 
			
		||||
      value: null,
 | 
			
		||||
      plus: mdiPlus,
 | 
			
		||||
      menu: false,
 | 
			
		||||
      dialog: false,
 | 
			
		||||
      componentRenderer: 0,
 | 
			
		||||
      timer: '',
 | 
			
		||||
      menuIcon: mdiMenu,
 | 
			
		||||
      close: mdiClose,
 | 
			
		||||
      checkValidate: false,
 | 
			
		||||
      stornoMessage: null,
 | 
			
		||||
      timeout: null,
 | 
			
		||||
      toSetAmount: null,
 | 
			
		||||
      overLimitUser: null,
 | 
			
		||||
      overOverLimit: null,
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.timer = setInterval(this.forceRender, 1000)
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions({
 | 
			
		||||
      addAmount: 'user/addAmount',
 | 
			
		||||
      commitStorno: 'user/storno'
 | 
			
		||||
    }),
 | 
			
		||||
    continueAdd(user) {
 | 
			
		||||
      this.overLimitUser = null
 | 
			
		||||
      user.checkedOverLimit = true
 | 
			
		||||
      if (this.value) {
 | 
			
		||||
        this.addAmount(Math.round(Math.abs(this.value * 100)))
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          this.value = null
 | 
			
		||||
          this.toSetAmount = null
 | 
			
		||||
        }, 300)
 | 
			
		||||
      } else {
 | 
			
		||||
        user.timeout = setTimeout(() => {
 | 
			
		||||
          this.addAmount(this.toSetAmount)
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            this.toSetAmount = null
 | 
			
		||||
          }, 300)
 | 
			
		||||
        }, 2000)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    cancel() {
 | 
			
		||||
      this.toSetAmount = null
 | 
			
		||||
      this.value = null
 | 
			
		||||
      this.overLimitUser = null
 | 
			
		||||
    },
 | 
			
		||||
    checkOverLimitIsValid(user) {
 | 
			
		||||
      if (this.toSetAmount && user.autoLock) {
 | 
			
		||||
        if ((this.getAllSum() - Number.parseInt(this.toSetAmount)) < -(user.limit + 500)) {
 | 
			
		||||
          this.overOverLimit = user
 | 
			
		||||
          this.toSetAmount = null
 | 
			
		||||
          this.value = null
 | 
			
		||||
          return false
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return true
 | 
			
		||||
    },
 | 
			
		||||
    checkOverLimit(user) {
 | 
			
		||||
      if (this.toSetAmount) {
 | 
			
		||||
        if (( this.getAllSum() - this.toSetAmount) < -user.limit) {
 | 
			
		||||
          return user.checkedOverLimit ? false : true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return false
 | 
			
		||||
    },
 | 
			
		||||
    addingAmount(amount) {
 | 
			
		||||
      clearTimeout(this.timeout)
 | 
			
		||||
      this.toSetAmount = this.toSetAmount ? this.toSetAmount + amount : amount
 | 
			
		||||
      if (this.checkOverLimitIsValid(this.user)) {
 | 
			
		||||
        if (this.checkOverLimit(this.user) && this.user.autoLock) {
 | 
			
		||||
          this.overLimitUser = this.user
 | 
			
		||||
        } else {
 | 
			
		||||
          this.timeout = setTimeout(() => {
 | 
			
		||||
            this.addAmount(this.toSetAmount)
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              this.toSetAmount = null
 | 
			
		||||
            }, 300)
 | 
			
		||||
          }, 2000)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    forceRender() {
 | 
			
		||||
      this.componentRenderer += 1
 | 
			
		||||
    },
 | 
			
		||||
    getColor(value) {
 | 
			
		||||
      return value >= 0 ? 'title green--text' : 'title red--text'
 | 
			
		||||
    },
 | 
			
		||||
    getAllSum() {
 | 
			
		||||
      if (this.user)
 | 
			
		||||
        return (
 | 
			
		||||
          this.user.creditList[this.year][2].sum +
 | 
			
		||||
          this.user.creditList[this.year][1].last
 | 
			
		||||
        )
 | 
			
		||||
      return 0
 | 
			
		||||
    },
 | 
			
		||||
    storno(message) {
 | 
			
		||||
      if (!message.error) {
 | 
			
		||||
        if (!this.under5minutes(message.date)) this.dialog = true
 | 
			
		||||
        else {
 | 
			
		||||
          this.checkValidate = true
 | 
			
		||||
          this.stornoMessage = message
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    acceptStorno() {
 | 
			
		||||
      this.commitStorno({
 | 
			
		||||
        amount: this.stornoMessage.amount,
 | 
			
		||||
        date: this.stornoMessage.date
 | 
			
		||||
      })
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        this.cancelStorno()
 | 
			
		||||
      }, 300)
 | 
			
		||||
    },
 | 
			
		||||
    cancelStorno() {
 | 
			
		||||
      this.stornoMessage = null
 | 
			
		||||
      this.checkValidate = null
 | 
			
		||||
    },
 | 
			
		||||
    addAmountMore() {
 | 
			
		||||
      this.toSetAmount = this.toSetAmount
 | 
			
		||||
              ? this.toSetAmount + Math.round(Math.abs(this.value * 100))
 | 
			
		||||
              : Math.round(Math.abs(this.value * 100))
 | 
			
		||||
      if (this.checkOverLimitIsValid(this.user)) {
 | 
			
		||||
        if (this.checkOverLimit(this.user) && this.user.autoLock) {
 | 
			
		||||
          this.overLimitUser = this.user
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          this.addAmount(Math.abs(this.value * 100))
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            this.value = null
 | 
			
		||||
          }, 300)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    createSum(message) {
 | 
			
		||||
      var text = '' + (message.amount / 100).toFixed(2) + '€'
 | 
			
		||||
      return text
 | 
			
		||||
    },
 | 
			
		||||
    createMessage(message) {
 | 
			
		||||
      var text = ''
 | 
			
		||||
      if (message.error) {
 | 
			
		||||
        text =
 | 
			
		||||
          ' konnten nicht zu ' +
 | 
			
		||||
          message.user.firstname +
 | 
			
		||||
          ' ' +
 | 
			
		||||
          message.user.lastname +
 | 
			
		||||
          ' hinzufügen.'
 | 
			
		||||
      } else {
 | 
			
		||||
        text =
 | 
			
		||||
          ' wurde zu ' +
 | 
			
		||||
          message.user.firstname +
 | 
			
		||||
          ' ' +
 | 
			
		||||
          message.user.lastname +
 | 
			
		||||
          ' hinzugefügt.'
 | 
			
		||||
      }
 | 
			
		||||
      return text
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters({
 | 
			
		||||
      user: 'user/user',
 | 
			
		||||
      year: 'user/year',
 | 
			
		||||
      loading: 'user/loading',
 | 
			
		||||
      addLoading: 'user/addLoading',
 | 
			
		||||
      messages: 'user/messages'
 | 
			
		||||
    }),
 | 
			
		||||
    under5minutes() {
 | 
			
		||||
      return now => {
 | 
			
		||||
        var actual = new Date()
 | 
			
		||||
        return actual - now < 15000
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    now() {
 | 
			
		||||
      return now => {
 | 
			
		||||
        var actual = new Date()
 | 
			
		||||
        var zero = new Date(0)
 | 
			
		||||
        var date = new Date(actual - now)
 | 
			
		||||
        if (date.getFullYear() === zero.getFullYear()) {
 | 
			
		||||
          if (date.getMonth() === zero.getMonth()) {
 | 
			
		||||
            if (date.getDate() === zero.getDate()) {
 | 
			
		||||
              if (date.getHours() === zero.getDate()) {
 | 
			
		||||
                if (date.getMinutes() < 1) {
 | 
			
		||||
                  return 'vor ' + date.getSeconds() + ' Sekunden'
 | 
			
		||||
                } else if (date.getMinutes() < 10) {
 | 
			
		||||
                  return 'vor ' + date.getMinutes() + ' Minuten'
 | 
			
		||||
                } else {
 | 
			
		||||
                  return (
 | 
			
		||||
                    (now.getHours() < 10 ? '0' : '') +
 | 
			
		||||
                    now.getHours() +
 | 
			
		||||
                    ':' +
 | 
			
		||||
                    (now.getMinutes() < 10 ? '0' : '') +
 | 
			
		||||
                    now.getMinutes()
 | 
			
		||||
                  )
 | 
			
		||||
                }
 | 
			
		||||
              } else {
 | 
			
		||||
                return (
 | 
			
		||||
                  (now.getHours() < 10 ? '0' : '') +
 | 
			
		||||
                  now.getHours() +
 | 
			
		||||
                  ':' +
 | 
			
		||||
                  (now.getMinutes() < 10 ? '0' : '') +
 | 
			
		||||
                  now.getMinutes()
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return (
 | 
			
		||||
          now.getDate() +
 | 
			
		||||
          '.' +
 | 
			
		||||
          now.getMonth() +
 | 
			
		||||
          '.' +
 | 
			
		||||
          now.getFullYear() +
 | 
			
		||||
          ' ' +
 | 
			
		||||
          (now.getHours() < 10 ? '0' : '') +
 | 
			
		||||
          now.getHours() +
 | 
			
		||||
          ':' +
 | 
			
		||||
          (now.getMinutes() < 10 ? '0' : '') +
 | 
			
		||||
          now.getMinutes()
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    clearInterval(this.timer)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,458 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <v-card v-if="user" :loading="loading" style="margin-top: 3px">
 | 
			
		||||
      <v-card-title>{{ user.firstname }} {{ user.lastname }}</v-card-title>
 | 
			
		||||
      <v-card-text>
 | 
			
		||||
        <v-row>
 | 
			
		||||
          <v-col cols="12" sm="6">
 | 
			
		||||
            <v-text-field
 | 
			
		||||
              outlined
 | 
			
		||||
              label="Vornamen"
 | 
			
		||||
              :placeholder="user.firstname"
 | 
			
		||||
              v-model="firstname"
 | 
			
		||||
              readonly
 | 
			
		||||
            />
 | 
			
		||||
          </v-col>
 | 
			
		||||
          <v-col cols="12" sm="6">
 | 
			
		||||
            <v-text-field
 | 
			
		||||
              outlined
 | 
			
		||||
              label="Nachname"
 | 
			
		||||
              :placeholder="user.lastname"
 | 
			
		||||
              v-model="lastname"
 | 
			
		||||
              readonly
 | 
			
		||||
            />
 | 
			
		||||
          </v-col>
 | 
			
		||||
        </v-row>
 | 
			
		||||
        <v-row>
 | 
			
		||||
          <v-col cols="12" sm="6">
 | 
			
		||||
            <v-text-field
 | 
			
		||||
              outlined
 | 
			
		||||
              label="Benutzername"
 | 
			
		||||
              :placeholder="user.username"
 | 
			
		||||
              v-model="username"
 | 
			
		||||
              readonly
 | 
			
		||||
            ></v-text-field>
 | 
			
		||||
          </v-col>
 | 
			
		||||
          <v-col cols="12" sm="6">
 | 
			
		||||
            <v-text-field
 | 
			
		||||
              ref="mail"
 | 
			
		||||
              outlined
 | 
			
		||||
              label="E-Mail"
 | 
			
		||||
              :placeholder="user.mail"
 | 
			
		||||
              v-model="mail"
 | 
			
		||||
              readonly
 | 
			
		||||
            ></v-text-field>
 | 
			
		||||
          </v-col>
 | 
			
		||||
        </v-row>
 | 
			
		||||
        <v-row>
 | 
			
		||||
          <v-col cols="12" sm="6">
 | 
			
		||||
            <v-text-field
 | 
			
		||||
              outlined
 | 
			
		||||
              label="neues Password"
 | 
			
		||||
              type="password"
 | 
			
		||||
              v-model="password"
 | 
			
		||||
            />
 | 
			
		||||
          </v-col>
 | 
			
		||||
          <v-col cols="12" sm="6">
 | 
			
		||||
            <v-form ref="newPassword">
 | 
			
		||||
              <v-text-field
 | 
			
		||||
                ref="password"
 | 
			
		||||
                v-model="controlPassword"
 | 
			
		||||
                outlined
 | 
			
		||||
                label="neues Password bestätigen"
 | 
			
		||||
                type="password"
 | 
			
		||||
                :disabled="!password"
 | 
			
		||||
                :rules="[equal_password]"
 | 
			
		||||
              />
 | 
			
		||||
            </v-form>
 | 
			
		||||
          </v-col>
 | 
			
		||||
        </v-row>
 | 
			
		||||
        <v-divider />
 | 
			
		||||
        <v-row>
 | 
			
		||||
          <v-col cols="12" sm="4">
 | 
			
		||||
            <v-text-field
 | 
			
		||||
              outlined
 | 
			
		||||
              label="Sperrlimit"
 | 
			
		||||
              readonly
 | 
			
		||||
              :value="(user.limit / 100).toFixed(2).toString() + '€'"
 | 
			
		||||
            />
 | 
			
		||||
          </v-col>
 | 
			
		||||
          <v-col cols="12" sm="4">
 | 
			
		||||
            <v-combobox
 | 
			
		||||
              outlined
 | 
			
		||||
              label="Sperrstatus"
 | 
			
		||||
              v-model="lock"
 | 
			
		||||
              append-icon
 | 
			
		||||
              readonly
 | 
			
		||||
            >
 | 
			
		||||
              <template v-slot:selection="data">
 | 
			
		||||
                <v-chip :color="lockColor">
 | 
			
		||||
                  {{ data.item }}
 | 
			
		||||
                </v-chip>
 | 
			
		||||
              </template>
 | 
			
		||||
            </v-combobox>
 | 
			
		||||
          </v-col>
 | 
			
		||||
          <v-col cols="12" sm="4">
 | 
			
		||||
            <v-combobox
 | 
			
		||||
              outlined
 | 
			
		||||
              label="Autosperre"
 | 
			
		||||
              v-model="autoLock"
 | 
			
		||||
              readonly
 | 
			
		||||
              append-icon
 | 
			
		||||
            >
 | 
			
		||||
              <template v-slot:selection="data">
 | 
			
		||||
                <v-chip :color="autoLockColor">
 | 
			
		||||
                  {{ data.item }}
 | 
			
		||||
                </v-chip>
 | 
			
		||||
              </template>
 | 
			
		||||
            </v-combobox>
 | 
			
		||||
          </v-col>
 | 
			
		||||
        </v-row>
 | 
			
		||||
        <v-row>
 | 
			
		||||
          <v-col v-bind:class="{ fulllineText: isFulllineText }">
 | 
			
		||||
            <v-combobox
 | 
			
		||||
              outlined
 | 
			
		||||
              multiple
 | 
			
		||||
              label="Gruppen"
 | 
			
		||||
              readonly
 | 
			
		||||
              v-model="user.group"
 | 
			
		||||
              append-icon
 | 
			
		||||
            >
 | 
			
		||||
              <template v-slot:selection="data">
 | 
			
		||||
                <v-icon class="ma-2">{{
 | 
			
		||||
                  data.item === 'user'
 | 
			
		||||
                    ? person
 | 
			
		||||
                    : data.item === 'bar'
 | 
			
		||||
                    ? bar
 | 
			
		||||
                    : data.item === 'moneymaster'
 | 
			
		||||
                    ? finanzer
 | 
			
		||||
                    : data.item === 'gastro'
 | 
			
		||||
                    ? gastro
 | 
			
		||||
                    : ''
 | 
			
		||||
                }}</v-icon>
 | 
			
		||||
              </template>
 | 
			
		||||
            </v-combobox>
 | 
			
		||||
          </v-col>
 | 
			
		||||
          <v-col v-bind:class="{ fulllineText: isFulllineText }">
 | 
			
		||||
            <v-text-field
 | 
			
		||||
              outlined
 | 
			
		||||
              :value="computeStatus"
 | 
			
		||||
              readonly
 | 
			
		||||
              label="Mitgliedsstatus"
 | 
			
		||||
            />
 | 
			
		||||
          </v-col>
 | 
			
		||||
          <v-col v-bind:class="{ fulllineText: isFulllineText }">
 | 
			
		||||
            <v-text-field
 | 
			
		||||
              outlined
 | 
			
		||||
              :value="user.voting ? 'ja' : 'nein'"
 | 
			
		||||
              readonly
 | 
			
		||||
              label="Stimmrecht"
 | 
			
		||||
            />
 | 
			
		||||
          </v-col>
 | 
			
		||||
        </v-row>
 | 
			
		||||
        <v-row>
 | 
			
		||||
          <v-col v-bind:class="{ fulllineText: isFulllineText }">
 | 
			
		||||
            <v-combobox
 | 
			
		||||
              chips
 | 
			
		||||
              outlined
 | 
			
		||||
              multiple
 | 
			
		||||
              label="Arbeitsgruppen"
 | 
			
		||||
              readonly
 | 
			
		||||
              v-model="user.workgroups"
 | 
			
		||||
              item-value="id"
 | 
			
		||||
              item-text="name"
 | 
			
		||||
              append-icon
 | 
			
		||||
            >
 | 
			
		||||
              <template v-slot:selection="data">
 | 
			
		||||
                <v-chip>{{ data.item.name }}</v-chip>
 | 
			
		||||
              </template>
 | 
			
		||||
            </v-combobox>
 | 
			
		||||
          </v-col>
 | 
			
		||||
        </v-row>
 | 
			
		||||
        <div class="subtitle-1">
 | 
			
		||||
          Gespeicherte Sessions
 | 
			
		||||
        </div>
 | 
			
		||||
        <v-card v-for="token in tokens" :key="token.id" outlined>
 | 
			
		||||
          <v-card-text>
 | 
			
		||||
            <v-row>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  Betriebssystem
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-icon>
 | 
			
		||||
                    {{
 | 
			
		||||
                      token.platform === 'macos' || token.platform === 'iphone'
 | 
			
		||||
                        ? apple
 | 
			
		||||
                        : token.platform === 'windows'
 | 
			
		||||
                        ? windows
 | 
			
		||||
                        : token.platform === 'android'
 | 
			
		||||
                        ? android
 | 
			
		||||
                        : token.platform === 'linux'
 | 
			
		||||
                        ? linux
 | 
			
		||||
                        : token.platfrom
 | 
			
		||||
                    }}
 | 
			
		||||
                  </v-icon>
 | 
			
		||||
                  <v-icon
 | 
			
		||||
                    v-if="
 | 
			
		||||
                      token.platform === 'macos' || token.platform === 'iphone'
 | 
			
		||||
                    "
 | 
			
		||||
                  >
 | 
			
		||||
                    {{ token.platform === 'macos' ? mac : iphone }}
 | 
			
		||||
                  </v-icon>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  Browser
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-icon>
 | 
			
		||||
                    {{
 | 
			
		||||
                      token.browser === 'chrome'
 | 
			
		||||
                        ? chrome
 | 
			
		||||
                        : token.browser === 'firefox'
 | 
			
		||||
                        ? firefox
 | 
			
		||||
                        : token.browser === 'opera'
 | 
			
		||||
                        ? opera
 | 
			
		||||
                        : token.browser === 'safari'
 | 
			
		||||
                        ? safari
 | 
			
		||||
                        : token.browser === 'msie'
 | 
			
		||||
                        ? msie
 | 
			
		||||
                        : token.browser
 | 
			
		||||
                    }}
 | 
			
		||||
                  </v-icon>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  Letzte Aktualisierung
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  {{ token.timestamp.day }}.{{ token.timestamp.month }}.{{
 | 
			
		||||
                    token.timestamp.year
 | 
			
		||||
                  }}
 | 
			
		||||
                  um
 | 
			
		||||
                  {{
 | 
			
		||||
                    10 > token.timestamp.hour
 | 
			
		||||
                      ? '0' + String(token.timestamp.hour)
 | 
			
		||||
                      : token.timestamp.hour
 | 
			
		||||
                  }}:{{
 | 
			
		||||
                    10 > token.timestamp.minute
 | 
			
		||||
                      ? '0' + String(token.timestamp.minute)
 | 
			
		||||
                      : token.timestamp.minute
 | 
			
		||||
                  }}:{{
 | 
			
		||||
                    10 > token.timestamp.second
 | 
			
		||||
                      ? '0' + String(token.timestamp.second)
 | 
			
		||||
                      : token.timestamp.second
 | 
			
		||||
                  }}</v-col
 | 
			
		||||
                >
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  Lebenszeit
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  {{ calcLifefime(token.lifetime) }}
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col class="text-right">
 | 
			
		||||
                <v-btn icon @click="deleteToken(token)">
 | 
			
		||||
                  <v-icon>
 | 
			
		||||
                    {{ trashCan }}
 | 
			
		||||
                  </v-icon>
 | 
			
		||||
                </v-btn>
 | 
			
		||||
              </v-col>
 | 
			
		||||
            </v-row>
 | 
			
		||||
          </v-card-text>
 | 
			
		||||
        </v-card>
 | 
			
		||||
      </v-card-text>
 | 
			
		||||
      <v-card-actions>
 | 
			
		||||
        <v-spacer></v-spacer>
 | 
			
		||||
        <v-form ref="acceptedPasswordTest">
 | 
			
		||||
          <v-text-field
 | 
			
		||||
            outlined
 | 
			
		||||
            label="Passwort"
 | 
			
		||||
            v-model="acceptedPassword"
 | 
			
		||||
            type="password"
 | 
			
		||||
            ref="acceptedPassword"
 | 
			
		||||
            :rules="[empty_password]"
 | 
			
		||||
          ></v-text-field>
 | 
			
		||||
        </v-form>
 | 
			
		||||
        <v-btn text color="primary" @click="save">Speichern</v-btn>
 | 
			
		||||
      </v-card-actions>
 | 
			
		||||
      <v-snackbar
 | 
			
		||||
              v-if="error ? error.value : false"
 | 
			
		||||
        :color="error ? (error.error ? 'error' : 'success') : ''"
 | 
			
		||||
        :value="error"
 | 
			
		||||
        v-model="error"
 | 
			
		||||
        :timeout="0"
 | 
			
		||||
      >
 | 
			
		||||
        {{ error ? error.value : null }}
 | 
			
		||||
      </v-snackbar>
 | 
			
		||||
    </v-card>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import {
 | 
			
		||||
  mdiAccount,
 | 
			
		||||
  mdiGlassCocktail,
 | 
			
		||||
  mdiCurrencyEur,
 | 
			
		||||
  mdiFoodForkDrink,
 | 
			
		||||
  mdiApple,
 | 
			
		||||
  mdiGoogleChrome,
 | 
			
		||||
  mdiFirefox,
 | 
			
		||||
  mdiOpera,
 | 
			
		||||
  mdiInternetExplorer,
 | 
			
		||||
  mdiAppleSafari,
 | 
			
		||||
  mdiLaptopMac,
 | 
			
		||||
  mdiCellphoneIphone,
 | 
			
		||||
  mdiTrashCan,
 | 
			
		||||
  mdiAndroid,
 | 
			
		||||
  mdiWindows,
 | 
			
		||||
  mdiLinux
 | 
			
		||||
} from '@mdi/js'
 | 
			
		||||
import { mapGetters, mapActions } from 'vuex'
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'Config',
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      apple: mdiApple,
 | 
			
		||||
      mac: mdiLaptopMac,
 | 
			
		||||
      iphone: mdiCellphoneIphone,
 | 
			
		||||
      android: mdiAndroid,
 | 
			
		||||
      windows: mdiWindows,
 | 
			
		||||
      linux: mdiLinux,
 | 
			
		||||
      chrome: mdiGoogleChrome,
 | 
			
		||||
      firefox: mdiFirefox,
 | 
			
		||||
      opera: mdiOpera,
 | 
			
		||||
      msie: mdiInternetExplorer,
 | 
			
		||||
      safari: mdiAppleSafari,
 | 
			
		||||
      person: mdiAccount,
 | 
			
		||||
      bar: mdiGlassCocktail,
 | 
			
		||||
      finanzer: mdiCurrencyEur,
 | 
			
		||||
      gastro: mdiFoodForkDrink,
 | 
			
		||||
      username: null,
 | 
			
		||||
      mail: null,
 | 
			
		||||
      firstname: null,
 | 
			
		||||
      lastname: null,
 | 
			
		||||
      password: null,
 | 
			
		||||
      controlPassword: null,
 | 
			
		||||
      trashCan: mdiTrashCan,
 | 
			
		||||
      isFulllineText: false,
 | 
			
		||||
      acceptedPassword: null,
 | 
			
		||||
      passError: null,
 | 
			
		||||
      equal_password: value =>
 | 
			
		||||
        this.password === value || 'Passwörter sind nicht identisch.',
 | 
			
		||||
      email: value => {
 | 
			
		||||
        if (value.length > 0) {
 | 
			
		||||
          const pattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
 | 
			
		||||
          return pattern.test(value) || 'keine gültige E-Mail'
 | 
			
		||||
        }
 | 
			
		||||
        return true
 | 
			
		||||
      },
 | 
			
		||||
      empty_password: data => {
 | 
			
		||||
        return !!data || 'Password wird bentögigt'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.$nextTick(function() {
 | 
			
		||||
      window.addEventListener('resize', this.getWindowWidth)
 | 
			
		||||
      this.getWindowWidth()
 | 
			
		||||
    })
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions({
 | 
			
		||||
      saveConfig: 'user/saveConfig',
 | 
			
		||||
      getStatus: 'user/getStatus',
 | 
			
		||||
      getTokens: 'user/getTokens',
 | 
			
		||||
      deleteToken: 'user/deleteToken'
 | 
			
		||||
    }),
 | 
			
		||||
    getWindowWidth() {
 | 
			
		||||
      this.isFulllineText = document.documentElement.clientWidth <= 600
 | 
			
		||||
    },
 | 
			
		||||
    save() {
 | 
			
		||||
      let user = {}
 | 
			
		||||
      if (this.firstname) user.firstname = this.firstname
 | 
			
		||||
      if (this.lastname) user.lastname = this.lastname
 | 
			
		||||
      if (this.username) user.username = this.username
 | 
			
		||||
      if (this.$refs.mail.validate()) {
 | 
			
		||||
        if (this.mail) user.mail = this.mail
 | 
			
		||||
      }
 | 
			
		||||
      if (this.$refs.newPassword.validate()) {
 | 
			
		||||
        if (this.password) user.password = this.password
 | 
			
		||||
      } else {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      console.log(this.$refs.acceptedPasswordTest.validate())
 | 
			
		||||
      if (this.$refs.acceptedPasswordTest.validate()) {
 | 
			
		||||
        this.saveConfig({
 | 
			
		||||
          oldUsername: user.username,
 | 
			
		||||
          ...user,
 | 
			
		||||
          acceptedPassword: this.acceptedPassword
 | 
			
		||||
        })
 | 
			
		||||
        this.$refs.acceptedPassword.reset()
 | 
			
		||||
      } else {
 | 
			
		||||
        this.passError = 'Du musst dein Password eingeben'
 | 
			
		||||
      }
 | 
			
		||||
      this.password = null
 | 
			
		||||
      this.controlPassword = null
 | 
			
		||||
    },
 | 
			
		||||
    calcLifefime(time) {
 | 
			
		||||
      if (time < 60) return String(time) + 'Sekunden'
 | 
			
		||||
      time = Math.round(time / 60)
 | 
			
		||||
      if (time < 60) return String(time) + 'Minuten'
 | 
			
		||||
      time = Math.round(time / 60)
 | 
			
		||||
      if (time < 24) return String(time) + 'Stunden'
 | 
			
		||||
      time = Math.round(time / 24)
 | 
			
		||||
      if (time < 7) return String(time) + 'Tage'
 | 
			
		||||
      time = Math.round(time / 7)
 | 
			
		||||
      if (time < 30) return String(time) + 'Wochen'
 | 
			
		||||
      time = Math.round(time / 30)
 | 
			
		||||
      if (time < 12) return String(time) + 'Monate'
 | 
			
		||||
      time = Math.round(time / 12)
 | 
			
		||||
      return String(time) + 'Jahre'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters({
 | 
			
		||||
      user: 'user/user',
 | 
			
		||||
      error: 'user/error',
 | 
			
		||||
      loading: 'user/loading',
 | 
			
		||||
      status: 'user/status',
 | 
			
		||||
      tokens: 'user/tokens'
 | 
			
		||||
    }),
 | 
			
		||||
    lock() {
 | 
			
		||||
      return this.user.locked ? 'gesperrt' : 'nicht gesperrt'
 | 
			
		||||
    },
 | 
			
		||||
    lockColor() {
 | 
			
		||||
      return this.user.locked ? 'red' : 'green'
 | 
			
		||||
    },
 | 
			
		||||
    autoLock() {
 | 
			
		||||
      return this.user.autoLock ? 'aktiviert' : 'deaktiviert'
 | 
			
		||||
    },
 | 
			
		||||
    autoLockColor() {
 | 
			
		||||
      return this.user.autoLock ? 'green' : 'red'
 | 
			
		||||
    },
 | 
			
		||||
    computeStatus() {
 | 
			
		||||
      try {
 | 
			
		||||
        return this.status.find(a => a.id == this.user.statusgroup).name
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        return null
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    this.getStatus()
 | 
			
		||||
    this.getTokens()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.fulllineText {
 | 
			
		||||
  flex-basis: unset;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,116 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <v-toolbar>
 | 
			
		||||
      <v-toolbar-title>Gesamtübersicht</v-toolbar-title>
 | 
			
		||||
      <v-spacer />
 | 
			
		||||
      <v-toolbar-items>
 | 
			
		||||
        <v-text-field
 | 
			
		||||
          v-model="filter"
 | 
			
		||||
          style="margin-top: 3px"
 | 
			
		||||
          outlined
 | 
			
		||||
          type="number"
 | 
			
		||||
          :rules="[isNumber]"
 | 
			
		||||
        >
 | 
			
		||||
          <template v-slot:append>
 | 
			
		||||
            <v-icon>{{magnify}}</v-icon>
 | 
			
		||||
          </template>
 | 
			
		||||
        </v-text-field>
 | 
			
		||||
      </v-toolbar-items>
 | 
			
		||||
    </v-toolbar>
 | 
			
		||||
    <CreditOverviewSkeleton v-if="loading" />
 | 
			
		||||
    <div v-for="year in years" :key="years.indexOf(year)">
 | 
			
		||||
      <v-card style="margin-top: 3px" v-if="isFiltered(year)">
 | 
			
		||||
        <v-card-title>{{ year }}</v-card-title>
 | 
			
		||||
        <Table v-bind:user="user" v-bind:year="year" />
 | 
			
		||||
        <v-container fluid>
 | 
			
		||||
          <v-col>
 | 
			
		||||
            <v-row>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-label>Vorjahr:</v-label>
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-chip
 | 
			
		||||
                  outlined
 | 
			
		||||
                  :text-color="getLastColor(user.creditList[year][1].last)"
 | 
			
		||||
                  >{{ (user.creditList[year][1].last / 100).toFixed(2) }}
 | 
			
		||||
                </v-chip>
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-label>Gesamt:</v-label>
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col>
 | 
			
		||||
                <v-chip
 | 
			
		||||
                  outlined
 | 
			
		||||
                  x-large
 | 
			
		||||
                  :text-color="
 | 
			
		||||
                    getLastColor(
 | 
			
		||||
                      getAllSum(
 | 
			
		||||
                        user.creditList[year][2].sum,
 | 
			
		||||
                        user.creditList[year][1].last
 | 
			
		||||
                      )
 | 
			
		||||
                    )
 | 
			
		||||
                  "
 | 
			
		||||
                >
 | 
			
		||||
                  {{
 | 
			
		||||
                    (
 | 
			
		||||
                      getAllSum(
 | 
			
		||||
                        user.creditList[year][2].sum,
 | 
			
		||||
                        user.creditList[year][1].last
 | 
			
		||||
                      ) / 100
 | 
			
		||||
                    ).toFixed(2)
 | 
			
		||||
                  }}
 | 
			
		||||
                </v-chip>
 | 
			
		||||
              </v-col>
 | 
			
		||||
            </v-row>
 | 
			
		||||
          </v-col>
 | 
			
		||||
        </v-container>
 | 
			
		||||
      </v-card>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapGetters } from 'vuex'
 | 
			
		||||
import Table from '../finanzer/Table'
 | 
			
		||||
import CreditOverviewSkeleton from './Skeleton/CreditOverviewSkeleton'
 | 
			
		||||
import { mdiMagnify } from '@mdi/js'
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'CreditOverview',
 | 
			
		||||
  components: { CreditOverviewSkeleton, Table },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      isNumber: value => Number.isInteger(parseInt(value === '' ? 0 : value)) || "Muss eine Zahl sein.",
 | 
			
		||||
      filter: '',
 | 
			
		||||
      magnify: mdiMagnify
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    getLastColor(value) {
 | 
			
		||||
      return value < 0 ? 'red' : 'green'
 | 
			
		||||
    },
 | 
			
		||||
    getAllSum(sum, lastYear) {
 | 
			
		||||
      return lastYear + sum
 | 
			
		||||
    },
 | 
			
		||||
    isFiltered(value) {
 | 
			
		||||
      return value.toString().includes(this.filter)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters({
 | 
			
		||||
      user: 'user/user',
 | 
			
		||||
      loading: 'user/loading'
 | 
			
		||||
    }),
 | 
			
		||||
    years() {
 | 
			
		||||
      let years = []
 | 
			
		||||
      if (this.user) {
 | 
			
		||||
        for (let year in this.user.creditList) {
 | 
			
		||||
          years.unshift(parseInt(year))
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return years
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,208 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <v-card tile :loading="jobInvitesLoading">
 | 
			
		||||
      <v-card-title>
 | 
			
		||||
        Eingehende Einladungen
 | 
			
		||||
      </v-card-title>
 | 
			
		||||
      <v-card-text>
 | 
			
		||||
        <v-expansion-panels>
 | 
			
		||||
          <v-expansion-panel
 | 
			
		||||
            v-for="jobInvite in jobInvitesToMe"
 | 
			
		||||
            :key="jobInvite.id"
 | 
			
		||||
            @click.once="seenJobIvnite(jobInvite)"
 | 
			
		||||
          >
 | 
			
		||||
            <v-expansion-panel-header>
 | 
			
		||||
              <div>
 | 
			
		||||
                {{ jobInvite.on_date.getDate() }}.{{
 | 
			
		||||
                jobInvite.on_date.getMonth() + 1
 | 
			
		||||
                }}.{{ jobInvite.on_date.getFullYear() }} von
 | 
			
		||||
                <v-badge dot :value="!jobInvite.watched" color="red">
 | 
			
		||||
                  {{ jobInvite.from_user.firstname }}
 | 
			
		||||
                  {{ jobInvite.from_user.lastname }}
 | 
			
		||||
                </v-badge>
 | 
			
		||||
              </div>
 | 
			
		||||
              <v-row class="text-right" style="margin-right: 5px">
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-progress-circular
 | 
			
		||||
                    indeterminate
 | 
			
		||||
                    v-if="jobInvitesLoading"
 | 
			
		||||
                  ></v-progress-circular>
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-icon color="green" v-show="userInWorker(jobInvite)">
 | 
			
		||||
                    {{ check }}
 | 
			
		||||
                  </v-icon>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
            </v-expansion-panel-header>
 | 
			
		||||
            <v-expansion-panel-content :eager="true">
 | 
			
		||||
              <v-row class="text-right">
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-btn icon @click="updatingJobInvite(jobInvite)">
 | 
			
		||||
                    <v-icon>
 | 
			
		||||
                      {{ jobInvite.watched ? seen : notSeen }}
 | 
			
		||||
                    </v-icon>
 | 
			
		||||
                  </v-btn>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
              <Day
 | 
			
		||||
                :day="jobInvite.day"
 | 
			
		||||
                :long="true"
 | 
			
		||||
                :loading="jobInvite.day.loading"
 | 
			
		||||
                @addingJob="addingJob(jobInvite, $event)"
 | 
			
		||||
                @deletingJob="deletingJob(jobInvite, $event)"
 | 
			
		||||
                @sendInvites="setJobInvites"
 | 
			
		||||
                @sendRequests="setJobRequests"
 | 
			
		||||
                @deleteJobInvite="deleteInvite"
 | 
			
		||||
                @deleteJobRequest="deleteRequest"
 | 
			
		||||
              />
 | 
			
		||||
            </v-expansion-panel-content>
 | 
			
		||||
          </v-expansion-panel>
 | 
			
		||||
        </v-expansion-panels>
 | 
			
		||||
      </v-card-text>
 | 
			
		||||
    </v-card>
 | 
			
		||||
    <v-card tile :loading="jobInvitesLoading">
 | 
			
		||||
      <v-card-title>
 | 
			
		||||
        Versendete Einladungen
 | 
			
		||||
      </v-card-title>
 | 
			
		||||
      <v-card-text>
 | 
			
		||||
        <v-expansion-panels>
 | 
			
		||||
          <v-expansion-panel
 | 
			
		||||
                  v-for="jobInvite in jobInvitesFromMe"
 | 
			
		||||
                  :key="jobInvite.id"
 | 
			
		||||
                  @click.once="seenJobIvnite(jobInvite)"
 | 
			
		||||
          >
 | 
			
		||||
            <v-expansion-panel-header>
 | 
			
		||||
 | 
			
		||||
              <div>
 | 
			
		||||
                {{ jobInvite.on_date.getDate() }}.{{
 | 
			
		||||
                jobInvite.on_date.getMonth() + 1
 | 
			
		||||
                }}.{{ jobInvite.on_date.getFullYear() }} an
 | 
			
		||||
                <v-badge :value="jobInvite.watched" icon="mdi-eye" color="grey" inline>
 | 
			
		||||
                  {{ jobInvite.to_user.firstname }}
 | 
			
		||||
                  {{ jobInvite.to_user.lastname }}
 | 
			
		||||
                </v-badge>
 | 
			
		||||
              </div>
 | 
			
		||||
              <v-row class="text-right" style="margin-right: 5px">
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-progress-circular
 | 
			
		||||
                          indeterminate
 | 
			
		||||
                          v-if="jobInvitesLoading"
 | 
			
		||||
                  ></v-progress-circular>
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-icon color="green" v-show="userInWorker(jobInvite)">
 | 
			
		||||
                    {{ check }}
 | 
			
		||||
                  </v-icon>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
            </v-expansion-panel-header>
 | 
			
		||||
            <v-expansion-panel-content :eager="true">
 | 
			
		||||
              <v-row class="text-right">
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-btn icon @click="deleteInvite(jobInvite)">
 | 
			
		||||
                    <v-icon>
 | 
			
		||||
                      {{trashCan}}
 | 
			
		||||
                    </v-icon>
 | 
			
		||||
                  </v-btn>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
              <Day
 | 
			
		||||
                      :day="jobInvite.day"
 | 
			
		||||
                      :long="true"
 | 
			
		||||
                      :loading="jobInvite.day.loading"
 | 
			
		||||
                      @addingJob="addingJob(jobInvite, $event)"
 | 
			
		||||
                      @deletingJob="deletingJob(jobInvite, $event)"
 | 
			
		||||
                      @sendInvites="setJobInvites"
 | 
			
		||||
                      @sendRequests="setJobRequests"
 | 
			
		||||
                      @deleteJobInvite="deleteInvite"
 | 
			
		||||
                      @deleteJobRequest="deleteRequest"
 | 
			
		||||
              />
 | 
			
		||||
            </v-expansion-panel-content>
 | 
			
		||||
          </v-expansion-panel>
 | 
			
		||||
        </v-expansion-panels>
 | 
			
		||||
      </v-card-text>
 | 
			
		||||
    </v-card>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapActions, mapGetters } from 'vuex'
 | 
			
		||||
import { mdiEyeOff, mdiEyeCheck, mdiCheck, mdiTrashCan } from '@mdi/js'
 | 
			
		||||
import Day from '@/components/user/Jobs/Day'
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'JobInvites',
 | 
			
		||||
  components: { Day },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      notSeen: mdiEyeOff,
 | 
			
		||||
      seen: mdiEyeCheck,
 | 
			
		||||
      check: mdiCheck,
 | 
			
		||||
      trashCan: mdiTrashCan,
 | 
			
		||||
      showNotSeen: false,
 | 
			
		||||
      showSeen: false,
 | 
			
		||||
      update: 0
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions({
 | 
			
		||||
      getJobInvites: 'jobInvites/getJobInvites',
 | 
			
		||||
      addJob: 'jobInvites/addJob',
 | 
			
		||||
      setJobInvites: 'jobInvites/setJobInvites',
 | 
			
		||||
      updateJobInviteToMe: 'jobInvites/updateJobInviteToMe',
 | 
			
		||||
      deleteJob: 'jobInvites/deleteJob',
 | 
			
		||||
      setJobRequests: 'jobRequests/setJobRequests',
 | 
			
		||||
      deleteInvite: 'jobInvites/deleteJobInviteFromMe',
 | 
			
		||||
      deleteRequest: 'jobRequests/deleteJobRequestFromMe'
 | 
			
		||||
    }),
 | 
			
		||||
    forceRender() {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        this.update += 0
 | 
			
		||||
      }, 500)
 | 
			
		||||
    },
 | 
			
		||||
    updatingJobInvite(jobInvite) {
 | 
			
		||||
      jobInvite.watched = !jobInvite.watched
 | 
			
		||||
      this.updateJobInviteToMe(jobInvite)
 | 
			
		||||
    },
 | 
			
		||||
    seenJobIvnite(jobInvite) {
 | 
			
		||||
      if (!jobInvite.watched) {
 | 
			
		||||
        jobInvite.watched = true
 | 
			
		||||
        this.updateJobInviteToMe(jobInvite)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    addingJob(jobInvite, event) {
 | 
			
		||||
      this.seenJobIvnite(jobInvite)
 | 
			
		||||
      this.addJob(event)
 | 
			
		||||
      this.forceRender()
 | 
			
		||||
    },
 | 
			
		||||
    deletingJob(jobInvite, event) {
 | 
			
		||||
      this.seenJobIvnite(jobInvite)
 | 
			
		||||
      this.deleteJob(event)
 | 
			
		||||
      this.forceRender()
 | 
			
		||||
    },
 | 
			
		||||
    userInWorker(jobinvite) {
 | 
			
		||||
      var jobkinddate = jobinvite.day.jobkinddate.find(item => {
 | 
			
		||||
        return item.worker.find(workeritem => {
 | 
			
		||||
          return workeritem.id === jobinvite.to_user.id
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
      return !!jobkinddate
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters({
 | 
			
		||||
      jobInvitesFromMe: 'jobInvites/jobInvitesFromMe',
 | 
			
		||||
      jobInvitesToMe: 'jobInvites/jobInvitesToMe',
 | 
			
		||||
      jobInvitesLoading: 'jobInvites/jobInvitesLoading',
 | 
			
		||||
      activeUser: 'user/user'
 | 
			
		||||
    })
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      this.getJobInvites(new Date())
 | 
			
		||||
    }, 200)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,224 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <v-card tile :loading="jobRequestsLoading">
 | 
			
		||||
      <v-card-title>
 | 
			
		||||
        Eingehende Anfragen
 | 
			
		||||
      </v-card-title>
 | 
			
		||||
      <v-card-text>
 | 
			
		||||
        <v-expansion-panels>
 | 
			
		||||
          <v-expansion-panel
 | 
			
		||||
            v-for="(jobrequest, index) in jobRequestsToMe"
 | 
			
		||||
            :key="index"
 | 
			
		||||
          >
 | 
			
		||||
            <v-expansion-panel-header @click.once="seenJobRequest(jobrequest)">
 | 
			
		||||
              <div>
 | 
			
		||||
                {{ jobrequest.on_date.getDate() }}.{{
 | 
			
		||||
                jobrequest.on_date.getMonth() + 1
 | 
			
		||||
                }}.{{ jobrequest.on_date.getFullYear() }} von
 | 
			
		||||
                <v-badge dot :value="!jobrequest.watched" color="red">
 | 
			
		||||
                  {{ jobrequest.from_user.firstname }}
 | 
			
		||||
                  {{ jobrequest.from_user.lastname }}
 | 
			
		||||
                </v-badge>
 | 
			
		||||
              </div>
 | 
			
		||||
              <v-row class="text-right" style="margin-right: 5px">
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-progress-circular
 | 
			
		||||
                    indeterminate
 | 
			
		||||
                    v-if="jobRequestsLoading"
 | 
			
		||||
                  ></v-progress-circular>
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-icon color="green" v-show="userInWorker(jobrequest)">
 | 
			
		||||
                    {{ check }}
 | 
			
		||||
                  </v-icon>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
            </v-expansion-panel-header>
 | 
			
		||||
            <v-expansion-panel-content>
 | 
			
		||||
              <v-row class="text-right">
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-btn icon @click="updatingSeenJobRequest(jobrequest)">
 | 
			
		||||
                    <v-icon>
 | 
			
		||||
                      {{ jobrequest.watched ? seen : notSeen }}
 | 
			
		||||
                    </v-icon>
 | 
			
		||||
                  </v-btn>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
              <Day
 | 
			
		||||
                :day="jobrequest.day"
 | 
			
		||||
                :long="true"
 | 
			
		||||
                @sendRequests="sendingJobRequests(jobrequest, $event)"
 | 
			
		||||
                @addingJob="addJob"
 | 
			
		||||
                @deletingJob="deleteJob"
 | 
			
		||||
                @sendInvites="setJobInvites"
 | 
			
		||||
                @deleteJobInvite="deleteInvite"
 | 
			
		||||
                @deleteJobRequest="deleteJobRequestFromMe"
 | 
			
		||||
              />
 | 
			
		||||
              <v-row class="text-right">
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-btn
 | 
			
		||||
                    v-show="!jobrequest.answered"
 | 
			
		||||
                    text
 | 
			
		||||
                    @click="updatingAcceptedJobRequest(jobrequest)"
 | 
			
		||||
                    >Annehmen</v-btn
 | 
			
		||||
                  >
 | 
			
		||||
                  <div v-show="jobrequest.answered && !jobrequest.accepted">
 | 
			
		||||
                    Dieser Dienst wurde schon übertragen.
 | 
			
		||||
                  </div>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
            </v-expansion-panel-content>
 | 
			
		||||
          </v-expansion-panel>
 | 
			
		||||
        </v-expansion-panels>
 | 
			
		||||
      </v-card-text>
 | 
			
		||||
    </v-card>
 | 
			
		||||
    <v-card tile :loading="jobRequestsLoading">
 | 
			
		||||
      <v-card-title>
 | 
			
		||||
        Ausgehende Anfragen
 | 
			
		||||
      </v-card-title>
 | 
			
		||||
      <v-card-text>
 | 
			
		||||
        <v-expansion-panels>
 | 
			
		||||
          <v-expansion-panel
 | 
			
		||||
            v-for="(jobrequest, index) in jobRequestsFromMe"
 | 
			
		||||
            :key="index"
 | 
			
		||||
          >
 | 
			
		||||
            <v-expansion-panel-header>
 | 
			
		||||
              <div>
 | 
			
		||||
                {{ jobrequest.on_date.getDate() }}.{{
 | 
			
		||||
                jobrequest.on_date.getMonth() + 1
 | 
			
		||||
                }}.{{ jobrequest.on_date.getFullYear() }} an
 | 
			
		||||
                <v-badge :value="jobrequest.watched" icon="mdi-eye" color="grey" inline>
 | 
			
		||||
                  {{ jobrequest.to_user.firstname }}
 | 
			
		||||
                  {{ jobrequest.to_user.lastname }}
 | 
			
		||||
                </v-badge>
 | 
			
		||||
              </div>
 | 
			
		||||
              <v-row class="text-right" style="margin-right: 5px">
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-progress-circular
 | 
			
		||||
                    indeterminate
 | 
			
		||||
                    v-if="jobRequestsLoading"
 | 
			
		||||
                  ></v-progress-circular>
 | 
			
		||||
                </v-col>
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-icon color="green" v-show="jobrequest.accepted">
 | 
			
		||||
                    {{ check }}
 | 
			
		||||
                  </v-icon>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
            </v-expansion-panel-header>
 | 
			
		||||
            <v-expansion-panel-content>
 | 
			
		||||
              <v-row class="text-right">
 | 
			
		||||
                <v-col>
 | 
			
		||||
                  <v-btn icon @click="deleteJobRequestFromMe(jobrequest)">
 | 
			
		||||
                    <v-icon>
 | 
			
		||||
                      {{trashCan}}
 | 
			
		||||
                    </v-icon>
 | 
			
		||||
                  </v-btn>
 | 
			
		||||
                </v-col>
 | 
			
		||||
              </v-row>
 | 
			
		||||
              <Day
 | 
			
		||||
                :day="jobrequest.day"
 | 
			
		||||
                :long="true"
 | 
			
		||||
                @sendRequests="sendingJobRequests(jobrequest, $event)"
 | 
			
		||||
                @addingJob="addJob"
 | 
			
		||||
                @deletingJob="deleteJob"
 | 
			
		||||
                @sendInvites="setJobInvites"
 | 
			
		||||
                @deleteJobInvite="deleteInvite"
 | 
			
		||||
                @deleteJobRequest="deleteJobRequestFromMe"
 | 
			
		||||
              />
 | 
			
		||||
            </v-expansion-panel-content>
 | 
			
		||||
          </v-expansion-panel>
 | 
			
		||||
        </v-expansion-panels>
 | 
			
		||||
      </v-card-text>
 | 
			
		||||
    </v-card>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapGetters, mapActions } from 'vuex'
 | 
			
		||||
import { mdiEyeOff, mdiEyeCheck, mdiCheck, mdiTrashCan } from '@mdi/js'
 | 
			
		||||
import Day from '@/components/user/Jobs/Day'
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'JobTransfer',
 | 
			
		||||
  components: { Day },
 | 
			
		||||
  props: {},
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      notSeen: mdiEyeOff,
 | 
			
		||||
      seen: mdiEyeCheck,
 | 
			
		||||
      check: mdiCheck,
 | 
			
		||||
      trashCan: mdiTrashCan,
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    ...mapActions({
 | 
			
		||||
      getJobRequests: 'jobRequests/getJobRequests',
 | 
			
		||||
      updateJobRequestToMe: 'jobRequests/updateJobRequestToMe',
 | 
			
		||||
      setJobRequests: 'jobRequests/setJobRequests',
 | 
			
		||||
      deleteJobRequestFromMe: 'jobRequests/deleteJobRequestFromMe',
 | 
			
		||||
      deleteInvite: 'jobInvites/deleteJobInviteFromMe',
 | 
			
		||||
      getJobInvites: 'jobInvites/getJobInvites',
 | 
			
		||||
      addJob: 'jobInvites/addJob',
 | 
			
		||||
      setJobInvites: 'jobInvites/setJobInvites',
 | 
			
		||||
      updateJobInviteToMe: 'jobInvites/updateJobInviteToMe',
 | 
			
		||||
      deleteJob: 'jobInvites/deleteJob',
 | 
			
		||||
    }),
 | 
			
		||||
    updatingAcceptedJobRequest(jobRequest) {
 | 
			
		||||
      jobRequest.accepted = true
 | 
			
		||||
      jobRequest.answered = true
 | 
			
		||||
      this.updateJobRequestToMe({ ...jobRequest })
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        this.getJobRequests(), 200
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    updatingSeenJobRequest(jobRequest) {
 | 
			
		||||
      jobRequest.watched = !jobRequest.watched
 | 
			
		||||
      this.updateJobRequestToMe({ ...jobRequest })
 | 
			
		||||
    },
 | 
			
		||||
    seenJobRequest(jobRequest) {
 | 
			
		||||
      if (!jobRequest.watched) {
 | 
			
		||||
        jobRequest.watched = true
 | 
			
		||||
        this.updateJobRequestToMe(jobRequest)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    userInWorker(jobrequest) {
 | 
			
		||||
      var jobkinddate = jobrequest.day.jobkinddate.find(item => {
 | 
			
		||||
        return item.worker.find(workeritem => {
 | 
			
		||||
          return workeritem.id === this.activeUser.id
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
      return !!jobkinddate
 | 
			
		||||
    },
 | 
			
		||||
    sendingJobRequests(jobrequest, event) {
 | 
			
		||||
      this.seenJobRequest(jobrequest)
 | 
			
		||||
      this.setJobRequests(event)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters({
 | 
			
		||||
      jobRequestsToMe: 'jobRequests/jobRequestsToMe',
 | 
			
		||||
      jobRequestsFromMe: 'jobRequests/jobRequestsFromMe',
 | 
			
		||||
      jobRequestsLoading: 'jobRequests/jobRequestsLoading',
 | 
			
		||||
      loading: 'user/loading',
 | 
			
		||||
      activeUser: 'user/user'
 | 
			
		||||
    })
 | 
			
		||||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    if (!this.loading) {
 | 
			
		||||
      this.getJobRequests()
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    loading(newValue) {
 | 
			
		||||
      if (!newValue) {
 | 
			
		||||
        this.getJobRequests()
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    jobRequestsLoading(newValue, oldValue) {
 | 
			
		||||
      console.log(newValue, oldValue)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||