release v2.0.0 #4
|
@ -40,7 +40,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, PropType } from 'vue';
|
import { computed, defineComponent, PropType } from 'vue';
|
||||||
import { date as q_date } from 'quasar';
|
import { date as q_date } from 'quasar';
|
||||||
import { stringIsDate, stringIsTime, stringIsDateTime, Validator } from 'src/utils/validators';
|
import { stringIsDate, stringIsTime, stringIsDateTime, Validator } from '..';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'IsoDateInput',
|
name: 'IsoDateInput',
|
|
@ -0,0 +1,4 @@
|
||||||
|
import IsoDateInput from './IsoDateInput.vue';
|
||||||
|
import PasswordInput from './PasswordInput.vue';
|
||||||
|
|
||||||
|
export { IsoDateInput, PasswordInput };
|
|
@ -0,0 +1,7 @@
|
||||||
|
export { api, pinia } from './src/internal';
|
||||||
|
|
||||||
|
export * from './src/stores/';
|
||||||
|
|
||||||
|
export * from './src/utils/datetime';
|
||||||
|
export * from './src/utils/permission';
|
||||||
|
export * from './src/utils/validators';
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"version": "1.0.0-alpha.1",
|
||||||
|
"name": "@flaschengeist/api",
|
||||||
|
"author": "Tim Gröger <flaschengeist@wu5.de>",
|
||||||
|
"homepage": "https://flaschengeist.dev/Flaschengeist",
|
||||||
|
"description": "Modular student club administration system",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist-frontend/issues"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"format": "prettier --config ./package.json --write '{,!(node_modules)/**/}*.ts'",
|
||||||
|
"lint": "eslint --ext .js,.ts,.vue ./src"
|
||||||
|
},
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@quasar/app": "^3.0.0-beta.26",
|
||||||
|
"flaschengeist": "^2.0.0-alpha.1",
|
||||||
|
"pinia": "^2.0.0-alpha.19"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@flaschengeist/types": "git+https://flaschengeist.dev/ferfissimo/flaschengeist-types.git#develop",
|
||||||
|
"@types/node": "^12.20.13",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.24.0",
|
||||||
|
"@typescript-eslint/parser": "^4.24.0",
|
||||||
|
"eslint": "^7.26.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-vue": "^7.9.0",
|
||||||
|
"eslint-webpack-plugin": "^2.5.4",
|
||||||
|
"prettier": "^2.3.0",
|
||||||
|
"typescript": "^4.2.4"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"arrowParens": "always"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
//https://github.com/vuejs/vue-next/issues/3130
|
||||||
|
declare module '*.vue' {
|
||||||
|
import { ComponentOptions } from 'vue';
|
||||||
|
const component: ComponentOptions;
|
||||||
|
export default component;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
|
||||||
|
export const api = axios.create();
|
||||||
|
|
||||||
|
export const pinia = createPinia();
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './main';
|
||||||
|
export * from './session';
|
||||||
|
export * from './user';
|
|
@ -1,9 +1,8 @@
|
||||||
import { useUserStore, useSessionStore } from 'src/plugins/user/store';
|
|
||||||
import { translateNotification } from 'src/boot/plugins';
|
|
||||||
import { LocalStorage, SessionStorage } from 'quasar';
|
import { LocalStorage, SessionStorage } from 'quasar';
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
|
import { useSessionStore, useUserStore } from '.';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { api } from 'src/boot/axios';
|
import { api } from '../internal';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
function loadCurrentSession() {
|
function loadCurrentSession() {
|
||||||
|
@ -119,10 +118,11 @@ export const useMainStore = defineStore({
|
||||||
data.forEach((n) => {
|
data.forEach((n) => {
|
||||||
n.time = new Date(n.time);
|
n.time = new Date(n.time);
|
||||||
notifications.push(
|
notifications.push(
|
||||||
(
|
(flaschengeist?.plugins.filter((p) => p.name === n.plugin)[0]?.notification)(
|
||||||
flaschengeist?.plugins.filter((p) => p.name === n.plugin)[0]?.notification ||
|
/*||
|
||||||
translateNotification
|
translateNotification*/
|
||||||
)(n)
|
n
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.notifications.push(...notifications);
|
this.notifications.push(...notifications);
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { api } from '../internal';
|
||||||
|
import { useMainStore } from '.';
|
||||||
|
|
||||||
|
export const useSessionStore = defineStore({
|
||||||
|
id: 'sessions',
|
||||||
|
|
||||||
|
state: () => ({}),
|
||||||
|
|
||||||
|
getters: {},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async getSession(token: string) {
|
||||||
|
return await api
|
||||||
|
.get(`/auth/${token}`)
|
||||||
|
.then(({ data }: AxiosResponse<FG.Session>) => data)
|
||||||
|
.catch(() => null);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getSessions() {
|
||||||
|
try {
|
||||||
|
const { data } = await api.get<FG.Session[]>('/auth');
|
||||||
|
data.forEach((session) => {
|
||||||
|
session.expires = new Date(session.expires);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mainStore = useMainStore();
|
||||||
|
const currentSession = data.find((session) => {
|
||||||
|
return session.token === mainStore.session?.token;
|
||||||
|
});
|
||||||
|
if (currentSession) {
|
||||||
|
mainStore.session = currentSession;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
return [] as FG.Session[];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteSession(token: string) {
|
||||||
|
const mainStore = useMainStore();
|
||||||
|
if (token === mainStore.session?.token) return mainStore.logout();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.delete(`/auth/${token}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (!error || !('response' in error) || (<AxiosError>error).response?.status != 401)
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateSession(lifetime: number, token: string) {
|
||||||
|
try {
|
||||||
|
const { data } = await api.put<FG.Session>(`auth/${token}`, { value: lifetime });
|
||||||
|
data.expires = new Date(data.expires);
|
||||||
|
|
||||||
|
const mainStore = useMainStore();
|
||||||
|
if (mainStore.session?.token == data.token) mainStore.session = data;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,7 +1,7 @@
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { api } from 'src/boot/axios';
|
import { AxiosError } from 'axios';
|
||||||
import { AxiosError, AxiosResponse } from 'axios';
|
import { api } from '../internal';
|
||||||
import { useMainStore } from 'src/stores';
|
import { useMainStore } from '.';
|
||||||
|
|
||||||
export const useUserStore = defineStore({
|
export const useUserStore = defineStore({
|
||||||
id: 'users',
|
id: 'users',
|
||||||
|
@ -110,68 +110,3 @@ export const useUserStore = defineStore({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useSessionStore = defineStore({
|
|
||||||
id: 'sessions',
|
|
||||||
|
|
||||||
state: () => ({}),
|
|
||||||
|
|
||||||
getters: {},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
async getSession(token: string) {
|
|
||||||
return await api
|
|
||||||
.get(`/auth/${token}`)
|
|
||||||
.then(({ data }: AxiosResponse<FG.Session>) => data)
|
|
||||||
.catch(() => null);
|
|
||||||
},
|
|
||||||
|
|
||||||
async getSessions() {
|
|
||||||
try {
|
|
||||||
const { data } = await api.get<FG.Session[]>('/auth');
|
|
||||||
data.forEach((session) => {
|
|
||||||
session.expires = new Date(session.expires);
|
|
||||||
});
|
|
||||||
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
const currentSession = data.find((session) => {
|
|
||||||
return session.token === mainStore.session?.token;
|
|
||||||
});
|
|
||||||
if (currentSession) {
|
|
||||||
mainStore.session = currentSession;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
return [] as FG.Session[];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteSession(token: string) {
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
if (token === mainStore.session?.token) return mainStore.logout();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.delete(`/auth/${token}`);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
if (!error || !('response' in error) || (<AxiosError>error).response?.status != 401)
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateSession(lifetime: number, token: string) {
|
|
||||||
try {
|
|
||||||
const { data } = await api.put<FG.Session>(`auth/${token}`, { value: lifetime });
|
|
||||||
data.expires = new Date(data.expires);
|
|
||||||
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
if (mainStore.session?.token == data.token) mainStore.session = data;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMainStore } from 'src/stores';
|
import { useMainStore } from '../stores';
|
||||||
|
|
||||||
export function hasPermission(permission: string) {
|
export function hasPermission(permission: string) {
|
||||||
const store = useMainStore();
|
const store = useMainStore();
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"extends": "@quasar/app/tsconfig-preset",
|
||||||
|
"target": "esnext",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"lib": [
|
||||||
|
"es2020",
|
||||||
|
"dom"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"@flaschengeist/types",
|
||||||
|
"@quasar/app",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
11
package.json
11
package.json
|
@ -15,14 +15,16 @@
|
||||||
"lint": "eslint --ext .js,.ts,.vue ./src"
|
"lint": "eslint --ext .js,.ts,.vue ./src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@flaschengeist/api": "file:./api",
|
||||||
|
"@flaschengeist/users": "git+https://flaschengeist.dev/ferfissimo/flaschengeist-users.git#develop",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"cordova": "^10.0.0",
|
"cordova": "^10.0.0",
|
||||||
"pinia": "^2.0.0-alpha.18",
|
"pinia": "^2.0.0-alpha.19",
|
||||||
"quasar": "^2.0.0-beta.17"
|
"quasar": "^2.0.0-beta.18"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@flaschengeist/typings": "file:../flaschengeist-typings",
|
"@flaschengeist/types": "git+https://flaschengeist.dev/ferfissimo/flaschengeist-types.git#develop",
|
||||||
"@quasar/app": "^3.0.0-beta.25",
|
"@quasar/app": "^3.0.0-beta.26",
|
||||||
"@quasar/extras": "^1.10.4",
|
"@quasar/extras": "^1.10.4",
|
||||||
"@quasar/quasar-app-extension-qcalendar": "4.0.0-alpha.8",
|
"@quasar/quasar-app-extension-qcalendar": "4.0.0-alpha.8",
|
||||||
"@types/node": "^12.20.13",
|
"@types/node": "^12.20.13",
|
||||||
|
@ -34,6 +36,7 @@
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-vue": "^7.9.0",
|
"eslint-plugin-vue": "^7.9.0",
|
||||||
"eslint-webpack-plugin": "^2.5.4",
|
"eslint-webpack-plugin": "^2.5.4",
|
||||||
|
"modify-source-webpack-plugin": "^3.0.0-rc.0",
|
||||||
"prettier": "^2.3.0",
|
"prettier": "^2.3.0",
|
||||||
"typescript": "^4.2.4",
|
"typescript": "^4.2.4",
|
||||||
"vuedraggable": "^4.0.1"
|
"vuedraggable": "^4.0.1"
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
// You can add your plugins here
|
||||||
|
module.exports = [
|
||||||
|
/* '@flaschengeist/balances', */
|
||||||
|
'@flaschengeist/users',
|
||||||
|
/* '@flaschengeist/schedule', */
|
||||||
|
]
|
|
@ -9,7 +9,9 @@
|
||||||
/* eslint-env node */
|
/* eslint-env node */
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
const ESLintPlugin = require('eslint-webpack-plugin')
|
const ESLintPlugin = require('eslint-webpack-plugin')
|
||||||
const { configure } = require('quasar/wrappers');
|
const { ModifySourcePlugin } = require('modify-source-webpack-plugin')
|
||||||
|
const { configure } = require('quasar/wrappers')
|
||||||
|
|
||||||
|
|
||||||
module.exports = configure(function (/* ctx */) {
|
module.exports = configure(function (/* ctx */) {
|
||||||
return {
|
return {
|
||||||
|
@ -53,17 +55,16 @@ module.exports = configure(function (/* ctx */) {
|
||||||
build: {
|
build: {
|
||||||
vueRouterMode: 'history', // available values: 'hash', 'history'
|
vueRouterMode: 'history', // available values: 'hash', 'history'
|
||||||
|
|
||||||
// transpile: false,
|
// transpile: false,// eslint-disable-next-line
|
||||||
|
|
||||||
|
|
||||||
// Add dependencies for transpiling with Babel (Array of string/regex)
|
// Add dependencies for transpiling with Babel (Array of string/regex)
|
||||||
// (from node_modules, which are by default not transpiled).
|
// (from node_modules, which are by default not transpiled).
|
||||||
// Applies only if "transpile" is set to true.
|
// Applies only if "transpile" is set to true.
|
||||||
// transpileDependencies: [],
|
// transpileDependencies: [],
|
||||||
|
|
||||||
// rtl: false, // https://quasar.dev/options/rtl-support
|
// rtl: false, // https://quasa// eslint-disable-next-line
|
||||||
// preloadChunks: true,
|
|
||||||
// showProgress: false,
|
|
||||||
// gzip: true,
|
|
||||||
// analyze: true,
|
// analyze: true,
|
||||||
|
|
||||||
// Options below are automatically set depending on the env, set them if you want to override
|
// Options below are automatically set depending on the env, set them if you want to override
|
||||||
|
@ -77,7 +78,23 @@ module.exports = configure(function (/* ctx */) {
|
||||||
extensions: [ 'ts', 'js', 'vue' ],
|
extensions: [ 'ts', 'js', 'vue' ],
|
||||||
exclude: 'node_modules'
|
exclude: 'node_modules'
|
||||||
}])
|
}])
|
||||||
},
|
chain.plugin('modify-source-webpack-plugin')
|
||||||
|
.use(ModifySourcePlugin, [{
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /plugins\.ts$/,
|
||||||
|
modify: (src, filename) => {
|
||||||
|
const custom_plgns = require('./plugin.config.js')
|
||||||
|
const required_plgns = require('./src/vendor-plugin.config.js')
|
||||||
|
return src.replace(/\/\* *INSERT_PLUGIN_LIST *\*\//,
|
||||||
|
[...custom_plgns, ...required_plgns].map(v => `import("${v}").then(v => success(v)).catch(() => failure("${v}"))`)
|
||||||
|
.join(','))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}])
|
||||||
|
//chain.resolve.alias.set('flaschengeist', '.')
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import config from 'src/config';
|
import { useMainStore, api } from '@flaschengeist/api';
|
||||||
import { boot } from 'quasar/wrappers';
|
|
||||||
import { LocalStorage, Notify } from 'quasar';
|
import { LocalStorage, Notify } from 'quasar';
|
||||||
import axios, { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { useMainStore } from 'src/stores';
|
import { boot } from 'quasar/wrappers';
|
||||||
|
import config from 'src/config';
|
||||||
const api = axios.create();
|
|
||||||
|
|
||||||
export default boot(({ router }) => {
|
export default boot(({ router }) => {
|
||||||
api.defaults.baseURL = LocalStorage.getItem<string>('baseURL') || config.baseURL;
|
api.defaults.baseURL = LocalStorage.getItem<string>('baseURL') || config.baseURL;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
|
import { useMainStore, hasPermissions } from '@flaschengeist/api';
|
||||||
import { boot } from 'quasar/wrappers';
|
import { boot } from 'quasar/wrappers';
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
import { hasPermissions } from 'src/utils/permission';
|
|
||||||
import { RouteRecord } from 'vue-router';
|
import { RouteRecord } from 'vue-router';
|
||||||
|
|
||||||
export default boot(({ router }) => {
|
export default boot(({ router }) => {
|
||||||
|
|
|
@ -4,43 +4,44 @@ import { boot } from 'quasar/wrappers';
|
||||||
import routes from 'src/router/routes';
|
import routes from 'src/router/routes';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { RouteRecordRaw } from 'vue-router';
|
import { RouteRecordRaw } from 'vue-router';
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
|
|
||||||
const config: { [key: string]: Array<string> } = {
|
|
||||||
// Do not change required Modules !!
|
|
||||||
requiredModules: ['User'],
|
|
||||||
// here you can import plugins.
|
|
||||||
loadModules: ['Balance', 'Schedule', 'Pricelist'],
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Stop!
|
|
||||||
|
|
||||||
// do not change anything here !!
|
|
||||||
|
|
||||||
// You can not even read? I said stop!
|
|
||||||
|
|
||||||
// Really are you stupid? Stop scrolling down here!
|
|
||||||
|
|
||||||
// Every line you scroll down, an unicorn will die painfully!
|
|
||||||
|
|
||||||
// Ok you must hate unicorns... But what if I say you I joked... Baby otters will die!
|
|
||||||
|
|
||||||
.-"""-.
|
|
||||||
/ o\
|
|
||||||
| o 0).-.
|
|
||||||
| .-;(_/ .-.
|
|
||||||
\ / /)).---._| `\ ,
|
|
||||||
'. ' /(( `'-./ _/|
|
|
||||||
\ .' ) .-.;` /
|
|
||||||
'. | `\-'
|
|
||||||
'._ -' /
|
|
||||||
``""--`------`
|
|
||||||
*/
|
|
||||||
|
|
||||||
/****************************************************
|
/****************************************************
|
||||||
******** Internal area for some magic **************
|
******** Internal area for some magic **************
|
||||||
****************************************************/
|
****************************************************/
|
||||||
|
|
||||||
|
declare type ImportPlgn = { default: FG_Plugin.Plugin };
|
||||||
|
|
||||||
|
function validatePlugin(plugin: FG_Plugin.Plugin) {
|
||||||
|
return (
|
||||||
|
typeof plugin.name === 'string' &&
|
||||||
|
typeof plugin.id === 'string' &&
|
||||||
|
plugin.id.length > 0 &&
|
||||||
|
typeof plugin.version === 'string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
// This functions are used by webpack magic
|
||||||
|
// Called when import promise resolved
|
||||||
|
function success(value: ImportPlgn, path: string) {
|
||||||
|
if (validatePlugin(value.default)) PLUGINS.plugins.set(value.default.id, value.default);
|
||||||
|
else failure(path);
|
||||||
|
}
|
||||||
|
// Called when import promise rejected
|
||||||
|
function failure(path = 'unknown') {
|
||||||
|
console.error(`Plugin ${path} could not be found and not imported`);
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
// Here does some magic happens, WebPack will automatically replace the following comment with the import statements
|
||||||
|
const PLUGINS = {
|
||||||
|
context: <Array<() => Promise<ImportPlgn>>>[
|
||||||
|
/*INSERT_PLUGIN_LIST*/
|
||||||
|
],
|
||||||
|
plugins: new Map<string, FG_Plugin.Plugin>(),
|
||||||
|
};
|
||||||
|
|
||||||
interface BackendPlugin {
|
interface BackendPlugin {
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
version: string;
|
version: string;
|
||||||
|
@ -221,81 +222,61 @@ function combineShortcuts(target: FG_Plugin.Shortcut[], source: FG_Plugin.MenuRo
|
||||||
/**
|
/**
|
||||||
* Load a Flaschengeist plugin
|
* Load a Flaschengeist plugin
|
||||||
* @param loadedPlugins Flaschgeist object
|
* @param loadedPlugins Flaschgeist object
|
||||||
* @param pluginName Plugin to load
|
* @param plugin Plugin to load
|
||||||
* @param context RequireContext of plugins
|
|
||||||
* @param router VueRouter instance
|
* @param router VueRouter instance
|
||||||
*/
|
*/
|
||||||
function loadPlugin(
|
function loadPlugin(
|
||||||
loadedPlugins: FG_Plugin.Flaschengeist,
|
loadedPlugins: FG_Plugin.Flaschengeist,
|
||||||
pluginName: string,
|
plugin: FG_Plugin.Plugin,
|
||||||
context: __WebpackModuleApi.RequireContext,
|
|
||||||
backend: Backend
|
backend: Backend
|
||||||
) {
|
) {
|
||||||
// Check if already loaded
|
// Check if already loaded
|
||||||
if (loadedPlugins.plugins.findIndex((p) => p.name === pluginName) !== -1) return true;
|
if (loadedPlugins.plugins.findIndex((p) => p.name === plugin.name) !== -1) return true;
|
||||||
|
|
||||||
// Search if plugin is installed
|
// Check backend dependencies
|
||||||
const available = context.keys();
|
if (
|
||||||
const plugin = available.includes(`./${pluginName.toLowerCase()}/plugin.ts`)
|
!plugin.requiredModules.every(
|
||||||
? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
(required) =>
|
||||||
<FG_Plugin.Plugin>context(`./${pluginName.toLowerCase()}/plugin.ts`).default
|
backend.plugins[required[0]] !== undefined &&
|
||||||
: undefined;
|
(required.length == 1 ||
|
||||||
|
true) /* validate the version, semver440 from python is... tricky on node*/
|
||||||
if (!plugin) {
|
)
|
||||||
// Plugin is not found, results in an error
|
) {
|
||||||
console.exception(`Could not find required Plugin ${pluginName}`);
|
console.error(`Plugin ${plugin.name}: Backend modules not satisfied`);
|
||||||
return false;
|
return false;
|
||||||
} else {
|
|
||||||
// Plugin found. Check backend dependencies
|
|
||||||
if (
|
|
||||||
!plugin.requiredBackendModules.every((required) => backend.plugins[required] !== undefined)
|
|
||||||
) {
|
|
||||||
console.error(`Plugin ${pluginName}: Backend modules not satisfied`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check frontend dependencies
|
|
||||||
if (
|
|
||||||
!plugin.requiredModules.every((required) =>
|
|
||||||
loadPlugin(loadedPlugins, required, context, backend)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
console.error(`Plugin ${pluginName}: 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.name + '_' + widget.name));
|
|
||||||
Array.prototype.push.apply(loadedPlugins.widgets, plugin.widgets);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadedPlugins.plugins.push({
|
|
||||||
name: plugin.name,
|
|
||||||
version: plugin.version,
|
|
||||||
notification: plugin.notification?.bind({}) || translateNotification,
|
|
||||||
});
|
|
||||||
|
|
||||||
return plugin;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.name + '_' + widget.name));
|
||||||
|
Array.prototype.push.apply(loadedPlugins.widgets, plugin.widgets);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedPlugins.plugins.push({
|
||||||
|
name: plugin.name,
|
||||||
|
version: plugin.version,
|
||||||
|
notification: plugin.notification?.bind({}) || translateNotification,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -317,8 +298,9 @@ async function getBackend() {
|
||||||
*/
|
*/
|
||||||
export default boot(async ({ router, app }) => {
|
export default boot(async ({ router, app }) => {
|
||||||
const backend = await getBackend();
|
const backend = await getBackend();
|
||||||
if (backend === null) {
|
if (!backend || typeof backend !== 'object' || !('plugins' in backend)) {
|
||||||
void router.push({ name: 'error' });
|
console.log('Backend error');
|
||||||
|
router.isReady().finally(() => void router.push({ name: 'offline', params: { refresh: 1 } }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,39 +313,23 @@ export default boot(async ({ router, app }) => {
|
||||||
widgets: [],
|
widgets: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// get all plugins
|
const BreakError = {};
|
||||||
const pluginsContext = require.context('src/plugins', true, /.+\/plugin.ts$/);
|
try {
|
||||||
|
PLUGINS.plugins.forEach((plugin, name) => {
|
||||||
|
if (!loadPlugin(loadedPlugins, plugin, backend)) {
|
||||||
|
void router.push({ name: 'error' });
|
||||||
|
|
||||||
// Start loading plugins
|
Notify.create({
|
||||||
// Load required modules, if not found or error when loading this will forward the user to the error page
|
type: 'negative',
|
||||||
config.requiredModules.forEach((required) => {
|
message: `Fehler beim Laden: Bitte wende dich an den Admin (error: PNF-${name}!`,
|
||||||
const plugin = loadPlugin(loadedPlugins, required, pluginsContext, backend);
|
timeout: 10000,
|
||||||
if (!plugin) {
|
progress: true,
|
||||||
void router.push({ name: 'error' });
|
});
|
||||||
return;
|
throw BreakError;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Load user defined plugins
|
|
||||||
// If there is an error with loading a plugin, the user will get informed.
|
|
||||||
const failed: string[] = [];
|
|
||||||
config.loadModules.forEach((required) => {
|
|
||||||
const plugin = loadPlugin(loadedPlugins, required, pluginsContext, backend);
|
|
||||||
if (!plugin) {
|
|
||||||
failed.push(required);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (failed.length > 0) {
|
|
||||||
// Log failed plugins
|
|
||||||
console.error('Could not load all plugins', failed);
|
|
||||||
// Inform user about error
|
|
||||||
Notify.create({
|
|
||||||
type: 'negative',
|
|
||||||
message:
|
|
||||||
'Fehler beim Laden: Nicht alle Funktionen stehen zur Verfügung. Bitte wende dich an den Admin!',
|
|
||||||
timeout: 10000,
|
|
||||||
progress: true,
|
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (e !== BreakError) throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort widgets by priority
|
// Sort widgets by priority
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
import { createPinia, Pinia } from 'pinia';
|
import { useMainStore, pinia } from '@flaschengeist/api';
|
||||||
import { boot } from 'quasar/wrappers';
|
import { boot } from 'quasar/wrappers';
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
export const pinia = ref<Pinia>();
|
|
||||||
|
|
||||||
export default boot(({ app }) => {
|
export default boot(({ app }) => {
|
||||||
pinia.value = createPinia();
|
app.use(pinia);
|
||||||
app.use(pinia.value);
|
|
||||||
|
|
||||||
const store = useMainStore();
|
const store = useMainStore();
|
||||||
void store.init();
|
void store.init();
|
||||||
|
|
|
@ -34,8 +34,8 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType, computed } from 'vue';
|
import { defineComponent, PropType, computed } from 'vue';
|
||||||
import { formatDateTime } from 'src/utils/datetime';
|
import { formatDateTime } from '@flaschengeist/api';
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
|
@ -30,8 +30,8 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, PropType } from 'vue';
|
import { computed, defineComponent, PropType } from 'vue';
|
||||||
import { hasPermissions } from 'src/utils/permission';
|
import { hasPermissions } from '@flaschengeist/api';
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'EssentialExpansionLink',
|
name: 'EssentialExpansionLink',
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, PropType } from 'vue';
|
import { computed, defineComponent, PropType } from 'vue';
|
||||||
import { hasPermissions } from 'src/utils/permission';
|
import { hasPermissions } from '@flaschengeist/api';
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'EssentialLink',
|
name: 'EssentialLink',
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, PropType } from 'vue';
|
import { computed, defineComponent, PropType } from 'vue';
|
||||||
import { hasPermissions } from 'src/utils/permission';
|
import { hasPermissions } from '@flaschengeist/api';
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'ShortcutLink',
|
name: 'ShortcutLink',
|
||||||
|
|
|
@ -89,11 +89,11 @@ import {
|
||||||
import { Screen } from 'quasar';
|
import { Screen } from 'quasar';
|
||||||
import config from 'src/config';
|
import config from 'src/config';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useMainStore } from 'src/stores';
|
import { useMainStore } from '@flaschengeist/api';
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
import EssentialExpansionLink from 'components/navigation/EssentialExpansionLink.vue';
|
import EssentialExpansionLink from 'components/navigation/EssentialExpansionLink.vue';
|
||||||
import draggable from 'vuedraggable';
|
import drag from 'vuedraggable';
|
||||||
const drag: ComponentPublicInstance = <ComponentPublicInstance>draggable;
|
|
||||||
const essentials: FG_Plugin.MenuLink[] = [
|
const essentials: FG_Plugin.MenuLink[] = [
|
||||||
{
|
{
|
||||||
title: 'Über Flaschengeist',
|
title: 'Über Flaschengeist',
|
||||||
|
@ -104,7 +104,13 @@ const essentials: FG_Plugin.MenuLink[] = [
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'MainLayout',
|
name: 'MainLayout',
|
||||||
components: { EssentialExpansionLink, EssentialLink, ShortcutLink, Notification, drag },
|
components: {
|
||||||
|
EssentialExpansionLink,
|
||||||
|
EssentialLink,
|
||||||
|
ShortcutLink,
|
||||||
|
Notification,
|
||||||
|
drag: <ComponentPublicInstance>drag,
|
||||||
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const mainStore = useMainStore();
|
const mainStore = useMainStore();
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, inject } from 'vue';
|
import { defineComponent, inject } from 'vue';
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
import ShortcutLink from 'components/navigation/ShortcutLink.vue';
|
import ShortcutLink from 'components/navigation/ShortcutLink.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, inject } from 'vue';
|
import { computed, defineComponent, inject } from 'vue';
|
||||||
import { hasPermissions } from 'src/utils/permission';
|
import { hasPermissions } from '@flaschengeist/api';
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
|
|
|
@ -37,14 +37,14 @@
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<div class="row justify-end">
|
<div class="row justify-end">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="$q.platform.is.cordova || $q.platform.is.electron"
|
v-if="quasar.platform.is.cordova || quasar.platform.is.electron"
|
||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
icon="mdi-menu-down"
|
icon="mdi-menu-down"
|
||||||
@click="openServerSettings"
|
@click="openServerSettings"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-slide-transition v-if="$q.platform.is.cordova || $q.platform.is.electron">
|
<q-slide-transition v-if="quasar.platform.is.cordova || quasar.platform.is.electron">
|
||||||
<div v-show="visible">
|
<div v-show="visible">
|
||||||
<q-separator />
|
<q-separator />
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
|
@ -63,12 +63,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { Loading, Notify } from 'quasar';
|
import { Loading, Notify } from 'quasar';
|
||||||
import { useMainStore } from 'src/stores';
|
import { notEmpty, useMainStore, useUserStore } from '@flaschengeist/api';
|
||||||
|
import { PasswordInput } from '@flaschengeist/api/components';
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
import { setBaseURL, api } from 'boot/axios';
|
import { setBaseURL, api } from 'boot/axios';
|
||||||
import { notEmpty } from 'src/utils/validators';
|
|
||||||
import { useUserStore } from 'src/plugins/user/store';
|
|
||||||
import PasswordInput from 'src/components/utils/PasswordInput.vue';
|
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
@ -76,6 +74,7 @@ export default defineComponent({
|
||||||
components: { PasswordInput },
|
components: { PasswordInput },
|
||||||
setup() {
|
setup() {
|
||||||
const mainStore = useMainStore();
|
const mainStore = useMainStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
const mainRoute = { name: 'dashboard' };
|
const mainRoute = { name: 'dashboard' };
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -84,7 +83,7 @@ export default defineComponent({
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
const server = ref<string | undefined>(api.defaults.baseURL);
|
const server = ref<string | undefined>(api.defaults.baseURL);
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const $q = useQuasar();
|
const quasar = useQuasar();
|
||||||
|
|
||||||
function openServerSettings() {
|
function openServerSettings() {
|
||||||
visible.value = !visible.value;
|
visible.value = !visible.value;
|
||||||
|
@ -101,7 +100,7 @@ export default defineComponent({
|
||||||
const status = await mainStore.login(userid.value, password.value);
|
const status = await mainStore.login(userid.value, password.value);
|
||||||
|
|
||||||
if (status === true) {
|
if (status === true) {
|
||||||
mainStore.user = (await useUserStore().getUser(userid.value, true)) || undefined;
|
mainStore.user = (await userStore.getUser(userid.value, true)) || undefined;
|
||||||
const x = router.currentRoute.value.query['redirect'];
|
const x = router.currentRoute.value.query['redirect'];
|
||||||
void router.push(typeof x === 'string' ? { path: x } : mainRoute);
|
void router.push(typeof x === 'string' ? { path: x } : mainRoute);
|
||||||
} else {
|
} else {
|
||||||
|
@ -158,7 +157,7 @@ export default defineComponent({
|
||||||
server,
|
server,
|
||||||
userid,
|
userid,
|
||||||
visible,
|
visible,
|
||||||
$q,
|
quasar,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -46,9 +46,9 @@ export default defineComponent({
|
||||||
const ival = setInterval(() => {
|
const ival = setInterval(() => {
|
||||||
reload.value -= 1;
|
reload.value -= 1;
|
||||||
if (reload.value <= 0) {
|
if (reload.value <= 0) {
|
||||||
|
if (router.currentRoute.value.params && 'refresh' in router.currentRoute.value.params)
|
||||||
|
router.go(0);
|
||||||
const path = router.currentRoute.value.query.redirect;
|
const path = router.currentRoute.value.query.redirect;
|
||||||
console.log('Offline: ');
|
|
||||||
console.log(path);
|
|
||||||
void router.replace(path ? { path: <string>path } : { name: 'login' });
|
void router.replace(path ? { path: <string>path } : { name: 'login' });
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
|
@ -34,10 +34,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { useMainStore } from '@flaschengeist/api';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { Loading, Notify } from 'quasar';
|
import { Loading, Notify } from 'quasar';
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
// name: 'PageName'
|
// name: 'PageName'
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, inject } from 'vue';
|
import { defineComponent, inject } from 'vue';
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
import Developer from 'components/about/Developer.vue';
|
import Developer from 'components/about/Developer.vue';
|
||||||
|
|
||||||
const developers = [
|
const developers = [
|
||||||
|
|
|
@ -1,115 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card>
|
|
||||||
<BalanceHeader v-model="user" :show-selector="showSelector" @open-history="openHistory" />
|
|
||||||
<q-separator />
|
|
||||||
|
|
||||||
<q-card-section v-if="shortCuts" class="row q-col-gutter-md">
|
|
||||||
<div v-for="(shortcut, index) in shortCuts" :key="index" class="col-4">
|
|
||||||
<q-btn
|
|
||||||
v-if="shortcut"
|
|
||||||
push
|
|
||||||
color="primary"
|
|
||||||
style="width: 100%"
|
|
||||||
:label="shortcut.toFixed(2).toString() + ' €'"
|
|
||||||
@click="changeBalance(shortcut)"
|
|
||||||
>
|
|
||||||
<q-popup-proxy context-menu>
|
|
||||||
<q-btn label="Entfernen" @click="removeShortcut(shortcut)" />
|
|
||||||
</q-popup-proxy>
|
|
||||||
<q-tooltip>Rechtsklick um Verknüpfung zu entfernen</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div></q-card-section
|
|
||||||
>
|
|
||||||
<q-card-section class="row q-col-gutter-md items-center">
|
|
||||||
<div class="col-sm-4 col-xs-12">
|
|
||||||
<q-input
|
|
||||||
v-model.number="amount"
|
|
||||||
type="number"
|
|
||||||
filled
|
|
||||||
label="Eigener Betrag"
|
|
||||||
step="0.1"
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4 col-xs-6">
|
|
||||||
<q-btn
|
|
||||||
style="width: 100%"
|
|
||||||
color="primary"
|
|
||||||
label="Anschreiben"
|
|
||||||
@click="changeBalance(amount * -1)"
|
|
||||||
><q-tooltip>Rechtsklick um Betrag als Verknüpfung hinzuzufügen</q-tooltip>
|
|
||||||
<q-popup-proxy v-model="showAddShortcut" context-menu>
|
|
||||||
<q-btn label="neue Verknüpfung" @click="addShortcut"></q-btn>
|
|
||||||
</q-popup-proxy>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4 col-xs-6">
|
|
||||||
<q-btn
|
|
||||||
v-if="canAddCredit"
|
|
||||||
style="width: 100%"
|
|
||||||
color="secondary"
|
|
||||||
label="Gutschreiben"
|
|
||||||
@click="changeBalance(amount)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, ref, defineComponent, onBeforeMount } from 'vue';
|
|
||||||
import { hasPermission } from 'src/utils/permission';
|
|
||||||
import BalanceHeader from '../components/BalanceHeader.vue';
|
|
||||||
import PERMISSIONS from '../permissions';
|
|
||||||
import { useBalanceStore } from '../store';
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'BalanceAdd',
|
|
||||||
components: { BalanceHeader },
|
|
||||||
emits: { 'open-history': () => true },
|
|
||||||
setup(_, { emit }) {
|
|
||||||
const store = useBalanceStore();
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
void store.getShortcuts();
|
|
||||||
});
|
|
||||||
|
|
||||||
const amount = ref<number>(0);
|
|
||||||
const showAddShortcut = ref(false);
|
|
||||||
const user = ref(mainStore.currentUser);
|
|
||||||
const shortCuts = computed(() => store.shortcuts);
|
|
||||||
|
|
||||||
const canAddCredit = hasPermission(PERMISSIONS.CREDIT);
|
|
||||||
const showSelector = hasPermission(PERMISSIONS.DEBIT) || hasPermission(PERMISSIONS.CREDIT);
|
|
||||||
|
|
||||||
function addShortcut() {
|
|
||||||
if (amount.value != 0) void store.createShortcut(amount.value * -1);
|
|
||||||
}
|
|
||||||
function removeShortcut(shortcut: number) {
|
|
||||||
void store.removeShortcut(shortcut);
|
|
||||||
}
|
|
||||||
async function changeBalance(amount: number) {
|
|
||||||
await store.changeBalance(amount, user.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openHistory() {
|
|
||||||
emit('open-history');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
user,
|
|
||||||
addShortcut,
|
|
||||||
canAddCredit,
|
|
||||||
removeShortcut,
|
|
||||||
showAddShortcut,
|
|
||||||
changeBalance,
|
|
||||||
amount,
|
|
||||||
showSelector,
|
|
||||||
shortCuts,
|
|
||||||
openHistory,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,66 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card-section class="fit row justify-left content-center items-center q-col-gutter-sm">
|
|
||||||
<div class="col-5">
|
|
||||||
<div v-if="balance" class="text-h6">
|
|
||||||
Aktueller Stand: {{ balance.balance.toFixed(2) }} €
|
|
||||||
<q-badge v-if="isLocked" color="negative" align="top"> gesperrt </q-badge>
|
|
||||||
</div>
|
|
||||||
<q-spinner v-else color="primary" size="3em" />
|
|
||||||
</div>
|
|
||||||
<div v-if="showSelector" class="col-6">
|
|
||||||
<UserSelector v-model="user" />
|
|
||||||
</div>
|
|
||||||
<div class="col-1 justify-end">
|
|
||||||
<q-btn round flat icon="mdi-format-list-checks" @click="openHistory" />
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, onBeforeMount, PropType } from 'vue';
|
|
||||||
import UserSelector from 'src/plugins/user/components/UserSelector.vue';
|
|
||||||
import { useBalanceStore } from '../store';
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'BalanceHeader',
|
|
||||||
components: { UserSelector },
|
|
||||||
props: {
|
|
||||||
showSelector: Boolean,
|
|
||||||
modelValue: {
|
|
||||||
required: true,
|
|
||||||
type: Object as PropType<FG.User>,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: { 'update:modelValue': (u: FG.User) => !!u, 'open-history': () => true },
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const store = useBalanceStore();
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
|
|
||||||
onBeforeMount(() => void store.getBalance(mainStore.currentUser));
|
|
||||||
|
|
||||||
const balance = computed(() =>
|
|
||||||
store.balances.find((x) => x.userid === props.modelValue.userid)
|
|
||||||
);
|
|
||||||
|
|
||||||
const isLocked = computed(
|
|
||||||
() =>
|
|
||||||
balance.value === undefined ||
|
|
||||||
(balance.value.limit !== undefined && balance.value.balance <= balance.value.limit)
|
|
||||||
);
|
|
||||||
const user = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (x: FG.User) => {
|
|
||||||
void store.getBalance(x);
|
|
||||||
emit('update:modelValue', x);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function openHistory() {
|
|
||||||
emit('open-history');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { user, balance, isLocked, openHistory };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,75 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card>
|
|
||||||
<BalanceHeader v-model="sender" :show-selector="showSelector" @open-history="openHistory" />
|
|
||||||
<q-separator />
|
|
||||||
<q-card-section class="row q-col-gutter-md items-center">
|
|
||||||
<div class="col-sm-4 col-xs-12">
|
|
||||||
<q-input v-model.number="amount" type="number" filled label="Betrag" step="0.1" min="0" />
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4 col-xs-6">
|
|
||||||
<UserSelector v-model="receiver" label="Empfänger" />
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4 col-xs-6">
|
|
||||||
<q-btn
|
|
||||||
style="width: 100%"
|
|
||||||
color="primary"
|
|
||||||
:disable="sendDisabled"
|
|
||||||
label="Senden"
|
|
||||||
@click="sendAmount"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, ref, defineComponent } from 'vue';
|
|
||||||
import { hasPermission } from 'src/utils/permission';
|
|
||||||
import UserSelector from 'src/plugins/user/components/UserSelector.vue';
|
|
||||||
import BalanceHeader from '../components/BalanceHeader.vue';
|
|
||||||
import PERMISSIONS from '../permissions';
|
|
||||||
import { useBalanceStore } from '../store';
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'BalanceTransfer',
|
|
||||||
components: { BalanceHeader, UserSelector },
|
|
||||||
emits: { 'open-history': () => true },
|
|
||||||
setup(_, { emit }) {
|
|
||||||
const store = useBalanceStore();
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
|
|
||||||
const showSelector = computed(() => hasPermission(PERMISSIONS.SEND_OTHER));
|
|
||||||
const sender = ref<FG.User | undefined>(mainStore.currentUser);
|
|
||||||
const receiver = ref<FG.User | undefined>(undefined);
|
|
||||||
const amount = ref<number>(0);
|
|
||||||
|
|
||||||
const sendDisabled = computed(() => {
|
|
||||||
return !(
|
|
||||||
receiver.value &&
|
|
||||||
sender.value &&
|
|
||||||
sender.value.userid != receiver.value.userid &&
|
|
||||||
amount.value > 0
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function sendAmount() {
|
|
||||||
if (receiver.value) await store.changeBalance(amount.value, receiver.value, sender.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openHistory() {
|
|
||||||
emit('open-history');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
sender,
|
|
||||||
receiver,
|
|
||||||
amount,
|
|
||||||
sendAmount,
|
|
||||||
showSelector,
|
|
||||||
sendDisabled,
|
|
||||||
openHistory,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,107 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<q-card flat square>
|
|
||||||
<q-card-section class="row items-center justify-between" horizontal>
|
|
||||||
<div class="col-5 text-left q-px-sm">
|
|
||||||
<div :class="{ 'text-negative': isNegative() }" class="text-weight-bold text-h6">
|
|
||||||
<span v-if="isNegative()">-</span>{{ transaction.amount.toFixed(2) }} €
|
|
||||||
</div>
|
|
||||||
<div class="text-caption">{{ text }}</div>
|
|
||||||
<div class="text-caption">{{ timeStr }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-5 q-px-sm text-center">
|
|
||||||
<div v-if="isReversed" class="text-subtitle1">Storniert</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-2 q-pr-sm" style="text-align: right">
|
|
||||||
<q-btn
|
|
||||||
:color="isReversed ? 'positive' : 'negative'"
|
|
||||||
aria-label="Reverse transaction"
|
|
||||||
:icon="!isReversed ? 'mdi-trash-can' : 'mdi-check-bold'"
|
|
||||||
size="sm"
|
|
||||||
round
|
|
||||||
:disable="!canReverse"
|
|
||||||
@click="reverse"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
<q-separator />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { ref, computed, defineComponent, onUnmounted, onMounted, PropType } from 'vue';
|
|
||||||
import { hasPermission } from 'src/utils/permission';
|
|
||||||
import { formatDateTime } from 'src/utils/datetime';
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
import { useUserStore } from 'src/plugins/user/store';
|
|
||||||
import { useBalanceStore } from '../store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'Transaction',
|
|
||||||
props: {
|
|
||||||
transaction: {
|
|
||||||
required: true,
|
|
||||||
type: Object as PropType<FG.Transaction>,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: { 'update:transaction': (t: FG.Transaction) => !!t },
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const balanceStore = useBalanceStore();
|
|
||||||
const now = ref(Date.now());
|
|
||||||
const ival = setInterval(() => (now.value = Date.now()), 1000);
|
|
||||||
const text = ref('');
|
|
||||||
|
|
||||||
onUnmounted(() => clearInterval(ival));
|
|
||||||
onMounted(() => refreshText());
|
|
||||||
|
|
||||||
const isNegative = () => props.transaction.sender_id === mainStore.currentUser.userid;
|
|
||||||
|
|
||||||
const refreshText = async () => {
|
|
||||||
if (isNegative()) {
|
|
||||||
text.value = 'Anschreiben';
|
|
||||||
if (props.transaction.receiver_id) {
|
|
||||||
const user = <FG.User>await userStore.getUser(props.transaction.receiver_id);
|
|
||||||
text.value = `Gesendet an ${user.display_name}`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
text.value = 'Gutschrift';
|
|
||||||
if (props.transaction.sender_id) {
|
|
||||||
const user = <FG.User>await userStore.getUser(props.transaction.sender_id);
|
|
||||||
text.value = `Bekommen von ${user.display_name}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isReversed = computed(
|
|
||||||
() => props.transaction.reversal_id != undefined || props.transaction.original_id != undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const canReverse = computed(
|
|
||||||
() =>
|
|
||||||
!isReversed.value &&
|
|
||||||
(hasPermission('balance_reversal') ||
|
|
||||||
(props.transaction.sender_id === mainStore.currentUser.userid &&
|
|
||||||
now.value - props.transaction.time.getTime() < 10000))
|
|
||||||
);
|
|
||||||
|
|
||||||
function reverse() {
|
|
||||||
if (canReverse.value)
|
|
||||||
void balanceStore.revert(props.transaction).then(() => {
|
|
||||||
emit('update:transaction', props.transaction);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeStr = computed(() => {
|
|
||||||
const elapsed = (now.value - props.transaction.time.getTime()) / 1000;
|
|
||||||
if (elapsed < 60) return `Vor ${elapsed.toFixed()} s`;
|
|
||||||
return formatDateTime(props.transaction.time, elapsed > 12 * 60 * 60, true, true) + ' Uhr';
|
|
||||||
});
|
|
||||||
|
|
||||||
return { timeStr, reverse, isNegative, text, refreshText, canReverse, isReversed };
|
|
||||||
},
|
|
||||||
watch: { transaction: 'refreshText' },
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,29 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card style="text-align: center">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">Gerücht: {{ balance.toFixed(2) }} €</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
import { useBalanceStore } from '../store';
|
|
||||||
import { computed, defineComponent, onBeforeMount } from 'vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'BalanceWidget',
|
|
||||||
setup() {
|
|
||||||
const store = useBalanceStore();
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
void store.getBalance(mainStore.currentUser);
|
|
||||||
});
|
|
||||||
|
|
||||||
const balance = computed(() => store.balance?.balance || NaN);
|
|
||||||
|
|
||||||
return { balance };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,59 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<q-page padding>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<q-table :rows="rows" row-key="userid" :columns="columns" />
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-page>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
// TODO: Fill usefull data
|
|
||||||
|
|
||||||
import { ref, defineComponent, onMounted } from 'vue';
|
|
||||||
import { useBalanceStore } from '../store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
// name: 'PageName'
|
|
||||||
setup() {
|
|
||||||
const store = useBalanceStore();
|
|
||||||
|
|
||||||
onMounted(() => void store.getBalances().then((balances) => rows.value.push(...balances)));
|
|
||||||
|
|
||||||
const rows = ref(store.balances);
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'userid',
|
|
||||||
label: 'Benutzer ID',
|
|
||||||
field: 'userid',
|
|
||||||
required: true,
|
|
||||||
align: 'left',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'credit',
|
|
||||||
label: 'Haben',
|
|
||||||
field: 'credit',
|
|
||||||
format: (val: number) => val.toFixed(2)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'debit',
|
|
||||||
label: 'Soll',
|
|
||||||
field: 'debit',
|
|
||||||
format: (val: number) => val.toFixed(2)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'balance',
|
|
||||||
label: 'Kontostand',
|
|
||||||
format: (_: undefined, row: { debit: number; credit: number }) =>
|
|
||||||
(row.credit - row.debit).toFixed(2)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
return { rows, columns };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,135 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
|
|
||||||
<q-tabs v-if="$q.screen.gt.sm" v-model="tab">
|
|
||||||
<q-tab
|
|
||||||
v-for="(tabindex, index) in tabs"
|
|
||||||
:key="'tab' + index"
|
|
||||||
:name="tabindex.name"
|
|
||||||
:label="tabindex.label"
|
|
||||||
/>
|
|
||||||
</q-tabs>
|
|
||||||
<div v-else class="fit row justify-end">
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
round
|
|
||||||
icon="mdi-menu"
|
|
||||||
@click="
|
|
||||||
showDrawer = !showDrawer;
|
|
||||||
show = false;
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<q-drawer v-model="showDrawer" side="right" behavior="mobile" @click="showDrawer = !showDrawer">
|
|
||||||
<q-list v-if="!$q.screen.gt.sm && !show" v-model="tab">
|
|
||||||
<q-item
|
|
||||||
v-for="(tabindex, index) in tabs"
|
|
||||||
:key="'tab' + index"
|
|
||||||
:active="tab == tabindex.name"
|
|
||||||
clickable
|
|
||||||
@click="tab = tabindex.name"
|
|
||||||
>
|
|
||||||
<q-item-label>{{ tabindex.label }}</q-item-label>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
<q-list v-if="show">
|
|
||||||
<div v-for="(transaction, index) in transactions" :key="index" class="col-sm-12">
|
|
||||||
<Transaction v-model:transaction="transactions[index]" />
|
|
||||||
</div>
|
|
||||||
</q-list>
|
|
||||||
</q-drawer>
|
|
||||||
<q-tab-panels
|
|
||||||
v-model="tab"
|
|
||||||
style="background-color: transparent"
|
|
||||||
class="q-pa-none col-12"
|
|
||||||
animated
|
|
||||||
>
|
|
||||||
<q-tab-panel name="add" class="q-px-xs">
|
|
||||||
<BalanceAdd
|
|
||||||
@open-history="
|
|
||||||
showDrawer = !showDrawer;
|
|
||||||
show = true;
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</q-tab-panel>
|
|
||||||
<q-tab-panel name="transfer" class="q-px-xs">
|
|
||||||
<BalanceTransfer
|
|
||||||
@open-history="
|
|
||||||
showDrawer = !showDrawer;
|
|
||||||
show = true;
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</q-tab-panel>
|
|
||||||
</q-tab-panels>
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, ref, onMounted } from 'vue';
|
|
||||||
import { hasSomePermissions } from 'src/utils/permission';
|
|
||||||
import PERMISSIONS from '../permissions';
|
|
||||||
import BalanceAdd from '../components/BalanceAdd.vue';
|
|
||||||
import BalanceTransfer from '../components/BalanceTransfer.vue';
|
|
||||||
import Transaction from '../components/Transaction.vue';
|
|
||||||
import { useBalanceStore } from '../store';
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'BalanceManage',
|
|
||||||
components: { BalanceAdd, BalanceTransfer, Transaction },
|
|
||||||
setup() {
|
|
||||||
const balanceStore = useBalanceStore();
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
onMounted(() => {
|
|
||||||
void balanceStore.getTransactions(mainStore.currentUser, {
|
|
||||||
from: new Date(now.getFullYear(), now.getMonth(), now.getDate()),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const transactions = computed(() => {
|
|
||||||
return balanceStore.transactions
|
|
||||||
.filter((t) => t.original_id == undefined)
|
|
||||||
.filter((t) => t.time > new Date(now.getFullYear(), now.getMonth(), now.getDate()))
|
|
||||||
.sort((a, b) => (a.time >= b.time ? -1 : 1));
|
|
||||||
});
|
|
||||||
|
|
||||||
const canAdd = () =>
|
|
||||||
hasSomePermissions([PERMISSIONS.DEBIT, PERMISSIONS.CREDIT, PERMISSIONS.DEBIT_OWN]);
|
|
||||||
|
|
||||||
interface Tab {
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabs: Tab[] = [
|
|
||||||
...(canAdd() ? [{ name: 'add', label: 'Anschreiben' }] : []),
|
|
||||||
...(hasSomePermissions([PERMISSIONS.SEND, PERMISSIONS.SEND_OTHER])
|
|
||||||
? [{ name: 'transfer', label: 'Übertragen' }]
|
|
||||||
: []),
|
|
||||||
];
|
|
||||||
|
|
||||||
//const drawer = ref<boolean>(false);
|
|
||||||
|
|
||||||
/* const showDrawer = computed({
|
|
||||||
get: () => {
|
|
||||||
return !Screen.gt.sm && drawer.value;
|
|
||||||
},
|
|
||||||
set: (val: boolean) => {
|
|
||||||
drawer.value = val;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
const showDrawer = ref<boolean>(false);
|
|
||||||
const tab = ref<string>(canAdd() ? 'add' : 'transfer');
|
|
||||||
const show = ref<boolean>(false);
|
|
||||||
return {
|
|
||||||
showDrawer,
|
|
||||||
tab,
|
|
||||||
tabs,
|
|
||||||
transactions,
|
|
||||||
show,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,166 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-page padding>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section class="text-center">
|
|
||||||
<div class="text-h4">Aktueller Stand</div>
|
|
||||||
<div style="font-size: 2em">{{ balance.toFixed(2) }} €</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-separator />
|
|
||||||
<q-card-section>
|
|
||||||
<q-table
|
|
||||||
v-model:pagination="pagination"
|
|
||||||
title="Buchungen"
|
|
||||||
:rows="data"
|
|
||||||
:columns="columns"
|
|
||||||
row-key="id"
|
|
||||||
:loading="loading"
|
|
||||||
binary-state-sort
|
|
||||||
@request="onRequest"
|
|
||||||
>
|
|
||||||
<template #top>
|
|
||||||
<q-toggle v-model="showCancelled" label="Stornierte einblenden" />
|
|
||||||
</template>
|
|
||||||
<template #body-cell="props">
|
|
||||||
<q-td :props="props" :class="{ 'bg-grey': props.row.reversal_id != null }">
|
|
||||||
{{ props.value }}
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, onMounted, ref } from 'vue';
|
|
||||||
import { formatDateTime } from 'src/utils/datetime';
|
|
||||||
import { useBalanceStore } from '../store';
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
import { useUserStore } from 'src/plugins/user/store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
// name: 'PageName'
|
|
||||||
setup() {
|
|
||||||
const store = useBalanceStore();
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
void store.getBalance(mainStore.currentUser);
|
|
||||||
void userStore.getUsers().then(() =>
|
|
||||||
onRequest({
|
|
||||||
pagination: pagination.value,
|
|
||||||
filter: undefined
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const showCancelled = ref(false);
|
|
||||||
const data = ref<FG.Transaction[]>([]);
|
|
||||||
const loading = ref(false);
|
|
||||||
const pagination = ref({
|
|
||||||
sortBy: 'time',
|
|
||||||
descending: false,
|
|
||||||
page: 1,
|
|
||||||
rowsPerPage: 3,
|
|
||||||
rowsNumber: 10
|
|
||||||
});
|
|
||||||
|
|
||||||
interface PaginationInterface {
|
|
||||||
sortBy: string;
|
|
||||||
descending: boolean;
|
|
||||||
page: number;
|
|
||||||
rowsPerPage: number;
|
|
||||||
rowsNumber: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onRequest(props: { pagination: PaginationInterface; filter?: string }) {
|
|
||||||
const { page, rowsPerPage, sortBy, descending } = props.pagination;
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
// get all rows if "All" (0) is selected
|
|
||||||
const fetchCount = rowsPerPage === 0 ? pagination.value.rowsNumber : rowsPerPage;
|
|
||||||
// calculate starting row of data
|
|
||||||
const startRow = (page - 1) * rowsPerPage;
|
|
||||||
try {
|
|
||||||
const result = await store.getTransactions(mainStore.currentUser, {
|
|
||||||
offset: startRow,
|
|
||||||
limit: fetchCount,
|
|
||||||
showCancelled: showCancelled.value,
|
|
||||||
showReversals: false
|
|
||||||
});
|
|
||||||
// clear out existing data and add new
|
|
||||||
data.value.splice(0, data.value.length, ...result.transactions);
|
|
||||||
// don't forget to update local pagination object
|
|
||||||
pagination.value.page = page;
|
|
||||||
pagination.value.rowsPerPage = rowsPerPage;
|
|
||||||
pagination.value.sortBy = sortBy;
|
|
||||||
pagination.value.descending = descending;
|
|
||||||
if (result.count) pagination.value.rowsNumber = result.count;
|
|
||||||
} catch (error) {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const balance = computed(() => store.balance?.balance || NaN);
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'time',
|
|
||||||
label: 'Datum',
|
|
||||||
field: 'time',
|
|
||||||
required: true,
|
|
||||||
sortable: true,
|
|
||||||
format: (val: Date) => formatDateTime(new Date(val), true, true, true)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'type',
|
|
||||||
label: 'Type',
|
|
||||||
format: (_: undefined, row: FG.Transaction) => {
|
|
||||||
if (row.sender_id == null) return 'Gutschrift';
|
|
||||||
else {
|
|
||||||
if (row.receiver_id == null) return 'Angeschrieben';
|
|
||||||
else {
|
|
||||||
if (row.receiver_id === mainStore.currentUser.userid) return 'Bekommen von X';
|
|
||||||
else return 'Gesendet an X';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'text',
|
|
||||||
label: 'Text',
|
|
||||||
format: (_: undefined, row: FG.Transaction) => {
|
|
||||||
if (row.sender_id == null) return 'Gutschrift';
|
|
||||||
else {
|
|
||||||
if (row.receiver_id == null) return 'Angeschrieben';
|
|
||||||
else {
|
|
||||||
if (row.receiver_id === mainStore.currentUser.userid) return 'Bekommen von X';
|
|
||||||
else return 'Gesendet an X';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'amount',
|
|
||||||
label: 'Betrag',
|
|
||||||
field: 'amount',
|
|
||||||
format: (val: number) => `${val.toFixed(2)}€`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'author_id',
|
|
||||||
label: 'Benutzer',
|
|
||||||
field: 'author_id',
|
|
||||||
format: (val: string) => {
|
|
||||||
const user = userStore.users.filter((x) => x.userid == val);
|
|
||||||
if (user.length > 0) return user[0].display_name;
|
|
||||||
else return val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return { data, pagination, onRequest, loading, balance, columns, showCancelled };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,21 +0,0 @@
|
||||||
const PERMISSIONS = {
|
|
||||||
// Show own and others balance
|
|
||||||
SHOW: 'balance_show',
|
|
||||||
SHOW_OTHER: 'balance_show_others',
|
|
||||||
// Credit balance (give)
|
|
||||||
CREDIT: 'balance_credit',
|
|
||||||
// Debit balance (take)
|
|
||||||
DEBIT: 'balance_debit',
|
|
||||||
// Debit own balance only
|
|
||||||
DEBIT_OWN: 'balance_debit_own',
|
|
||||||
// Send from to other
|
|
||||||
SEND: 'balance_send',
|
|
||||||
// Send from other to another
|
|
||||||
SEND_OTHER: 'balance_send_others',
|
|
||||||
// Can set limit for users
|
|
||||||
SET_LIMIT: 'balance_set_limit',
|
|
||||||
//Allow sending / sub while exceeding the set limit
|
|
||||||
EXCEED_LIMIT: 'balance_exceed_limit',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PERMISSIONS;
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
|
||||||
import { defineAsyncComponent } from 'vue';
|
|
||||||
import routes from './routes';
|
|
||||||
|
|
||||||
const plugin: FG_Plugin.Plugin = {
|
|
||||||
name: 'Balance',
|
|
||||||
innerRoutes: routes,
|
|
||||||
requiredModules: ['User'],
|
|
||||||
requiredBackendModules: ['balance'],
|
|
||||||
version: '0.0.2',
|
|
||||||
widgets: [
|
|
||||||
{
|
|
||||||
priority: 0,
|
|
||||||
name: 'current',
|
|
||||||
permissions: ['balance_show'],
|
|
||||||
widget: defineAsyncComponent(() => import('./components/Widget.vue')),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default plugin;
|
|
|
@ -1,50 +0,0 @@
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
|
||||||
import permissions from '../permissions';
|
|
||||||
|
|
||||||
const mainRoutes: FG_Plugin.MenuRoute[] = [
|
|
||||||
{
|
|
||||||
title: 'Gerücht',
|
|
||||||
icon: 'mdi-cash-100',
|
|
||||||
permissions: ['user'],
|
|
||||||
route: {
|
|
||||||
path: 'balance',
|
|
||||||
name: 'balance',
|
|
||||||
redirect: { name: 'balance-view' },
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
title: 'Übersicht',
|
|
||||||
icon: 'mdi-cash-check',
|
|
||||||
permissions: [permissions.SHOW],
|
|
||||||
route: {
|
|
||||||
path: 'overview',
|
|
||||||
name: 'balance-view',
|
|
||||||
component: () => import('../pages/Overview.vue'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Buchen',
|
|
||||||
icon: 'mdi-cash-plus',
|
|
||||||
shortcut: true,
|
|
||||||
permissions: [permissions.DEBIT_OWN, permissions.SHOW],
|
|
||||||
route: {
|
|
||||||
path: 'change',
|
|
||||||
name: 'balance-change',
|
|
||||||
component: () => import('../pages/MainPage.vue'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Verwaltung',
|
|
||||||
icon: 'mdi-account-cash',
|
|
||||||
permissions: [permissions.SET_LIMIT, permissions.SHOW_OTHER],
|
|
||||||
route: {
|
|
||||||
path: 'admin',
|
|
||||||
name: 'balance-admin',
|
|
||||||
component: () => import('../pages/Admin.vue'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default mainRoutes;
|
|
|
@ -1,166 +0,0 @@
|
||||||
import { api } from 'src/boot/axios';
|
|
||||||
|
|
||||||
interface BalanceResponse {
|
|
||||||
balance: number;
|
|
||||||
credit: number;
|
|
||||||
debit: number;
|
|
||||||
limit?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BalancesResponse extends BalanceResponse {
|
|
||||||
userid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransactionsResponse {
|
|
||||||
transactions: Array<FG.Transaction>;
|
|
||||||
count?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
import { AxiosResponse } from 'axios';
|
|
||||||
import { Notify } from 'quasar';
|
|
||||||
|
|
||||||
function fixTransaction(t: FG.Transaction) {
|
|
||||||
t.time = new Date(t.time);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useBalanceStore = defineStore({
|
|
||||||
id: 'balance',
|
|
||||||
|
|
||||||
state: () => ({
|
|
||||||
balances: [] as BalancesResponse[],
|
|
||||||
shortcuts: [] as number[],
|
|
||||||
transactions: [] as FG.Transaction[],
|
|
||||||
_balances_dirty: 0,
|
|
||||||
}),
|
|
||||||
|
|
||||||
getters: {
|
|
||||||
balance(): BalancesResponse | undefined {
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
return this.balances.find((v) => v.userid === mainStore.user?.userid);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
async createShortcut(shortcut: number) {
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
this.shortcuts.push(shortcut);
|
|
||||||
this.shortcuts.sort((a, b) => a - b);
|
|
||||||
await api.put(`/users/${mainStore.currentUser.userid}/balance/shortcuts`, this.shortcuts);
|
|
||||||
},
|
|
||||||
|
|
||||||
async removeShortcut(shortcut: number) {
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
this.shortcuts = this.shortcuts.filter((value: number) => value !== shortcut);
|
|
||||||
this.shortcuts.sort((a, b) => a - b);
|
|
||||||
await api.put(`/users/${mainStore.currentUser.userid}/balance/shortcuts`, this.shortcuts);
|
|
||||||
},
|
|
||||||
|
|
||||||
async getShortcuts(force = false) {
|
|
||||||
if (force || this.shortcuts.length == 0) {
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
const { data } = await api.get<number[]>(
|
|
||||||
`/users/${mainStore.currentUser.userid}/balance/shortcuts`
|
|
||||||
);
|
|
||||||
this.shortcuts = data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async getBalance(user: FG.User) {
|
|
||||||
const { data } = await api.get<BalanceResponse>(`/users/${user.userid}/balance`);
|
|
||||||
const idx = this.balances.findIndex((x) => x.userid === user.userid);
|
|
||||||
if (idx == -1) this.balances.push(Object.assign(data, { userid: user.userid }));
|
|
||||||
else this.balances[idx] = Object.assign(data, { userid: user.userid });
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async getBalances(force = false) {
|
|
||||||
if (
|
|
||||||
force ||
|
|
||||||
this.balances.length == 0 ||
|
|
||||||
new Date().getTime() - this._balances_dirty > 60000
|
|
||||||
) {
|
|
||||||
const { data } = await api.get<BalancesResponse[]>('/balance');
|
|
||||||
this.balances = data;
|
|
||||||
}
|
|
||||||
return this.balances;
|
|
||||||
},
|
|
||||||
|
|
||||||
async changeBalance(amount: number, user: FG.User, sender: FG.User | undefined = undefined) {
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
try {
|
|
||||||
const { data } = await api.put<FG.Transaction>(`/users/${user.userid}/balance`, {
|
|
||||||
amount,
|
|
||||||
user: user.userid,
|
|
||||||
sender: sender?.userid,
|
|
||||||
});
|
|
||||||
fixTransaction(data);
|
|
||||||
if (
|
|
||||||
user.userid === mainStore.currentUser.userid ||
|
|
||||||
sender?.userid === mainStore.currentUser.userid
|
|
||||||
)
|
|
||||||
this.transactions.push(data);
|
|
||||||
const f = this.balances.find((x) => x.userid === user.userid);
|
|
||||||
if (f) f.balance += amount;
|
|
||||||
if (sender) {
|
|
||||||
const f = this.balances.find((x) => x.userid === sender.userid);
|
|
||||||
if (f) f.balance += -1 * amount;
|
|
||||||
}
|
|
||||||
this._balances_dirty = 0;
|
|
||||||
return data;
|
|
||||||
} catch ({ response }) {
|
|
||||||
// Maybe Balance changed
|
|
||||||
if (response && (<AxiosResponse>response).status == 409) {
|
|
||||||
Notify.create({
|
|
||||||
type: 'negative',
|
|
||||||
group: false,
|
|
||||||
message: 'Das Limit wurde überschritten!',
|
|
||||||
timeout: 10000,
|
|
||||||
progress: true,
|
|
||||||
actions: [{ icon: 'mdi-close', color: 'white' }],
|
|
||||||
});
|
|
||||||
//void this.getTransactions(true);
|
|
||||||
void this.getBalance(sender ? sender : user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async getTransactions(
|
|
||||||
user: FG.User,
|
|
||||||
filter:
|
|
||||||
| {
|
|
||||||
limit?: number;
|
|
||||||
offset?: number;
|
|
||||||
from?: Date;
|
|
||||||
to?: Date;
|
|
||||||
showReversals?: boolean;
|
|
||||||
showCancelled?: boolean;
|
|
||||||
}
|
|
||||||
| undefined = undefined
|
|
||||||
) {
|
|
||||||
if (!filter) filter = { limit: 10 };
|
|
||||||
const { data } = await api.get<TransactionsResponse>(
|
|
||||||
`/users/${user.userid}/balance/transactions`,
|
|
||||||
{ params: filter }
|
|
||||||
);
|
|
||||||
data.transactions.forEach((t) => fixTransaction(t));
|
|
||||||
if (data.transactions) this.transactions.push(...data.transactions);
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async revert(transaction: FG.Transaction) {
|
|
||||||
try {
|
|
||||||
const { data } = await api.delete<FG.Transaction>(`/balance/${transaction.id}`);
|
|
||||||
fixTransaction(data);
|
|
||||||
const f = this.transactions.find((x) => x.id === transaction.id);
|
|
||||||
if (f) f.reversal_id = data.id;
|
|
||||||
this.transactions.push(data);
|
|
||||||
console.log(data);
|
|
||||||
} catch (error) {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
this._balances_dirty = 0;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,62 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-carousel
|
|
||||||
v-model="volume"
|
|
||||||
transition-prev="slide-right"
|
|
||||||
transition-next="slide-left"
|
|
||||||
animated
|
|
||||||
swipeable
|
|
||||||
control-color="primary"
|
|
||||||
arrows
|
|
||||||
>
|
|
||||||
<q-carousel-slide v-for="volume in volumes" :key="volume.id" :name="volume.id">
|
|
||||||
<build-manual-volume-part :volume="volume" />
|
|
||||||
</q-carousel-slide>
|
|
||||||
</q-carousel>
|
|
||||||
<div class="full-width row justify-center q-pa-sm">
|
|
||||||
<div class="q-px-sm">
|
|
||||||
<q-btn-toggle v-model="volume" :options="btn_options" rounded />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, PropType, ref, computed } from 'vue';
|
|
||||||
import { DrinkPriceVolume } from '../../store';
|
|
||||||
import BuildManualVolumePart from './BuildManualVolumePart.vue';
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'BuildManualVolume',
|
|
||||||
components: { BuildManualVolumePart },
|
|
||||||
props: {
|
|
||||||
volumes: {
|
|
||||||
type: Array as PropType<Array<DrinkPriceVolume>>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const _volume = ref<number>();
|
|
||||||
const volume = computed({
|
|
||||||
get: () => {
|
|
||||||
if (_volume.value !== undefined) {
|
|
||||||
return _volume.value;
|
|
||||||
}
|
|
||||||
return props.volumes[0].id;
|
|
||||||
},
|
|
||||||
set: (val: number) => (_volume.value = val),
|
|
||||||
});
|
|
||||||
const options = computed(() => {
|
|
||||||
let ret: Array<{ label: number; value: number }> = [];
|
|
||||||
props.volumes.forEach((volume: DrinkPriceVolume) => {
|
|
||||||
ret.push({ label: volume.id, value: volume.id });
|
|
||||||
});
|
|
||||||
return ret;
|
|
||||||
});
|
|
||||||
const btn_options = computed<Array<{ label: string; value: number }>>(() => {
|
|
||||||
const retVal: Array<{ label: string; value: number }> = [];
|
|
||||||
props.volumes.forEach((volume: DrinkPriceVolume) => {
|
|
||||||
retVal.push({ label: `${(<number>volume.volume).toFixed(3)}L`, value: volume.id });
|
|
||||||
});
|
|
||||||
return retVal;
|
|
||||||
});
|
|
||||||
return { volume, options, btn_options };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,65 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">Zutaten</div>
|
|
||||||
<div v-for="ingredient in volume.ingredients" :key="ingredient.id">
|
|
||||||
<div v-if="ingredient.drink_ingredient">
|
|
||||||
<div class="full-width row q-gutter-sm q-py-sm">
|
|
||||||
<div class="col">
|
|
||||||
{{ name(ingredient.drink_ingredient?.ingredient_id) }}
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
{{
|
|
||||||
ingredient.drink_ingredient?.volume
|
|
||||||
? `${ingredient.drink_ingredient?.volume * 100} cl`
|
|
||||||
: ''
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-separator />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-for="ingredient in volume.ingredients" :key="ingredient.id">
|
|
||||||
<div v-if="ingredient.extra_ingredient">
|
|
||||||
<div class="full-width row q-gutter-sm q-py-sm">
|
|
||||||
<div class="col">
|
|
||||||
{{ ingredient.extra_ingredient?.name }}
|
|
||||||
</div>
|
|
||||||
<div class="col"></div>
|
|
||||||
</div>
|
|
||||||
<q-separator />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">Preise</div>
|
|
||||||
<div class="full-width row q-gutter-sm justify-around">
|
|
||||||
<div v-for="price in volume.prices" :key="price.id">
|
|
||||||
<div class="text-body1">{{ price.price.toFixed(2) }}€</div>
|
|
||||||
<q-badge v-if="price.public" class="text-caption"> öffentlich </q-badge>
|
|
||||||
<div class="text-caption text-weight-thin">
|
|
||||||
{{ price.description }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</template>
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, PropType } from 'vue';
|
|
||||||
import { DrinkPriceVolume, usePricelistStore } from '../../store';
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'BuildManualVolumePart',
|
|
||||||
props: {
|
|
||||||
volume: {
|
|
||||||
type: Object as PropType<DrinkPriceVolume>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const store = usePricelistStore();
|
|
||||||
function name(id: number) {
|
|
||||||
return store.drinks.find((a) => a.id === id)?.name;
|
|
||||||
}
|
|
||||||
return { name };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,512 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-table
|
|
||||||
v-model:pagination="pagination"
|
|
||||||
title="Preistabelle"
|
|
||||||
:columns="columns"
|
|
||||||
:rows="drinks"
|
|
||||||
dense
|
|
||||||
:filter="search"
|
|
||||||
:filter-method="filter"
|
|
||||||
grid
|
|
||||||
:rows-per-page-options="[0]"
|
|
||||||
>
|
|
||||||
<template #top-right>
|
|
||||||
<div class="row justify-end q-gutter-sm">
|
|
||||||
<search-input v-model="search" :keys="search_keys" />
|
|
||||||
<slot></slot>
|
|
||||||
<q-btn v-if="!public && !nodetails" label="Aufpreise">
|
|
||||||
<q-menu anchor="center middle" self="center middle">
|
|
||||||
<min-price-setting />
|
|
||||||
</q-menu>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
v-if="!public && !nodetails && editable && hasPermission(PERMISSIONS.CREATE)"
|
|
||||||
color="primary"
|
|
||||||
round
|
|
||||||
icon="mdi-plus"
|
|
||||||
@click="newDrink"
|
|
||||||
>
|
|
||||||
<q-tooltip> Neues Getränk </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #item="props">
|
|
||||||
<div class="q-pa-xs col-xs-12 col-sm-6 col-md-4">
|
|
||||||
<q-card>
|
|
||||||
<q-img style="max-height: 256px" :src="image(props.row.uuid)">
|
|
||||||
<div
|
|
||||||
v-if="!public && !nodetails && editable"
|
|
||||||
class="absolute-top-right justify-end"
|
|
||||||
style="background-color: transparent"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
icon="mdi-pencil"
|
|
||||||
style="background-color: rgba(0, 0, 0, 0.5)"
|
|
||||||
@click="editDrink = props.row"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="absolute-bottom-right justify-end">
|
|
||||||
<div class="text-subtitle1 text-right">
|
|
||||||
{{ props.row.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-caption text-right">
|
|
||||||
{{ props.row.type.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-img>
|
|
||||||
<q-card-section>
|
|
||||||
<q-badge
|
|
||||||
v-for="tag in props.row.tags"
|
|
||||||
:key="`${props.row.id}-${tag.id}`"
|
|
||||||
class="text-caption"
|
|
||||||
rounded
|
|
||||||
:style="`background-color: ${tag.color}`"
|
|
||||||
>
|
|
||||||
{{ tag.name }}
|
|
||||||
</q-badge>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section v-if="!public && !nodetails">
|
|
||||||
<div class="fit row">
|
|
||||||
<q-input
|
|
||||||
v-if="props.row.article_id"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
:model-value="props.row.article_id"
|
|
||||||
outlined
|
|
||||||
readonly
|
|
||||||
label="Artikelnummer"
|
|
||||||
dense
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-if="props.row.volume"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
:model-value="props.row.volume"
|
|
||||||
outlined
|
|
||||||
readonly
|
|
||||||
label="Inhalt"
|
|
||||||
dense
|
|
||||||
suffix="L"
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-if="props.row.package_size"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
:model-value="props.row.package_size"
|
|
||||||
outlined
|
|
||||||
readonly
|
|
||||||
label="Gebindegröße"
|
|
||||||
dense
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-if="props.row.cost_per_package"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
:model-value="props.row.cost_per_package"
|
|
||||||
outlined
|
|
||||||
readonly
|
|
||||||
label="Preis Gebinde"
|
|
||||||
suffix="€"
|
|
||||||
dense
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-if="props.row.cost_per_volume"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm q-pb-lg"
|
|
||||||
:model-value="props.row.cost_per_volume"
|
|
||||||
outlined
|
|
||||||
readonly
|
|
||||||
label="Preis pro L"
|
|
||||||
hint="Inkl. 19% Mehrwertsteuer"
|
|
||||||
suffix="€"
|
|
||||||
dense
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section v-if="props.row.volumes.length > 0 && notLoading">
|
|
||||||
<drink-price-volumes
|
|
||||||
:model-value="props.row.volumes"
|
|
||||||
:public="public"
|
|
||||||
:nodetails="nodetails"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
<q-dialog :model-value="editDrink !== undefined" persistent>
|
|
||||||
<drink-modify
|
|
||||||
:drink="editDrink"
|
|
||||||
@save="editing_drink"
|
|
||||||
@cancel="editDrink = undefined"
|
|
||||||
@delete="deleteDrink"
|
|
||||||
/>
|
|
||||||
</q-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, onBeforeMount, ComputedRef, computed, ref } from 'vue';
|
|
||||||
import { Drink, usePricelistStore, DrinkPriceVolume } from 'src/plugins/pricelist/store';
|
|
||||||
import MinPriceSetting from 'src/plugins/pricelist/components/MinPriceSetting.vue';
|
|
||||||
import SearchInput from './SearchInput.vue';
|
|
||||||
import DrinkPriceVolumes from 'src/plugins/pricelist/components/CalculationTable/DrinkPriceVolumes.vue';
|
|
||||||
import DrinkModify from './DrinkModify.vue';
|
|
||||||
import { filter, Search } from '../utils/filter';
|
|
||||||
import { Notify } from 'quasar';
|
|
||||||
import { sort } from '../utils/sort';
|
|
||||||
import { DeleteObjects } from 'src/plugins/pricelist/utils/utils';
|
|
||||||
import { hasPermission } from 'src/utils/permission';
|
|
||||||
import { PERMISSIONS } from 'src/plugins/pricelist/permissions';
|
|
||||||
import { baseURL } from 'src/config';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'CalculationTable',
|
|
||||||
components: {
|
|
||||||
SearchInput,
|
|
||||||
MinPriceSetting,
|
|
||||||
DrinkPriceVolumes,
|
|
||||||
DrinkModify,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
public: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
editable: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
nodetails: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const store = usePricelistStore();
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
void store.getDrinks();
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'picture',
|
|
||||||
label: 'Bild',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
label: 'Name',
|
|
||||||
field: 'name',
|
|
||||||
sortable: true,
|
|
||||||
sort,
|
|
||||||
filterable: true,
|
|
||||||
public: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'drink_type',
|
|
||||||
label: 'Kategorie',
|
|
||||||
field: 'type',
|
|
||||||
format: (val: FG.DrinkType) => `${val.name}`,
|
|
||||||
sortable: true,
|
|
||||||
sort: (a: FG.DrinkType, b: FG.DrinkType) => sort(a.name, b.name),
|
|
||||||
filterable: true,
|
|
||||||
public: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tags',
|
|
||||||
label: 'Tags',
|
|
||||||
field: 'tags',
|
|
||||||
format: (val: Array<FG.Tag>) => {
|
|
||||||
let retVal = '';
|
|
||||||
val.forEach((tag, index) => {
|
|
||||||
if (index > 0) {
|
|
||||||
retVal += ', ';
|
|
||||||
}
|
|
||||||
retVal += tag.name;
|
|
||||||
});
|
|
||||||
return retVal;
|
|
||||||
},
|
|
||||||
filterable: true,
|
|
||||||
public: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'article_id',
|
|
||||||
label: 'Artikelnummer',
|
|
||||||
field: 'article_id',
|
|
||||||
sortable: true,
|
|
||||||
sort,
|
|
||||||
filterable: true,
|
|
||||||
public: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'volume_package',
|
|
||||||
label: 'Inhalt in l des Gebinde',
|
|
||||||
field: 'volume',
|
|
||||||
sortable: true,
|
|
||||||
sort,
|
|
||||||
public: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'package_size',
|
|
||||||
label: 'Gebindegröße',
|
|
||||||
field: 'package_size',
|
|
||||||
sortable: true,
|
|
||||||
sort,
|
|
||||||
public: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'cost_per_package',
|
|
||||||
label: 'Preis Netto/Gebinde',
|
|
||||||
field: 'cost_per_package',
|
|
||||||
format: (val: number | null) => (val ? `${val.toFixed(3)}€` : ''),
|
|
||||||
sortable: true,
|
|
||||||
sort,
|
|
||||||
public: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'cost_per_volume',
|
|
||||||
label: 'Preis mit 19%/Liter',
|
|
||||||
field: 'cost_per_volume',
|
|
||||||
format: (val: number | null) => (val ? `${val.toFixed(3)}€` : ''),
|
|
||||||
sortable: true,
|
|
||||||
sort: (a: ComputedRef, b: ComputedRef) => sort(a.value, b.value),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'volumes',
|
|
||||||
label: 'Preiskalkulation',
|
|
||||||
field: 'volumes',
|
|
||||||
format: (val: Array<DrinkPriceVolume>) => {
|
|
||||||
let retVal = '';
|
|
||||||
val.forEach((val, index) => {
|
|
||||||
if (index > 0) {
|
|
||||||
retVal += ', ';
|
|
||||||
}
|
|
||||||
retVal += val.id;
|
|
||||||
});
|
|
||||||
return retVal;
|
|
||||||
},
|
|
||||||
sortable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'receipt',
|
|
||||||
label: 'Bauanleitung',
|
|
||||||
field: 'receipt',
|
|
||||||
format: (val: Array<string>) => {
|
|
||||||
let retVal = '';
|
|
||||||
val.forEach((value, index) => {
|
|
||||||
if (index > 0) {
|
|
||||||
retVal += ', ';
|
|
||||||
}
|
|
||||||
retVal += value;
|
|
||||||
});
|
|
||||||
return retVal;
|
|
||||||
},
|
|
||||||
filterable: true,
|
|
||||||
sortable: false,
|
|
||||||
public: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const column_calc = [
|
|
||||||
{
|
|
||||||
name: 'volume',
|
|
||||||
label: 'Abgabe in l',
|
|
||||||
field: 'volume',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'min_prices',
|
|
||||||
label: 'Minimal Preise',
|
|
||||||
field: 'min_prices',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'prices',
|
|
||||||
label: 'Preise',
|
|
||||||
field: 'prices',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const column_prices = [
|
|
||||||
{
|
|
||||||
name: 'price',
|
|
||||||
label: 'Preis',
|
|
||||||
field: 'price',
|
|
||||||
format: (val: number) => `${val.toFixed(2)}€`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
label: 'Beschreibung',
|
|
||||||
field: 'description',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'public',
|
|
||||||
label: 'Öffentlich',
|
|
||||||
field: 'public',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const search_keys = computed(() =>
|
|
||||||
columns.filter(
|
|
||||||
(column) => column.filterable && (props.public || props.nodetails ? column.public : true)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const pagination = ref({
|
|
||||||
sortBy: 'name',
|
|
||||||
descending: false,
|
|
||||||
rowsPerPage: store.drinks.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
const drinkTypes = computed(() => store.drinkTypes);
|
|
||||||
|
|
||||||
function updateDrink(drink: Drink) {
|
|
||||||
void store.updateDrink(drink);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteDrink() {
|
|
||||||
if (editDrink.value) {
|
|
||||||
store.deleteDrink(editDrink.value);
|
|
||||||
}
|
|
||||||
editDrink.value = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const showNewDrink = ref(false);
|
|
||||||
|
|
||||||
const drinkPic = ref<File>();
|
|
||||||
|
|
||||||
function onPictureRejected() {
|
|
||||||
Notify.create({
|
|
||||||
group: false,
|
|
||||||
type: 'negative',
|
|
||||||
message: 'Datei zu groß oder keine gültige Bilddatei.',
|
|
||||||
timeout: 10000,
|
|
||||||
progress: true,
|
|
||||||
actions: [{ icon: 'mdi-close', color: 'white' }],
|
|
||||||
});
|
|
||||||
drinkPic.value = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function savePicture(drinkPic: File) {
|
|
||||||
if (editDrink.value) {
|
|
||||||
await store.upload_drink_picture(editDrink.value, drinkPic).catch((response: Response) => {
|
|
||||||
if (response && response.status == 400) {
|
|
||||||
onPictureRejected();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deletePicture() {
|
|
||||||
if (editDrink.value) {
|
|
||||||
await store.delete_drink_picture(editDrink.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = ref<Search>({
|
|
||||||
value: '',
|
|
||||||
key: '',
|
|
||||||
label: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const emptyDrink: Drink = {
|
|
||||||
id: -1,
|
|
||||||
article_id: undefined,
|
|
||||||
package_size: undefined,
|
|
||||||
name: '',
|
|
||||||
volume: undefined,
|
|
||||||
cost_per_volume: undefined,
|
|
||||||
cost_per_package: undefined,
|
|
||||||
tags: [],
|
|
||||||
type: undefined,
|
|
||||||
volumes: [],
|
|
||||||
uuid: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
function newDrink() {
|
|
||||||
editDrink.value = Object.assign({}, emptyDrink);
|
|
||||||
}
|
|
||||||
|
|
||||||
const editDrink = ref<Drink>();
|
|
||||||
|
|
||||||
async function editing_drink(
|
|
||||||
drink: Drink,
|
|
||||||
toDeleteObjects: DeleteObjects,
|
|
||||||
drinkPic: File | undefined,
|
|
||||||
deletePic: boolean
|
|
||||||
) {
|
|
||||||
notLoading.value = false;
|
|
||||||
for (const ingredient of toDeleteObjects.ingredients) {
|
|
||||||
await store.deleteIngredient(ingredient);
|
|
||||||
}
|
|
||||||
for (const price of toDeleteObjects.prices) {
|
|
||||||
await store.deletePrice(price);
|
|
||||||
}
|
|
||||||
for (const volume of toDeleteObjects.volumes) {
|
|
||||||
await store.deleteVolume(volume, drink);
|
|
||||||
}
|
|
||||||
if (drink.id > 0) {
|
|
||||||
await store.updateDrink(drink);
|
|
||||||
} else {
|
|
||||||
const _drink = await store.setDrink(drink);
|
|
||||||
if (editDrink.value) {
|
|
||||||
editDrink.value.id = _drink.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (deletePic) {
|
|
||||||
await deletePicture();
|
|
||||||
}
|
|
||||||
if (drinkPic instanceof File) {
|
|
||||||
await savePicture(drinkPic);
|
|
||||||
}
|
|
||||||
editDrink.value = undefined;
|
|
||||||
notLoading.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_volumes(drink_id: number) {
|
|
||||||
return store.drinks.find((a) => a.id === drink_id)?.volumes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const notLoading = ref(true);
|
|
||||||
|
|
||||||
const imageloading = ref<Array<{ id: number; loading: boolean }>>([]);
|
|
||||||
function getImageLoading(id: number) {
|
|
||||||
const loading = imageloading.value.find((a) => a.id === id);
|
|
||||||
if (loading) {
|
|
||||||
return loading.loading;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function image(uuid: string | undefined) {
|
|
||||||
if (uuid) {
|
|
||||||
return `${baseURL.value}/pricelist/picture/${uuid}?size=256`;
|
|
||||||
}
|
|
||||||
return 'no-image.svg';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
drinks: computed(() => store.drinks),
|
|
||||||
pagination,
|
|
||||||
columns,
|
|
||||||
column_calc,
|
|
||||||
column_prices,
|
|
||||||
drinkTypes,
|
|
||||||
updateDrink,
|
|
||||||
deleteDrink,
|
|
||||||
showNewDrink,
|
|
||||||
drinkPic,
|
|
||||||
savePicture,
|
|
||||||
deletePicture,
|
|
||||||
search,
|
|
||||||
filter,
|
|
||||||
search_keys,
|
|
||||||
tags: computed(() => store.tags),
|
|
||||||
editDrink,
|
|
||||||
editing_drink,
|
|
||||||
get_volumes,
|
|
||||||
notLoading,
|
|
||||||
getImageLoading,
|
|
||||||
newDrink,
|
|
||||||
hasPermission,
|
|
||||||
PERMISSIONS,
|
|
||||||
image,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,60 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
v-for="(step, index) in steps"
|
|
||||||
:key="index"
|
|
||||||
class="full-width row q-gutter-sm justify-between q-py-sm"
|
|
||||||
>
|
|
||||||
<div class="row">
|
|
||||||
<div>{{ index + 1 }}.</div>
|
|
||||||
<div class="q-pl-sm">
|
|
||||||
{{ step }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-btn
|
|
||||||
v-if="editable"
|
|
||||||
round
|
|
||||||
color="negative"
|
|
||||||
size="sm"
|
|
||||||
icon="mdi-delete"
|
|
||||||
@click="deleteStep(index)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="editable" class="full-width row q-gutter-sm justify-between">
|
|
||||||
<q-input v-model="newStep" filled label="Arbeitsschritt" dense />
|
|
||||||
<q-btn label="Schritt hinzufügen" dense @click="addStep" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { PropType, defineComponent, ref } from 'vue';
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'BuildManual',
|
|
||||||
props: {
|
|
||||||
steps: {
|
|
||||||
type: Array as PropType<Array<string>>,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
editable: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: {
|
|
||||||
deleteStep: (index: number) => index,
|
|
||||||
addStep: (val: string) => val,
|
|
||||||
},
|
|
||||||
setup(_, { emit }) {
|
|
||||||
const newStep = ref('');
|
|
||||||
function deleteStep(index: number) {
|
|
||||||
emit('deleteStep', index);
|
|
||||||
}
|
|
||||||
function addStep() {
|
|
||||||
emit('addStep', newStep.value);
|
|
||||||
newStep.value = '';
|
|
||||||
}
|
|
||||||
return { newStep, addStep, deleteStep };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,340 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-carousel
|
|
||||||
v-model="volume"
|
|
||||||
transition-prev="slide-right"
|
|
||||||
transition-next="slide-left"
|
|
||||||
animated
|
|
||||||
swipeable
|
|
||||||
control-color="primary"
|
|
||||||
arrows
|
|
||||||
:keep-alive="false"
|
|
||||||
>
|
|
||||||
<q-carousel-slide v-for="volume in volumes" :key="volume.id" :name="volume.id">
|
|
||||||
<div class="full-width row">
|
|
||||||
<q-input
|
|
||||||
v-model.number="volume._volume"
|
|
||||||
class="q-pa-sm col-10"
|
|
||||||
:outlined="!editable || !volume_can_edit"
|
|
||||||
:filled="editable && volume_can_edit"
|
|
||||||
:readonly="!editable || !volume_can_edit"
|
|
||||||
dense
|
|
||||||
label="Inhalt"
|
|
||||||
mask="#.###"
|
|
||||||
fill-mask="0"
|
|
||||||
suffix="L"
|
|
||||||
min="0"
|
|
||||||
step="0.001"
|
|
||||||
@update:model-value="updateVolume(volume)"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="deleteable && editable && hasPermission(PERMISSIONS.DELETE_VOLUME)"
|
|
||||||
class="q-pa-sm col-2 text-right"
|
|
||||||
>
|
|
||||||
<q-btn round icon="mdi-delete" size="sm" color="negative" @click="deleteVolume">
|
|
||||||
<q-tooltip> Abgabe entfernen </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="!public && !nodetails" class="full-width row q-gutter-sm q-pa-sm justify-around">
|
|
||||||
<div v-for="(min_price, index) in volume.min_prices" :key="index">
|
|
||||||
<q-badge class="text-body1" color="primary"> {{ min_price.percentage }}% </q-badge>
|
|
||||||
<div class="text-body1">{{ min_price.price.toFixed(3) }}€</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="q-pa-sm">
|
|
||||||
<div v-for="(price, index) in volume.prices" :key="price.id">
|
|
||||||
<div class="fit row justify-around q-py-sm">
|
|
||||||
<div
|
|
||||||
v-if="!editable || !hasPermission(PERMISSIONS.EDIT_PRICE)"
|
|
||||||
class="text-body1 col-3"
|
|
||||||
>
|
|
||||||
{{ price.price.toFixed(2) }}€
|
|
||||||
</div>
|
|
||||||
<q-input
|
|
||||||
v-else
|
|
||||||
v-model.number="price.price"
|
|
||||||
class="col-3"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="0.01"
|
|
||||||
suffix="€"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
label="Preis"
|
|
||||||
@update:model-value="change"
|
|
||||||
/>
|
|
||||||
<div class="text-body1 col-2">
|
|
||||||
<q-toggle
|
|
||||||
v-model="price.public"
|
|
||||||
:disable="!editable || !hasPermission(PERMISSIONS.EDIT_PRICE)"
|
|
||||||
checked-icon="mdi-earth"
|
|
||||||
unchecked-icon="mdi-earth-off"
|
|
||||||
@update:model-value="change"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="!editable || !hasPermission(PERMISSIONS.EDIT_PRICE)"
|
|
||||||
class="text-body1 col-5"
|
|
||||||
>
|
|
||||||
{{ price.description }}
|
|
||||||
</div>
|
|
||||||
<q-input
|
|
||||||
v-else
|
|
||||||
v-model="price.description"
|
|
||||||
class="col-5"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
label="Beschreibung"
|
|
||||||
@update:model-value="change"
|
|
||||||
/>
|
|
||||||
<div v-if="editable && hasPermission(PERMISSIONS.DELETE_PRICE)" class="col-1">
|
|
||||||
<q-btn round icon="mdi-delete" color="negative" size="xs" @click="deletePrice(price)">
|
|
||||||
<q-tooltip> Preis entfernen </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-separator v-if="index < volume.prices.length - 1" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="!public && !nodetails && isUnderMinPrice"
|
|
||||||
class="fit warning bg-red text-center text-white text-body1"
|
|
||||||
>
|
|
||||||
Einer der Preise ist unterhalb des niedrigsten minimal Preises.
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="editable && hasPermission(PERMISSIONS.EDIT_PRICE)"
|
|
||||||
class="full-width row justify-end text-right"
|
|
||||||
>
|
|
||||||
<q-btn round icon="mdi-plus" size="sm" color="primary">
|
|
||||||
<q-tooltip> Preis hinzufügen </q-tooltip>
|
|
||||||
<q-menu anchor="center middle" self="center middle">
|
|
||||||
<new-price @save="addPrice" />
|
|
||||||
</q-menu>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="q-pa-sm">
|
|
||||||
<ingredients
|
|
||||||
v-if="!public && !costPerVolume"
|
|
||||||
v-model="volume.ingredients"
|
|
||||||
:editable="editable && hasPermission(PERMISSIONS.EDIT_INGREDIENTS_DRINK)"
|
|
||||||
@update="updateVolume(volume)"
|
|
||||||
@delete-ingredient="deleteIngredient"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-carousel-slide>
|
|
||||||
</q-carousel>
|
|
||||||
<div class="full-width row justify-center q-pa-sm">
|
|
||||||
<div class="q-px-sm">
|
|
||||||
<q-btn-toggle v-model="volume" :options="options" rounded />
|
|
||||||
</div>
|
|
||||||
<div v-if="editable" class="q-px-sm">
|
|
||||||
<q-btn class="q-px-sm" round icon="mdi-plus" color="primary" size="sm" @click="newVolume">
|
|
||||||
<q-tooltip> Abgabe hinzufügen </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, PropType, ref, onBeforeMount } from 'vue';
|
|
||||||
import { DrinkPriceVolume } from 'src/plugins/pricelist/store';
|
|
||||||
import Ingredients from 'src/plugins/pricelist/components/CalculationTable/Ingredients.vue';
|
|
||||||
import NewPrice from 'src/plugins/pricelist/components/CalculationTable/NewPrice.vue';
|
|
||||||
import { calc_volume, clone } from '../../utils/utils';
|
|
||||||
import { hasPermission } from 'src/utils/permission';
|
|
||||||
import { PERMISSIONS } from '../../permissions';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'DrinkPriceVolume',
|
|
||||||
components: { Ingredients, NewPrice },
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: Array as PropType<Array<DrinkPriceVolume>>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
costPerVolume: {
|
|
||||||
type: undefined,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
editable: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
public: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
nodetails: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: {
|
|
||||||
'update:modelValue': (val: Array<DrinkPriceVolume>) => val,
|
|
||||||
update: (val: number) => val,
|
|
||||||
'delete-volume': (val: DrinkPriceVolume) => val,
|
|
||||||
'delete-price': (val: FG.DrinkPrice) => val,
|
|
||||||
'delete-ingredient': (val: FG.Ingredient) => val,
|
|
||||||
},
|
|
||||||
setup(props, { emit }) {
|
|
||||||
onBeforeMount(() => {
|
|
||||||
//volumes.value = <Array<DrinkPriceVolume>>JSON.parse(JSON.stringify(props.modelValue));
|
|
||||||
volumes.value = clone(props.modelValue);
|
|
||||||
});
|
|
||||||
const volumes = ref<Array<DrinkPriceVolume>>([]);
|
|
||||||
const _volume = ref<number | undefined>();
|
|
||||||
const volume = computed<number | undefined>({
|
|
||||||
get: () => {
|
|
||||||
if (_volume.value !== undefined) {
|
|
||||||
return _volume.value;
|
|
||||||
}
|
|
||||||
if (volumes.value.length > 0) {
|
|
||||||
return volumes.value[0].id;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
set: (val: number | undefined) => (_volume.value = val),
|
|
||||||
});
|
|
||||||
const edit_volume = computed(() => {
|
|
||||||
return volumes.value.find((a) => a.id === volume.value);
|
|
||||||
});
|
|
||||||
const options = computed<Array<{ label: string; value: number }>>(() => {
|
|
||||||
const retVal: Array<{ label: string; value: number }> = [];
|
|
||||||
volumes.value.forEach((volume: DrinkPriceVolume) => {
|
|
||||||
retVal.push({ label: `${(<number>volume.volume).toFixed(3)}L`, value: volume.id });
|
|
||||||
});
|
|
||||||
return retVal;
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateVolume(_volume: DrinkPriceVolume) {
|
|
||||||
const index = volumes.value.findIndex((a) => a.id === _volume.id);
|
|
||||||
if (index > -1) {
|
|
||||||
volumes.value[index].volume = calc_volume(_volume);
|
|
||||||
volumes.value[index]._volume = <number>volumes.value[index].volume;
|
|
||||||
}
|
|
||||||
change();
|
|
||||||
setTimeout(() => {
|
|
||||||
emit('update', index);
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
const volume_can_edit = computed(() => {
|
|
||||||
if (edit_volume.value) {
|
|
||||||
return !edit_volume.value.ingredients.some((ingredient) => ingredient.drink_ingredient);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const newVolumeId = ref(-1);
|
|
||||||
|
|
||||||
function newVolume() {
|
|
||||||
const new_volume: DrinkPriceVolume = {
|
|
||||||
id: newVolumeId.value,
|
|
||||||
_volume: 0,
|
|
||||||
volume: 0,
|
|
||||||
prices: [],
|
|
||||||
ingredients: [],
|
|
||||||
min_prices: [],
|
|
||||||
};
|
|
||||||
newVolumeId.value--;
|
|
||||||
volumes.value.push(new_volume);
|
|
||||||
change();
|
|
||||||
_volume.value = volumes.value[volumes.value.length - 1].id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteVolume() {
|
|
||||||
if (edit_volume.value) {
|
|
||||||
if (edit_volume.value.id > 0) {
|
|
||||||
emit('delete-volume', edit_volume.value);
|
|
||||||
}
|
|
||||||
const index = volumes.value.findIndex((a) => a.id === edit_volume.value?.id);
|
|
||||||
if (index > -1) {
|
|
||||||
_volume.value = volumes.value[0].id;
|
|
||||||
volumes.value.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteable = computed(() => {
|
|
||||||
if (edit_volume.value) {
|
|
||||||
const has_ingredients = edit_volume.value.ingredients.length > 0;
|
|
||||||
const has_prices = edit_volume.value.prices.length > 0;
|
|
||||||
|
|
||||||
return !(has_ingredients || has_prices);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
function addPrice(price: FG.DrinkPrice) {
|
|
||||||
if (edit_volume.value) {
|
|
||||||
edit_volume.value.prices.push(price);
|
|
||||||
change();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function deletePrice(price: FG.DrinkPrice) {
|
|
||||||
if (edit_volume.value) {
|
|
||||||
const index = edit_volume.value.prices.findIndex((a) => a.id === price.id);
|
|
||||||
if (index > -1) {
|
|
||||||
if (edit_volume.value.prices[index].id > 0) {
|
|
||||||
emit('delete-price', edit_volume.value.prices[index]);
|
|
||||||
change();
|
|
||||||
}
|
|
||||||
edit_volume.value.prices.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteIngredient(ingredient: FG.Ingredient) {
|
|
||||||
emit('delete-ingredient', ingredient);
|
|
||||||
}
|
|
||||||
|
|
||||||
function change() {
|
|
||||||
emit('update:modelValue', volumes.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUnderMinPrice = computed(() => {
|
|
||||||
if (volumes.value) {
|
|
||||||
const this_volume = volumes.value.find((a) => a.id === volume.value);
|
|
||||||
if (this_volume) {
|
|
||||||
if (this_volume.min_prices.length > 0) {
|
|
||||||
const min_price = this_volume.min_prices.sort((a, b) => {
|
|
||||||
if (a.price > b.price) return 1;
|
|
||||||
if (a.price < b.price) return -1;
|
|
||||||
return 0;
|
|
||||||
})[0];
|
|
||||||
console.log('min_price', min_price);
|
|
||||||
return this_volume.prices.some((a) => a.price < min_price.price);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
volumes,
|
|
||||||
volume,
|
|
||||||
options,
|
|
||||||
updateVolume,
|
|
||||||
volume_can_edit,
|
|
||||||
newVolume,
|
|
||||||
deleteable,
|
|
||||||
addPrice,
|
|
||||||
deletePrice,
|
|
||||||
deleteVolume,
|
|
||||||
deleteIngredient,
|
|
||||||
change,
|
|
||||||
isUnderMinPrice,
|
|
||||||
hasPermission,
|
|
||||||
PERMISSIONS,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.warning {
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,271 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="full-width">
|
|
||||||
<div
|
|
||||||
v-for="ingredient in edit_ingredients"
|
|
||||||
:key="`ingredient:${ingredient.id}`"
|
|
||||||
class="full-width row justify-evenly q-py-xs"
|
|
||||||
>
|
|
||||||
<div class="full-width row justify-evenly">
|
|
||||||
<div v-if="ingredient.drink_ingredient" class="col">
|
|
||||||
<div class="full-width row justify-evenly q-py-xs">
|
|
||||||
<div class="col">
|
|
||||||
{{ get_drink_ingredient_name(ingredient.drink_ingredient.ingredient_id) }}
|
|
||||||
<q-popup-edit
|
|
||||||
v-if="editable"
|
|
||||||
v-slot="scope"
|
|
||||||
v-model="ingredient.drink_ingredient.ingredient_id"
|
|
||||||
buttons
|
|
||||||
label-cancel="Abbrechen"
|
|
||||||
label-set="Speichern"
|
|
||||||
@save="updateValue"
|
|
||||||
>
|
|
||||||
<q-select
|
|
||||||
v-model="scope.ingredient_id"
|
|
||||||
class="col q-px-sm"
|
|
||||||
label="Getränk"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
:options="drinks"
|
|
||||||
option-label="name"
|
|
||||||
option-value="id"
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
/>
|
|
||||||
</q-popup-edit>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
{{ ingredient.drink_ingredient.volume.toFixed(3) }}L
|
|
||||||
<q-popup-edit
|
|
||||||
v-if="editable"
|
|
||||||
v-slot="scope"
|
|
||||||
v-model="ingredient.drink_ingredient.volume"
|
|
||||||
buttons
|
|
||||||
label-cancel="Abbrechen"
|
|
||||||
label-set="Speichern"
|
|
||||||
@save="updateValue"
|
|
||||||
>
|
|
||||||
<q-input
|
|
||||||
v-model.number="scope.value"
|
|
||||||
class="col q-px-sm"
|
|
||||||
label="Volume"
|
|
||||||
type="number"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
suffix="L"
|
|
||||||
step="0.01"
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
</q-popup-edit>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="ingredient.extra_ingredient" class="col">
|
|
||||||
<div class="full-width row justify-evenly q-py-xs">
|
|
||||||
<div class="col">
|
|
||||||
{{ ingredient.extra_ingredient.name }}
|
|
||||||
</div>
|
|
||||||
<div class="col">{{ ingredient.extra_ingredient.price.toFixed(3) }}€</div>
|
|
||||||
</div>
|
|
||||||
<q-popup-edit
|
|
||||||
v-if="editable"
|
|
||||||
v-model="ingredient.extra_ingredient"
|
|
||||||
buttons
|
|
||||||
label-cancel="Abbrechen"
|
|
||||||
label-set="Speichern"
|
|
||||||
@save="updateValue"
|
|
||||||
>
|
|
||||||
<q-select
|
|
||||||
v-model="ingredient.extra_ingredient"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
:options="extra_ingredients"
|
|
||||||
option-label="name"
|
|
||||||
/>
|
|
||||||
</q-popup-edit>
|
|
||||||
</div>
|
|
||||||
<div v-if="editable" class="col-1 row justify-end q-pa-xs">
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-delete"
|
|
||||||
round
|
|
||||||
size="xs"
|
|
||||||
color="negative"
|
|
||||||
@click="deleteIngredient(ingredient)"
|
|
||||||
>
|
|
||||||
<q-tooltip> Zutat entfernen </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-separator />
|
|
||||||
</div>
|
|
||||||
<div v-if="editable" class="full-width row justify-end q-py-xs">
|
|
||||||
<q-btn size="sm" round icon="mdi-plus" color="primary">
|
|
||||||
<q-tooltip> Neue Zutat hinzufügen </q-tooltip>
|
|
||||||
<q-menu anchor="center middle" self="center middle" persistent>
|
|
||||||
<div class="full-width row justify-around q-gutter-sm q-pa-sm">
|
|
||||||
<div class="col">
|
|
||||||
<q-select
|
|
||||||
v-model="newIngredient"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
label="Zutat"
|
|
||||||
:options="[...drinks, ...extra_ingredients]"
|
|
||||||
option-label="name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<q-slide-transition>
|
|
||||||
<q-input
|
|
||||||
v-if="newIngredient && newIngredient.volume"
|
|
||||||
v-model.number="newIngredientVolume"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
label="Volume"
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
min="0"
|
|
||||||
suffix="L"
|
|
||||||
/>
|
|
||||||
</q-slide-transition>
|
|
||||||
<q-slide-transition>
|
|
||||||
<q-input
|
|
||||||
v-if="newIngredient && newIngredient.price"
|
|
||||||
v-model="newIngredient.price"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
label="Preis"
|
|
||||||
disable
|
|
||||||
min="0"
|
|
||||||
step="0.1"
|
|
||||||
fill-mask="0"
|
|
||||||
mask="#.##"
|
|
||||||
suffix="€"
|
|
||||||
/>
|
|
||||||
</q-slide-transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="full-width row justify-around q-gutter-sm q-pa-sm">
|
|
||||||
<q-btn v-close-popup flat label="Abbrechen" @click="cancelAddIngredient" />
|
|
||||||
<q-btn v-close-popup flat label="Speichern" color="primary" @click="addIngredient" />
|
|
||||||
</div>
|
|
||||||
</q-menu>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, PropType, ref, onBeforeMount, unref } from 'vue';
|
|
||||||
import { usePricelistStore } from '../../store';
|
|
||||||
import { clone } from '../../utils/utils';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'Ingredients',
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: Object as PropType<Array<FG.Ingredient>>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
editable: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: {
|
|
||||||
'update:modelValue': (val: Array<FG.Ingredient>) => val,
|
|
||||||
update: () => true,
|
|
||||||
'delete-ingredient': (val: FG.Ingredient) => val,
|
|
||||||
},
|
|
||||||
setup(props, { emit }) {
|
|
||||||
onBeforeMount(() => {
|
|
||||||
//edit_ingredients.value = <Array<FG.Ingredient>>JSON.parse(JSON.stringify(props.modelValue))
|
|
||||||
//edit_ingredients.value = props.modelValue
|
|
||||||
edit_ingredients.value = clone(props.modelValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = usePricelistStore();
|
|
||||||
|
|
||||||
const edit_ingredients = ref<Array<FG.Ingredient>>([]);
|
|
||||||
const newIngredient = ref<FG.Drink | FG.ExtraIngredient>();
|
|
||||||
const newIngredientVolume = ref<number>(0);
|
|
||||||
function addIngredient() {
|
|
||||||
let _ingredient: FG.Ingredient;
|
|
||||||
if ((<FG.Drink>newIngredient.value)?.volume && newIngredient.value) {
|
|
||||||
_ingredient = {
|
|
||||||
id: -1,
|
|
||||||
drink_ingredient: {
|
|
||||||
id: -1,
|
|
||||||
ingredient_id: newIngredient.value.id,
|
|
||||||
volume: newIngredientVolume.value,
|
|
||||||
},
|
|
||||||
extra_ingredient: undefined,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
_ingredient = {
|
|
||||||
id: -1,
|
|
||||||
drink_ingredient: undefined,
|
|
||||||
extra_ingredient: <FG.ExtraIngredient>newIngredient.value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
edit_ingredients.value.push(_ingredient);
|
|
||||||
emit('update:modelValue', unref(edit_ingredients));
|
|
||||||
update();
|
|
||||||
cancelAddIngredient();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateValue(value: number, initValue: number) {
|
|
||||||
console.log('updateValue', value, initValue);
|
|
||||||
emit('update:modelValue', unref(edit_ingredients));
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelAddIngredient() {
|
|
||||||
setTimeout(() => {
|
|
||||||
(newIngredient.value = undefined), (newIngredientVolume.value = 0);
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
function deleteIngredient(ingredient: FG.Ingredient) {
|
|
||||||
const index = edit_ingredients.value.findIndex((a) => a.id === ingredient.id);
|
|
||||||
if (index > -1) {
|
|
||||||
if (edit_ingredients.value[index].id > 0) {
|
|
||||||
emit('delete-ingredient', edit_ingredients.value[index]);
|
|
||||||
}
|
|
||||||
edit_ingredients.value.splice(index, 1);
|
|
||||||
}
|
|
||||||
emit('update:modelValue', unref(edit_ingredients));
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
const drinks = computed(() =>
|
|
||||||
store.drinks.filter((drink) => {
|
|
||||||
console.log('computed drinks', drink.name, drink.cost_per_volume);
|
|
||||||
return drink.cost_per_volume;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const extra_ingredients = computed(() => store.extraIngredients);
|
|
||||||
|
|
||||||
function get_drink_ingredient_name(id: number) {
|
|
||||||
return store.drinks.find((a) => a.id === id)?.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
setTimeout(() => {
|
|
||||||
emit('update');
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
addIngredient,
|
|
||||||
drinks,
|
|
||||||
extra_ingredients,
|
|
||||||
newIngredient,
|
|
||||||
newIngredientVolume,
|
|
||||||
cancelAddIngredient,
|
|
||||||
updateValue,
|
|
||||||
deleteIngredient,
|
|
||||||
get_drink_ingredient_name,
|
|
||||||
edit_ingredients,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,59 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="row justify-around q-pa-sm">
|
|
||||||
<q-input
|
|
||||||
v-model.number="newPrice.price"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class="q-px-sm"
|
|
||||||
type="number"
|
|
||||||
label="Preis"
|
|
||||||
suffix="€"
|
|
||||||
min="0"
|
|
||||||
step="0.1"
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-model="newPrice.description"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class="q-px-sm"
|
|
||||||
label="Beschreibung"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
<q-toggle v-model="newPrice.public" dense class="q-px-sm" label="Öffentlich" />
|
|
||||||
</div>
|
|
||||||
<div class="row justify-between q-pa-sm">
|
|
||||||
<q-btn v-close-popup label="Abbrechen" @click="cancelAddPrice" />
|
|
||||||
<q-btn v-close-popup label="Speichern" color="primary" @click="addPrice(row)" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, ref } from 'vue';
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'NewPrice',
|
|
||||||
emits: {
|
|
||||||
save: (val: FG.DrinkPrice) => val,
|
|
||||||
},
|
|
||||||
setup(_, { emit }) {
|
|
||||||
const emptyPrice: FG.DrinkPrice = {
|
|
||||||
id: -1,
|
|
||||||
price: 0,
|
|
||||||
description: '',
|
|
||||||
public: true,
|
|
||||||
};
|
|
||||||
const newPrice = ref(emptyPrice);
|
|
||||||
function addPrice() {
|
|
||||||
emit('save', newPrice.value);
|
|
||||||
cancelAddPrice();
|
|
||||||
}
|
|
||||||
function cancelAddPrice() {
|
|
||||||
setTimeout(() => {
|
|
||||||
newPrice.value = emptyPrice;
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
return { newPrice, addPrice, cancelAddPrice };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,354 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">Getränk Bearbeiten</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="full-width row">
|
|
||||||
<q-input
|
|
||||||
v-model="edit_drink.name"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
filled
|
|
||||||
label="Name"
|
|
||||||
dense
|
|
||||||
/>
|
|
||||||
<q-select
|
|
||||||
v-model="edit_drink.type"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
filled
|
|
||||||
label="Kategorie"
|
|
||||||
dense
|
|
||||||
:options="types"
|
|
||||||
option-label="name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<q-img :src="image" style="max-height: 256px" fit="contain" />
|
|
||||||
<div class="full-width row">
|
|
||||||
<div class="col-10 q-pa-sm">
|
|
||||||
<q-file
|
|
||||||
v-model="drinkPic"
|
|
||||||
filled
|
|
||||||
clearable
|
|
||||||
dense
|
|
||||||
@update:model-value="imagePreview"
|
|
||||||
@clear="imgsrc = undefined"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-image" />
|
|
||||||
</template>
|
|
||||||
</q-file>
|
|
||||||
</div>
|
|
||||||
<div class="col-2 q-pa-sm text-right">
|
|
||||||
<q-btn round icon="mdi-delete" color="negative" size="sm" @click="delete_pic">
|
|
||||||
<q-tooltip> Bild entfernen </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<q-select
|
|
||||||
v-model="edit_drink.tags"
|
|
||||||
multiple
|
|
||||||
:options="tags"
|
|
||||||
label="Tags"
|
|
||||||
option-label="name"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
>
|
|
||||||
<template #selected-item="item">
|
|
||||||
<q-chip
|
|
||||||
removable
|
|
||||||
:tabindex="item.tabindex"
|
|
||||||
:style="`background-color: ${item.opt.color}`"
|
|
||||||
@remove="item.removeAtIndex(item.index)"
|
|
||||||
>
|
|
||||||
{{ item.opt.name }}
|
|
||||||
</q-chip>
|
|
||||||
</template>
|
|
||||||
<template #option="item">
|
|
||||||
<q-item v-bind="item.itemProps" v-on="item.itemEvents">
|
|
||||||
<q-chip :style="`background-color: ${item.opt.color}`">
|
|
||||||
<q-avatar v-if="item.selected" icon="mdi-check" color="positive" text-color="white" />
|
|
||||||
{{ item.opt.name }}
|
|
||||||
</q-chip>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
</q-select>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="fit row">
|
|
||||||
<q-input
|
|
||||||
v-model="edit_drink.article_id"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
filled
|
|
||||||
label="Artikelnummer"
|
|
||||||
dense
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-model="edit_drink.volume"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
filled
|
|
||||||
label="Inhalt"
|
|
||||||
dense
|
|
||||||
suffix="L"
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-model="edit_drink.package_size"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
filled
|
|
||||||
label="Gebindegröße"
|
|
||||||
dense
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-model="edit_drink.cost_per_package"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
filled
|
|
||||||
label="Preis Gebinde"
|
|
||||||
suffix="€"
|
|
||||||
dense
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-model="cost_per_volume"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm q-pb-lg"
|
|
||||||
:outlined="auto_cost_per_volume || hasIngredients"
|
|
||||||
:filled="!auto_cost_per_volume && !hasIngredients"
|
|
||||||
:readonly="auto_cost_per_volume || hasIngredients"
|
|
||||||
label="Preis pro L"
|
|
||||||
hint="Inkl. 19% Mehrwertsteuer"
|
|
||||||
suffix="€"
|
|
||||||
dense
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section :key="key">
|
|
||||||
<drink-price-volumes
|
|
||||||
v-model="edit_volumes"
|
|
||||||
:cost-per-volume="cost_per_volume"
|
|
||||||
:editable="hasPermission(PERMISSIONS.EDIT_VOLUME)"
|
|
||||||
@update="updateVolume"
|
|
||||||
@delete-volume="deleteVolume"
|
|
||||||
@delete-price="deletePrice"
|
|
||||||
@delete-ingredient="deleteIngredient"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<build-manual :steps="edit_drink.receipt" @deleteStep="deleteStep" @addStep="addStep" />
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-actions class="justify-around">
|
|
||||||
<q-btn label="Abbrechen" @click="cancel" />
|
|
||||||
<q-btn v-if="can_delete" label="Löschen" color="negative" @click="delete_drink" />
|
|
||||||
<q-btn label="Speichern" color="primary" @click="save" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, PropType, ref, onBeforeMount, computed } from 'vue';
|
|
||||||
import { Drink, DrinkPriceVolume, usePricelistStore } from '../store';
|
|
||||||
import DrinkPriceVolumes from './CalculationTable/DrinkPriceVolumes.vue';
|
|
||||||
import { clone, calc_min_prices, DeleteObjects, calc_cost_per_volume } from '../utils/utils';
|
|
||||||
import BuildManual from 'src/plugins/pricelist/components/CalculationTable/BuildManual.vue';
|
|
||||||
import { baseURL } from 'src/config';
|
|
||||||
import { hasPermission } from 'src/utils/permission';
|
|
||||||
import { PERMISSIONS } from 'src/plugins/pricelist/permissions';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'DrinkModify',
|
|
||||||
components: { BuildManual, DrinkPriceVolumes },
|
|
||||||
props: {
|
|
||||||
drink: {
|
|
||||||
type: Object as PropType<Drink>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: {
|
|
||||||
save: (
|
|
||||||
drink: Drink,
|
|
||||||
toDeleteObjects: DeleteObjects,
|
|
||||||
drinkPic: File | undefined,
|
|
||||||
deletePic: boolean
|
|
||||||
) => (drink && toDeleteObjects) || drinkPic || deletePic,
|
|
||||||
delete: () => true,
|
|
||||||
cancel: () => true,
|
|
||||||
},
|
|
||||||
setup(props, { emit }) {
|
|
||||||
onBeforeMount(() => {
|
|
||||||
//edit_drink.value = <Drink>JSON.parse(JSON.stringify(props.drink));
|
|
||||||
edit_drink.value = clone(props.drink);
|
|
||||||
edit_volumes.value = clone(props.drink.volumes);
|
|
||||||
});
|
|
||||||
|
|
||||||
const key = ref(0);
|
|
||||||
|
|
||||||
const store = usePricelistStore();
|
|
||||||
|
|
||||||
const toDeleteObjects = ref<DeleteObjects>({
|
|
||||||
prices: [],
|
|
||||||
volumes: [],
|
|
||||||
ingredients: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const edit_drink = ref<Drink>();
|
|
||||||
const edit_volumes = ref<Array<DrinkPriceVolume>>([]);
|
|
||||||
function save() {
|
|
||||||
(<Drink>edit_drink.value).volumes = edit_volumes.value;
|
|
||||||
emit('save', <Drink>edit_drink.value, toDeleteObjects.value, drinkPic.value, deletePic.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancel() {
|
|
||||||
emit('cancel');
|
|
||||||
}
|
|
||||||
function updateVolume(index: number) {
|
|
||||||
if (index > -1 && edit_volumes.value) {
|
|
||||||
edit_volumes.value[index].min_prices = calc_min_prices(
|
|
||||||
edit_volumes.value[index],
|
|
||||||
//edit_drink.value.cost_per_volume,
|
|
||||||
cost_per_volume.value,
|
|
||||||
store.min_prices
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateVolumes() {
|
|
||||||
setTimeout(() => {
|
|
||||||
edit_volumes.value?.forEach((_, index) => {
|
|
||||||
updateVolume(index);
|
|
||||||
});
|
|
||||||
key.value++;
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deletePrice(price: FG.DrinkPrice) {
|
|
||||||
toDeleteObjects.value.prices.push(price);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteVolume(volume: DrinkPriceVolume) {
|
|
||||||
toDeleteObjects.value.volumes.push(volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteIngredient(ingredient: FG.Ingredient) {
|
|
||||||
toDeleteObjects.value.ingredients.push(ingredient);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addStep(event: string) {
|
|
||||||
edit_drink.value?.receipt?.push(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteStep(event: number) {
|
|
||||||
edit_drink.value?.receipt?.splice(event, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const drinkPic = ref();
|
|
||||||
const imgsrc = ref();
|
|
||||||
|
|
||||||
const deletePic = ref(false);
|
|
||||||
|
|
||||||
function delete_pic() {
|
|
||||||
deletePic.value = true;
|
|
||||||
imgsrc.value = undefined;
|
|
||||||
drinkPic.value = undefined;
|
|
||||||
if (edit_drink.value) {
|
|
||||||
edit_drink.value.uuid = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function imagePreview() {
|
|
||||||
if (drinkPic.value && drinkPic.value instanceof File) {
|
|
||||||
let reader = new FileReader();
|
|
||||||
|
|
||||||
reader.onload = (e) => {
|
|
||||||
imgsrc.value = e.target?.result;
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.readAsDataURL(drinkPic.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const image = computed(() => {
|
|
||||||
if (deletePic.value) {
|
|
||||||
return 'no-image.svg';
|
|
||||||
}
|
|
||||||
if (imgsrc.value) {
|
|
||||||
return <string>imgsrc.value;
|
|
||||||
}
|
|
||||||
if (edit_drink.value?.uuid) {
|
|
||||||
return `${baseURL.value}/pricelist/picture/${edit_drink.value.uuid}?size=256`;
|
|
||||||
}
|
|
||||||
return 'no-image.svg';
|
|
||||||
});
|
|
||||||
|
|
||||||
const can_delete = computed(() => {
|
|
||||||
if (edit_drink.value) {
|
|
||||||
if (edit_drink.value.id < 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const _edit_drink = edit_drink.value;
|
|
||||||
const test = _edit_drink.volumes ? _edit_drink.volumes.length === 0 : true;
|
|
||||||
console.log(test);
|
|
||||||
return test;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
function delete_drink() {
|
|
||||||
emit('delete');
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto_cost_per_volume = computed(
|
|
||||||
() =>
|
|
||||||
!!(
|
|
||||||
edit_drink.value?.cost_per_package &&
|
|
||||||
edit_drink.value?.package_size &&
|
|
||||||
edit_drink.value?.volume
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const cost_per_volume = computed({
|
|
||||||
get: () => {
|
|
||||||
let retVal: number;
|
|
||||||
if (auto_cost_per_volume.value) {
|
|
||||||
retVal = <number>calc_cost_per_volume(<Drink>edit_drink.value);
|
|
||||||
} else {
|
|
||||||
retVal = <number>(<Drink>edit_drink.value).cost_per_volume;
|
|
||||||
}
|
|
||||||
updateVolumes();
|
|
||||||
return retVal;
|
|
||||||
},
|
|
||||||
set: (val: number) => ((<Drink>edit_drink.value).cost_per_volume = val),
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasIngredients = computed(() =>
|
|
||||||
edit_volumes.value?.some((a) => a.ingredients.length > 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
edit_drink,
|
|
||||||
save,
|
|
||||||
cancel,
|
|
||||||
updateVolume,
|
|
||||||
deletePrice,
|
|
||||||
deleteIngredient,
|
|
||||||
deleteVolume,
|
|
||||||
addStep,
|
|
||||||
deleteStep,
|
|
||||||
tags: computed(() => store.tags),
|
|
||||||
image,
|
|
||||||
imgsrc,
|
|
||||||
drinkPic,
|
|
||||||
imagePreview,
|
|
||||||
delete_pic,
|
|
||||||
types: computed(() => store.drinkTypes),
|
|
||||||
can_delete,
|
|
||||||
delete_drink,
|
|
||||||
auto_cost_per_volume,
|
|
||||||
cost_per_volume,
|
|
||||||
edit_volumes,
|
|
||||||
key,
|
|
||||||
hasIngredients,
|
|
||||||
hasPermission,
|
|
||||||
PERMISSIONS,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,113 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-table
|
|
||||||
title="Getränkearten"
|
|
||||||
:rows="rows"
|
|
||||||
:row-key="(row) => row.id"
|
|
||||||
:columns="columns"
|
|
||||||
style="height: 100%"
|
|
||||||
:pagination="pagination"
|
|
||||||
>
|
|
||||||
<template #top-right>
|
|
||||||
<div class="full-width row q-gutter-sm">
|
|
||||||
<q-input v-model="newDrinkType" dense placeholder="Neue Getränkeart" filled />
|
|
||||||
<q-btn round color="primary" icon="mdi-plus" @click="addType">
|
|
||||||
<q-tooltip> Getränkeart hinzufügen </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td key="drinkTypeName" :props="props">
|
|
||||||
{{ props.row.name }}
|
|
||||||
<q-popup-edit
|
|
||||||
v-model="props.row.name"
|
|
||||||
buttons
|
|
||||||
label-set="Speichern"
|
|
||||||
label-cancel="Abbrechen"
|
|
||||||
@save="saveChanges(props.row)"
|
|
||||||
>
|
|
||||||
<template #default="scope">
|
|
||||||
<q-input v-model="scope.value" dense label="name" filled />
|
|
||||||
</template>
|
|
||||||
</q-popup-edit>
|
|
||||||
</q-td>
|
|
||||||
<q-td key="actions" :props="props">
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
icon="mdi-delete"
|
|
||||||
color="negative"
|
|
||||||
size="sm"
|
|
||||||
@click="deleteType(props.row.id)"
|
|
||||||
/>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, onBeforeMount, ref } from 'vue';
|
|
||||||
import { usePricelistStore } from '../store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'DrinkTypes',
|
|
||||||
setup() {
|
|
||||||
const store = usePricelistStore();
|
|
||||||
const newDrinkType = ref('');
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
void store.getDrinkTypes(true);
|
|
||||||
});
|
|
||||||
const rows = computed(() => store.drinkTypes);
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'drinkTypeName',
|
|
||||||
label: 'Getränkeart',
|
|
||||||
field: 'name',
|
|
||||||
align: 'left',
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'actions',
|
|
||||||
label: 'Aktionen',
|
|
||||||
field: 'actions',
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
async function addType() {
|
|
||||||
await store.addDrinkType(newDrinkType.value);
|
|
||||||
newDrinkType.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveChanges(drinkType: FG.DrinkType) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const _drinkType = store.drinkTypes.find((a) => a.id === drinkType.id);
|
|
||||||
if (_drinkType) {
|
|
||||||
void store.changeDrinkTypeName(drinkType);
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteType(id: number) {
|
|
||||||
void store.removeDrinkType(id);
|
|
||||||
}
|
|
||||||
const pagination = ref({
|
|
||||||
sortBy: 'name',
|
|
||||||
rowsPerPage: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
columns,
|
|
||||||
rows,
|
|
||||||
addType,
|
|
||||||
newDrinkType,
|
|
||||||
deleteType,
|
|
||||||
saveChanges,
|
|
||||||
pagination,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,154 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<q-page padding>
|
|
||||||
<q-table
|
|
||||||
title="Getränkearten"
|
|
||||||
:rows="rows"
|
|
||||||
:row-key="(row) => row.id"
|
|
||||||
:columns="columns"
|
|
||||||
:pagination="pagination"
|
|
||||||
>
|
|
||||||
<template #top-right>
|
|
||||||
<div class="full-width row q-gutter-sm">
|
|
||||||
<q-input
|
|
||||||
v-model="newExtraIngredient.name"
|
|
||||||
dense
|
|
||||||
placeholder="Neue Zutatenbezeichnung"
|
|
||||||
label="Neue Zutatenbezeichnung"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-model.number="newExtraIngredient.price"
|
|
||||||
dense
|
|
||||||
placeholder="Preis"
|
|
||||||
label="Preis"
|
|
||||||
filled
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="0.1"
|
|
||||||
suffix="€"
|
|
||||||
/>
|
|
||||||
<q-btn color="primary" icon="mdi-plus" round @click="addExtraIngredient">
|
|
||||||
<q-tooltip> Zutat hinzufügen </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td key="name" :props="props" align="left">
|
|
||||||
{{ props.row.name }}
|
|
||||||
<q-popup-edit
|
|
||||||
v-model="props.row.name"
|
|
||||||
buttons
|
|
||||||
label-set="Speichern"
|
|
||||||
label-cancel="Abbrechen"
|
|
||||||
@save="saveChanges(props.row)"
|
|
||||||
>
|
|
||||||
<template #default="scope">
|
|
||||||
<q-input v-model="scope.value" dense label="name" filled />
|
|
||||||
</template>
|
|
||||||
</q-popup-edit>
|
|
||||||
</q-td>
|
|
||||||
<q-td key="price" :props="props" align="right">
|
|
||||||
{{ props.row.price.toFixed(2) }}€
|
|
||||||
</q-td>
|
|
||||||
<q-td key="actions" :props="props" align="right" auto-width>
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
icon="mdi-delete"
|
|
||||||
color="negative"
|
|
||||||
size="sm"
|
|
||||||
@click="deleteType(props.row)"
|
|
||||||
/>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-page>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, ComputedRef, defineComponent, ref } from 'vue';
|
|
||||||
import { usePricelistStore } from 'src/plugins/pricelist/store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'DrinkTypes',
|
|
||||||
setup() {
|
|
||||||
const store = usePricelistStore();
|
|
||||||
const emptyExtraIngredient: FG.ExtraIngredient = {
|
|
||||||
name: '',
|
|
||||||
price: 0,
|
|
||||||
id: -1,
|
|
||||||
};
|
|
||||||
const newExtraIngredient = ref<FG.ExtraIngredient>(emptyExtraIngredient);
|
|
||||||
|
|
||||||
const rows = computed(() => store.extraIngredients);
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
label: 'Bezeichnung',
|
|
||||||
field: 'name',
|
|
||||||
align: 'left',
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'price',
|
|
||||||
label: 'Preis',
|
|
||||||
field: 'price',
|
|
||||||
sortable: true,
|
|
||||||
format: (val: number) => `${val.toFixed(2)}€`,
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'actions',
|
|
||||||
label: 'Aktionen',
|
|
||||||
field: 'actions',
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
async function addExtraIngredient() {
|
|
||||||
await store.setExtraIngredient((<ComputedRef>newExtraIngredient).value);
|
|
||||||
newExtraIngredient.value = Object.assign({}, emptyExtraIngredient);
|
|
||||||
discardChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveChanges(ingredient: FG.ExtraIngredient) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const _ingredient = store.extraIngredients.find((a) => a.id === ingredient.id);
|
|
||||||
if (_ingredient) {
|
|
||||||
void store.updateExtraIngredient(_ingredient);
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
function discardChanges() {
|
|
||||||
newExtraIngredient.value.name = '';
|
|
||||||
newExtraIngredient.value.price = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteType(extraIngredient: FG.ExtraIngredient) {
|
|
||||||
void store.deleteExtraIngredient(extraIngredient);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pagination = ref({
|
|
||||||
sortBy: 'name',
|
|
||||||
rowsPerPage: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
columns,
|
|
||||||
rows,
|
|
||||||
addExtraIngredient,
|
|
||||||
newExtraIngredient,
|
|
||||||
deleteType,
|
|
||||||
discardChanges,
|
|
||||||
saveChanges,
|
|
||||||
pagination,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,55 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-list>
|
|
||||||
<div v-for="(min_price, index) in min_prices" :key="index">
|
|
||||||
<q-item>
|
|
||||||
<q-item-section>{{ min_price }}%</q-item-section>
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
icon="mdi-delete"
|
|
||||||
size="sm"
|
|
||||||
color="negative"
|
|
||||||
@click="delete_min_price(min_price)"
|
|
||||||
/>
|
|
||||||
</q-item>
|
|
||||||
<q-separator />
|
|
||||||
</div>
|
|
||||||
</q-list>
|
|
||||||
<q-input v-model.number="new_min_price" class="q-pa-sm" type="number" suffix="%" filled dense />
|
|
||||||
<q-btn class="full-width" label="speichern" @click="save_min_prices"></q-btn>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, ref } from 'vue';
|
|
||||||
import { usePricelistStore } from 'src/plugins/pricelist/store';
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'MinPriceSetting',
|
|
||||||
setup() {
|
|
||||||
const store = usePricelistStore();
|
|
||||||
const min_prices = computed(() => store.min_prices);
|
|
||||||
const new_min_price = ref<number>();
|
|
||||||
function save_min_prices() {
|
|
||||||
const index = min_prices.value.findIndex((a) => a === new_min_price.value);
|
|
||||||
if (index < 0) {
|
|
||||||
min_prices.value.push(<number>new_min_price.value);
|
|
||||||
void store.set_min_prices();
|
|
||||||
new_min_price.value = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function delete_min_price(min_price: number) {
|
|
||||||
const index = min_prices.value.findIndex((a) => a === min_price);
|
|
||||||
if (index > -1) {
|
|
||||||
min_prices.value.splice(index, 1);
|
|
||||||
void store.set_min_prices();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
min_prices,
|
|
||||||
new_min_price,
|
|
||||||
save_min_prices,
|
|
||||||
delete_min_price,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,327 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-table
|
|
||||||
title="Preisliste"
|
|
||||||
:columns="columns"
|
|
||||||
:rows="drinks"
|
|
||||||
:visible-columns="visibleColumns"
|
|
||||||
:filter="search"
|
|
||||||
:filter-method="filter"
|
|
||||||
dense
|
|
||||||
:pagination="pagination"
|
|
||||||
:fullscreen="fullscreen"
|
|
||||||
>
|
|
||||||
<template #top-right>
|
|
||||||
<div class="row justify-end q-gutter-sm">
|
|
||||||
<search-input v-model="search" :keys="options" />
|
|
||||||
<q-select
|
|
||||||
v-model="visibleColumns"
|
|
||||||
multiple
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
options-dense
|
|
||||||
display-value="Sichtbarkeit"
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
:options="options"
|
|
||||||
option-value="name"
|
|
||||||
options-cover
|
|
||||||
/>
|
|
||||||
<q-btn round icon="mdi-backburger">
|
|
||||||
<q-tooltip anchor='top middle' self='bottom middle'> Reihenfolge ändern </q-tooltip>
|
|
||||||
<q-menu anchor="bottom middle" self="top middle">
|
|
||||||
<drag v-model="order" class="q-list" ghost-class="ghost" group="people" item-key="id">
|
|
||||||
<template #item="{ element }">
|
|
||||||
<q-item>
|
|
||||||
<q-item-section>
|
|
||||||
{{ element.label }}
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
</drag>
|
|
||||||
</q-menu>
|
|
||||||
</q-btn>
|
|
||||||
<slot></slot>
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
:icon="fullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'"
|
|
||||||
@click="fullscreen = !fullscreen"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #body-cell-tags="props">
|
|
||||||
<q-td :props="props">
|
|
||||||
<q-badge
|
|
||||||
v-for="tag in props.row.tags"
|
|
||||||
:key="`${props.row.id}-${tag.id}`"
|
|
||||||
class="text-caption"
|
|
||||||
rounded
|
|
||||||
:style="`background-color: ${tag.color}`"
|
|
||||||
>
|
|
||||||
{{ tag.name }}
|
|
||||||
</q-badge>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
<template #body-cell-public="props">
|
|
||||||
<q-td :props="props">
|
|
||||||
<q-toggle
|
|
||||||
v-model="props.row.public"
|
|
||||||
disable
|
|
||||||
checked-icon="mdi-earth"
|
|
||||||
unchecked-icon="mdi-earth-off"
|
|
||||||
/>
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, onBeforeMount, ref, ComponentPublicInstance } from 'vue';
|
|
||||||
import { usePricelistStore, Order } from '../store';
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
import { Search, filter } from 'src/plugins/pricelist/utils/filter';
|
|
||||||
import SearchInput from 'src/plugins/pricelist/components/SearchInput.vue';
|
|
||||||
import draggable from 'vuedraggable';
|
|
||||||
const drag: ComponentPublicInstance = <ComponentPublicInstance>draggable;
|
|
||||||
|
|
||||||
interface Row {
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
field: string;
|
|
||||||
sortable?: boolean;
|
|
||||||
filterable?: boolean;
|
|
||||||
format?: (val: never) => string;
|
|
||||||
align?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'Pricelist',
|
|
||||||
components: { SearchInput, drag },
|
|
||||||
props: {
|
|
||||||
public: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const store = usePricelistStore();
|
|
||||||
const user = ref('');
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
if (!props.public) {
|
|
||||||
user.value = useMainStore().currentUser.userid;
|
|
||||||
void store.getPriceListColumnOrder(user.value);
|
|
||||||
void store.getDrinks();
|
|
||||||
void store.getPriceCalcColumn(user.value);
|
|
||||||
} else {
|
|
||||||
user.value = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const _order = ref<Array<Order>>([
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
label: 'Name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'type',
|
|
||||||
label: 'Kategorie',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tags',
|
|
||||||
label: 'Tags',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'volume',
|
|
||||||
label: 'Inhalt',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'price',
|
|
||||||
label: 'Preis',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'public',
|
|
||||||
label: 'Öffentlich',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
label: 'Beschreibung',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const order = computed<Array<Order>>({
|
|
||||||
get: () => {
|
|
||||||
if (props.public) {
|
|
||||||
return _order.value;
|
|
||||||
}
|
|
||||||
if (store.pricelist_columns_order.length === 0) {
|
|
||||||
return _order.value;
|
|
||||||
}
|
|
||||||
return store.pricelist_columns_order;
|
|
||||||
},
|
|
||||||
set: (val: Array<Order>) => {
|
|
||||||
if (!props.public) {
|
|
||||||
void store.updatePriceListColumnOrder(user.value, val);
|
|
||||||
} else {
|
|
||||||
_order.value = val;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const _columns: Array<Row> = [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
label: 'Name',
|
|
||||||
field: 'name',
|
|
||||||
sortable: true,
|
|
||||||
filterable: true,
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'type',
|
|
||||||
label: 'Kategorie',
|
|
||||||
field: 'type',
|
|
||||||
sortable: true,
|
|
||||||
filterable: true,
|
|
||||||
format: (val: FG.DrinkType) => val.name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tags',
|
|
||||||
label: 'Tags',
|
|
||||||
field: 'tags',
|
|
||||||
filterable: true,
|
|
||||||
|
|
||||||
format: (val: Array<FG.Tag>) => {
|
|
||||||
let retVal = '';
|
|
||||||
val.forEach((tag, index) => {
|
|
||||||
if (index >= val.length - 1 && index > 0) {
|
|
||||||
retVal += ', ';
|
|
||||||
}
|
|
||||||
retVal += tag.name;
|
|
||||||
});
|
|
||||||
return retVal;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'volume',
|
|
||||||
label: 'Inhalt',
|
|
||||||
field: 'volume',
|
|
||||||
filterable: true,
|
|
||||||
sortable: true,
|
|
||||||
format: (val: number) => `${val.toFixed(3)}L`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'price',
|
|
||||||
label: 'Preis',
|
|
||||||
field: 'price',
|
|
||||||
sortable: true,
|
|
||||||
filterable: true,
|
|
||||||
format: (val: number) => `${val.toFixed(2)}€`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'public',
|
|
||||||
label: 'Öffentlich',
|
|
||||||
field: 'public',
|
|
||||||
format: (val: boolean) => (val ? 'Öffentlich' : 'nicht Öffentlich'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
label: 'Beschreibung',
|
|
||||||
field: 'description',
|
|
||||||
filterable: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const columns = computed(() => {
|
|
||||||
const retVal: Array<Row> = [];
|
|
||||||
if (order.value) {
|
|
||||||
order.value.forEach((col) => {
|
|
||||||
const _col = _columns.find((a) => a.name === col.name);
|
|
||||||
if (_col) {
|
|
||||||
retVal.push(_col);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
retVal.forEach((element, index) => {
|
|
||||||
element.align = 'right';
|
|
||||||
if (index === 0) {
|
|
||||||
element.align = 'left';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return retVal;
|
|
||||||
}
|
|
||||||
return _columns;
|
|
||||||
});
|
|
||||||
|
|
||||||
const _options = computed(() => {
|
|
||||||
const retVal: Array<{ name: string; label: string; field: string }> = [];
|
|
||||||
columns.value.forEach((col) => {
|
|
||||||
if (props.public) {
|
|
||||||
if (col.name !== 'public') {
|
|
||||||
retVal.push(col);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
retVal.push(col);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return retVal;
|
|
||||||
});
|
|
||||||
|
|
||||||
const _colums = computed<Array<string>>(() => {
|
|
||||||
const retVal: Array<string> = [];
|
|
||||||
columns.value.forEach((col) => {
|
|
||||||
if (props.public) {
|
|
||||||
if (col.name !== 'public') {
|
|
||||||
retVal.push(col.name);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
retVal.push(col.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return retVal;
|
|
||||||
});
|
|
||||||
|
|
||||||
const _visibleColumns = ref(_colums.value);
|
|
||||||
|
|
||||||
const visibleColumns = computed({
|
|
||||||
get: () => (props.public ? _visibleColumns.value : store.pricecalc_columns),
|
|
||||||
set: (val) => {
|
|
||||||
if (!props.public) {
|
|
||||||
void store.updatePriceCalcColumn(user.value, val);
|
|
||||||
} else {
|
|
||||||
_visibleColumns.value = val;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const search = ref<Search>({
|
|
||||||
value: '',
|
|
||||||
key: '',
|
|
||||||
label: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const pagination = ref({
|
|
||||||
sortBy: 'name',
|
|
||||||
rowsPerPage: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fullscreen = ref(false);
|
|
||||||
|
|
||||||
return {
|
|
||||||
drinks: computed(() => store.pricelist),
|
|
||||||
columns,
|
|
||||||
order,
|
|
||||||
visibleColumns,
|
|
||||||
options: _options,
|
|
||||||
search,
|
|
||||||
filter,
|
|
||||||
pagination,
|
|
||||||
fullscreen,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="sass">
|
|
||||||
.ghost
|
|
||||||
opacity: 0.5
|
|
||||||
background: $accent
|
|
||||||
</style>
|
|
|
@ -1,89 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-dialog v-model="alert">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">Suche</div>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section class="q-pt-none">
|
|
||||||
<div>
|
|
||||||
Wenn du in die Suche etwas eingibst, wird in allen Spalten gesucht. Mit einem `@` Zeichen,
|
|
||||||
kann man die Suche eingrenzen auf eine Spalte. Zumbeispiel: `Tequilaparty@Tags`
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<div>Mögliche Suchbegriffe nach dem @:</div>
|
|
||||||
<div class="fit row q-gutter-sm">
|
|
||||||
<div v-for="key in keys" :key="key.name">
|
|
||||||
{{ key.label }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<q-btn v-close-popup flat label="OK" color="primary" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<q-input v-model="v_model" filled dense>
|
|
||||||
<template #append>
|
|
||||||
<q-icon name="mdi-magnify" />
|
|
||||||
</template>
|
|
||||||
<template #prepend>
|
|
||||||
<q-btn icon="mdi-help-circle" flat round @click="alert = true" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</template>
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, computed, PropType, ref } from 'vue';
|
|
||||||
import { Search, Col } from '../utils/filter';
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'SearchInput',
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: Object as PropType<Search>,
|
|
||||||
default: () => ({ value: '', key: undefined, label: '' }),
|
|
||||||
},
|
|
||||||
keys: {
|
|
||||||
type: Object as PropType<Array<Col>>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: {
|
|
||||||
'update:modelValue': (val: {
|
|
||||||
value: string;
|
|
||||||
key: string | undefined;
|
|
||||||
label: string | undefined;
|
|
||||||
}) => val,
|
|
||||||
},
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const v_model = computed<string>({
|
|
||||||
get: () => {
|
|
||||||
if (!props.modelValue.label || props.modelValue.label === '') {
|
|
||||||
return `${props.modelValue.value}`;
|
|
||||||
}
|
|
||||||
return `${props.modelValue.value}@${props.modelValue.label}`;
|
|
||||||
},
|
|
||||||
set: (val: string) => {
|
|
||||||
const split = val.toLowerCase().split('@');
|
|
||||||
if (split.length < 2) {
|
|
||||||
emit('update:modelValue', { value: split[0], label: undefined, key: undefined });
|
|
||||||
} else {
|
|
||||||
props.keys.find((key) => {
|
|
||||||
if (key.label.toLowerCase() === split[1]) {
|
|
||||||
console.log(key.name);
|
|
||||||
emit('update:modelValue', { value: split[0], label: split[1], key: key.name });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const alert = ref(false);
|
|
||||||
return { v_model, alert };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
a
|
|
|
@ -1,181 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<q-page padding>
|
|
||||||
<q-table
|
|
||||||
title="Tags"
|
|
||||||
:rows="rows"
|
|
||||||
:row-key="(row) => row.id"
|
|
||||||
:columns="columns"
|
|
||||||
:pagination="pagination"
|
|
||||||
>
|
|
||||||
<template #top-right>
|
|
||||||
<q-btn color="primary" icon="mdi-plus" round>
|
|
||||||
<q-tooltip> Tag hinzufügen </q-tooltip>
|
|
||||||
<q-menu v-model="popup" anchor="center middle" self="center middle" persistent>
|
|
||||||
<q-input
|
|
||||||
v-model="newTag.name"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
label="Name"
|
|
||||||
class="q-pa-sm"
|
|
||||||
:rule="[notExists]"
|
|
||||||
/>
|
|
||||||
<q-color
|
|
||||||
:model-value="newTag.color"
|
|
||||||
flat
|
|
||||||
class="q-pa-sm"
|
|
||||||
@change="
|
|
||||||
(val) => {
|
|
||||||
newTag.color = val;
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<div class="full-width row q-gutter-sm justify-around q-py-sm">
|
|
||||||
<q-btn v-close-popup flat label="Abbrechen" />
|
|
||||||
<q-btn flat label="Speichern" color="primary" @click="save" />
|
|
||||||
</div>
|
|
||||||
</q-menu>
|
|
||||||
</q-btn>
|
|
||||||
</template>
|
|
||||||
<template #header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
|
||||||
{{ col.label }}
|
|
||||||
</q-th>
|
|
||||||
<q-th auto-width />
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
<template #body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td key="name" :props="props">
|
|
||||||
{{ props.row.name }}
|
|
||||||
<q-popup-edit
|
|
||||||
v-model="props.row.name"
|
|
||||||
buttons
|
|
||||||
label-cancel="Abbrechen"
|
|
||||||
label-set="Speichern"
|
|
||||||
@update:modelValue="updateTag(props.row)"
|
|
||||||
>
|
|
||||||
<template #default="scope">
|
|
||||||
<q-input v-model="scope.value" :rules="[notExists]" dense filled />
|
|
||||||
</template>
|
|
||||||
</q-popup-edit>
|
|
||||||
</q-td>
|
|
||||||
<q-td key="color" :props="props">
|
|
||||||
<div class="full-width row q-gutter-sm justify-end items-center">
|
|
||||||
<div>
|
|
||||||
{{ props.row.color }}
|
|
||||||
</div>
|
|
||||||
<div class="color-box" :style="`background-color: ${props.row.color};`"> </div>
|
|
||||||
</div>
|
|
||||||
<q-popup-edit
|
|
||||||
v-model="props.row.color"
|
|
||||||
buttons
|
|
||||||
label-cancel="Abbrechen"
|
|
||||||
label-set="Speichern"
|
|
||||||
@update:modelValue="updateTag(props.row)"
|
|
||||||
>
|
|
||||||
<template #default="slot">
|
|
||||||
<div class="full-width row justify-center">
|
|
||||||
<q-color
|
|
||||||
:model-value="slot.value"
|
|
||||||
class="full-width"
|
|
||||||
flat
|
|
||||||
@change="(val) => (slot.value = val)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</q-popup-edit>
|
|
||||||
</q-td>
|
|
||||||
<q-td>
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-delete"
|
|
||||||
color="negative"
|
|
||||||
round
|
|
||||||
size="sm"
|
|
||||||
@click="deleteTag(props.row)"
|
|
||||||
/>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-page>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, ref, onBeforeMount, computed } from 'vue';
|
|
||||||
import { usePricelistStore } from '../store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'Tags',
|
|
||||||
setup() {
|
|
||||||
const store = usePricelistStore();
|
|
||||||
onBeforeMount(() => {
|
|
||||||
void store.getTags();
|
|
||||||
});
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
label: 'Name',
|
|
||||||
field: 'name',
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'color',
|
|
||||||
label: 'Farbe',
|
|
||||||
field: 'color',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const rows = computed(() => store.tags);
|
|
||||||
const emptyTag = {
|
|
||||||
id: -1,
|
|
||||||
color: '#1976d2',
|
|
||||||
name: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
async function save() {
|
|
||||||
await store.setTag(newTag.value);
|
|
||||||
popup.value = false;
|
|
||||||
newTag.value = emptyTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newTag = ref(emptyTag);
|
|
||||||
const popup = ref(false);
|
|
||||||
function notExists(val: string) {
|
|
||||||
const index = store.tags.findIndex((a) => a.name === val);
|
|
||||||
if (index > -1) {
|
|
||||||
return 'Tag existiert bereits.';
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const pagination = ref({
|
|
||||||
sortBy: 'name',
|
|
||||||
rowsPerPage: 10,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
columns,
|
|
||||||
rows,
|
|
||||||
newTag,
|
|
||||||
popup,
|
|
||||||
save,
|
|
||||||
updateTag: store.updateTag,
|
|
||||||
notExists,
|
|
||||||
deleteTag: store.deleteTag,
|
|
||||||
pagination,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.color-box {
|
|
||||||
min-width: 28px;
|
|
||||||
min-heigh: 28px;
|
|
||||||
max-width: 28px;
|
|
||||||
max-height: 28px;
|
|
||||||
border-width: 1px;
|
|
||||||
border-color: black;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,65 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="q-table__title">Cocktailbuilder</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<ingredients
|
|
||||||
v-model="volume.ingredients"
|
|
||||||
class="q-pa-sm"
|
|
||||||
editable
|
|
||||||
@update:modelValue="update"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="q-table__title">Du solltest mindest sowiel verlangen oder bezahlen:</div>
|
|
||||||
<div class="full-width row q-gutter-sm justify-around">
|
|
||||||
<div v-for="min_price in volume.min_prices" :key="min_price.percentage">
|
|
||||||
<div>
|
|
||||||
<q-badge class="text-h6" color="primary"> {{ min_price.percentage }}% </q-badge>
|
|
||||||
</div>
|
|
||||||
<div>{{ min_price.price.toFixed(2) }}€</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, onBeforeMount, ref } from 'vue';
|
|
||||||
import Ingredients from 'src/plugins/pricelist/components/CalculationTable/Ingredients.vue';
|
|
||||||
import { DrinkPriceVolume, usePricelistStore } from 'src/plugins/pricelist/store';
|
|
||||||
import { calc_min_prices } from '../utils/utils';
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'CocktailBuilder',
|
|
||||||
components: { Ingredients },
|
|
||||||
setup() {
|
|
||||||
onBeforeMount(() => {
|
|
||||||
void store.get_min_prices().finally(() => {
|
|
||||||
volume.value.min_prices = calc_min_prices(volume.value, undefined, store.min_prices);
|
|
||||||
});
|
|
||||||
void store.getDrinks();
|
|
||||||
void store.getDrinkTypes();
|
|
||||||
void store.getExtraIngredients();
|
|
||||||
});
|
|
||||||
const store = usePricelistStore();
|
|
||||||
|
|
||||||
const emptyVolume: DrinkPriceVolume = {
|
|
||||||
id: -1,
|
|
||||||
_volume: 0,
|
|
||||||
min_prices: [],
|
|
||||||
prices: [],
|
|
||||||
ingredients: [],
|
|
||||||
};
|
|
||||||
const volume = ref(emptyVolume);
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
volume.value.min_prices = calc_min_prices(volume.value, undefined, store.min_prices);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { volume, update };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,38 +0,0 @@
|
||||||
<template>
|
|
||||||
<calculation-table v-if="!list" nodetails>
|
|
||||||
<q-btn icon="mdi-view-list" round @click="list = !list">
|
|
||||||
<q-tooltip> Zur Listenansicht wechseln </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</calculation-table>
|
|
||||||
<pricelist v-if="list">
|
|
||||||
<q-btn icon="mdi-cards-variant" round @click="list = !list">
|
|
||||||
<q-tooltip> Zur Kartenansicht wechseln </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</pricelist>
|
|
||||||
</template>
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, onBeforeMount, computed } from 'vue';
|
|
||||||
import CalculationTable from '../components/CalculationTable.vue';
|
|
||||||
import Pricelist from 'src/plugins/pricelist/components/Pricelist.vue';
|
|
||||||
import { usePricelistStore } from 'src/plugins/pricelist/store';
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'InnerPricelist',
|
|
||||||
components: { Pricelist, CalculationTable },
|
|
||||||
setup() {
|
|
||||||
const store = usePricelistStore();
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
void store.getDrinks();
|
|
||||||
void store.getPriceListView(mainStore.currentUser.userid);
|
|
||||||
});
|
|
||||||
|
|
||||||
const list = computed({
|
|
||||||
get: () => store.pricelist_view,
|
|
||||||
set: (val: boolean) => store.updatePriceListView(mainStore.currentUser.userid, val),
|
|
||||||
});
|
|
||||||
return { list };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,36 +0,0 @@
|
||||||
<template>
|
|
||||||
<calculation-table v-if="!list" public>
|
|
||||||
<q-btn icon="mdi-view-list" round @click="list = !list">
|
|
||||||
<q-tooltip> Zur Listenansicht wechseln </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</calculation-table>
|
|
||||||
<pricelist v-if="list" public>
|
|
||||||
<q-btn icon="mdi-cards-variant" round @click="list = !list">
|
|
||||||
<q-tooltip> Zur Kartenansicht wechseln </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</pricelist>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import CalculationTable from '../components/CalculationTable.vue';
|
|
||||||
import Pricelist from '../components/Pricelist.vue';
|
|
||||||
import { usePricelistStore } from '../store';
|
|
||||||
import { onBeforeMount, ref } from 'vue';
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'OuterPricelist',
|
|
||||||
components: { Pricelist, CalculationTable },
|
|
||||||
setup() {
|
|
||||||
const store = usePricelistStore();
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
void store.getDrinks();
|
|
||||||
});
|
|
||||||
|
|
||||||
const list = ref(false);
|
|
||||||
return { list };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,175 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-table
|
|
||||||
grid
|
|
||||||
title="Rezepte"
|
|
||||||
:rows="drinks"
|
|
||||||
row-key="id"
|
|
||||||
hide-header
|
|
||||||
:filter="search"
|
|
||||||
:filter-method="filter"
|
|
||||||
:columns="options"
|
|
||||||
>
|
|
||||||
<template #top-right>
|
|
||||||
<search-input v-model="search" :keys="search_keys" />
|
|
||||||
</template>
|
|
||||||
<template #item="props">
|
|
||||||
<div class="q-pa-xs col-xs-12 col-sm-6 col-md-4">
|
|
||||||
<q-card>
|
|
||||||
<q-img
|
|
||||||
style="max-height: 256px"
|
|
||||||
loading="lazy"
|
|
||||||
:src="image(props.row.uuid)"
|
|
||||||
placeholder-src="no-image.svg"
|
|
||||||
>
|
|
||||||
<div class="absolute-bottom-right justify-end">
|
|
||||||
<div class="text-subtitle1 text-right">
|
|
||||||
{{ props.row.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-caption text-right">
|
|
||||||
{{ props.row.type.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-img>
|
|
||||||
<q-card-section>
|
|
||||||
<q-badge
|
|
||||||
v-for="tag in props.row.tags"
|
|
||||||
:key="`${props.row.id}-${tag.id}`"
|
|
||||||
class="text-caption"
|
|
||||||
rounded
|
|
||||||
:style="`background-color: ${tag.color}`"
|
|
||||||
>
|
|
||||||
{{ tag.name }}
|
|
||||||
</q-badge>
|
|
||||||
</q-card-section>
|
|
||||||
<build-manual-volume :volumes="props.row.volumes" />
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">Anleitung</div>
|
|
||||||
<build-manual :steps="props.row.receipt" :editable="false" />
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, onBeforeMount, ref } from 'vue';
|
|
||||||
import { usePricelistStore } from 'src/plugins/pricelist/store';
|
|
||||||
import BuildManual from 'src/plugins/pricelist/components/CalculationTable/BuildManual.vue';
|
|
||||||
import BuildManualVolume from '../components/BuildManual/BuildManualVolume.vue';
|
|
||||||
import SearchInput from '../components/SearchInput.vue';
|
|
||||||
import { filter, Search } from '../utils/filter';
|
|
||||||
import { sort } from '../utils/sort';
|
|
||||||
import { baseURL } from 'src/config';
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'Reciepts',
|
|
||||||
components: { BuildManual, BuildManualVolume, SearchInput },
|
|
||||||
setup() {
|
|
||||||
const store = usePricelistStore();
|
|
||||||
onBeforeMount(() => {
|
|
||||||
void store.getDrinks();
|
|
||||||
});
|
|
||||||
const drinks = computed(() =>
|
|
||||||
store.drinks.filter((drink) => {
|
|
||||||
return drink.volumes.some((volume) => volume.ingredients.length > 0);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const columns_drinks = [
|
|
||||||
{
|
|
||||||
name: 'picture',
|
|
||||||
label: 'Bild',
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
label: 'Name',
|
|
||||||
field: 'name',
|
|
||||||
align: 'center',
|
|
||||||
sortable: true,
|
|
||||||
filterable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'drink_type',
|
|
||||||
label: 'Kategorie',
|
|
||||||
field: 'type',
|
|
||||||
format: (val: FG.DrinkType) => `${val.name}`,
|
|
||||||
sortable: true,
|
|
||||||
sort: (a: FG.DrinkType, b: FG.DrinkType) => sort(a.name, b.name),
|
|
||||||
filterable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tags',
|
|
||||||
label: 'Tag',
|
|
||||||
field: 'tags',
|
|
||||||
format: (val: Array<FG.Tag>) => {
|
|
||||||
let retVal = '';
|
|
||||||
val.forEach((tag, index) => {
|
|
||||||
if (index > 0) {
|
|
||||||
retVal += ', ';
|
|
||||||
}
|
|
||||||
retVal += tag.name;
|
|
||||||
});
|
|
||||||
return retVal;
|
|
||||||
},
|
|
||||||
filterable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'volumes',
|
|
||||||
label: 'Preise',
|
|
||||||
field: 'volumes',
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const columns_volumes = [
|
|
||||||
{
|
|
||||||
name: 'volume',
|
|
||||||
label: 'Inhalt',
|
|
||||||
field: 'volume',
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'prices',
|
|
||||||
label: 'Preise',
|
|
||||||
field: 'prices',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const columns_prices = [
|
|
||||||
{
|
|
||||||
name: 'price',
|
|
||||||
label: 'Preis',
|
|
||||||
field: 'price',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
label: 'Beschreibung',
|
|
||||||
field: 'description',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'public',
|
|
||||||
label: 'Öffentlich',
|
|
||||||
field: 'public',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const search = ref<Search>({ value: '', key: '', label: '' });
|
|
||||||
const search_keys = computed(() => columns_drinks.filter((column) => column.filterable));
|
|
||||||
function image(uuid: string | undefined) {
|
|
||||||
if (uuid) {
|
|
||||||
return `${baseURL.value}/pricelist/picture/${uuid}?size=256`;
|
|
||||||
}
|
|
||||||
return 'no-image.svg';
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
drinks,
|
|
||||||
options: [...columns_drinks, ...columns_volumes, ...columns_prices],
|
|
||||||
search,
|
|
||||||
filter,
|
|
||||||
search_keys,
|
|
||||||
image,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,136 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<q-tabs v-if="$q.screen.gt.sm" v-model="tab">
|
|
||||||
<q-tab
|
|
||||||
v-for="(tabindex, index) in tabs"
|
|
||||||
:key="'tab' + index"
|
|
||||||
:name="tabindex.name"
|
|
||||||
:label="tabindex.label"
|
|
||||||
/>
|
|
||||||
</q-tabs>
|
|
||||||
<div v-else class="fit row justify-end">
|
|
||||||
<q-btn flat round icon="mdi-menu" @click="showDrawer = !showDrawer"></q-btn>
|
|
||||||
</div>
|
|
||||||
<q-drawer v-model="showDrawer" side="right" behavior="mobile" @click="showDrawer = !showDrawer">
|
|
||||||
<q-list v-model="tab">
|
|
||||||
<q-item
|
|
||||||
v-for="(tabindex, index) in tabs"
|
|
||||||
:key="'tab' + index"
|
|
||||||
:active="tab == tabindex.name"
|
|
||||||
clickable
|
|
||||||
@click="tab = tabindex.name"
|
|
||||||
>
|
|
||||||
<q-item-label>{{ tabindex.label }}</q-item-label>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-drawer>
|
|
||||||
<q-page paddding class="fit row justify-center content-start items-start q-gutter-sm">
|
|
||||||
<q-tab-panels
|
|
||||||
v-model="tab"
|
|
||||||
style="background-color: transparent"
|
|
||||||
animated
|
|
||||||
class="q-ma-none q-pa-none fit row justify-center content-start items-start"
|
|
||||||
>
|
|
||||||
<q-tab-panel name="pricelist">
|
|
||||||
<calculation-table editable />
|
|
||||||
</q-tab-panel>
|
|
||||||
<q-tab-panel name="extra_ingredients">
|
|
||||||
<extra-ingredients />
|
|
||||||
</q-tab-panel>
|
|
||||||
<q-tab-panel name="drink_types">
|
|
||||||
<drink-types />
|
|
||||||
</q-tab-panel>
|
|
||||||
<q-tab-panel name="tags">
|
|
||||||
<tags />
|
|
||||||
</q-tab-panel>
|
|
||||||
</q-tab-panels>
|
|
||||||
</q-page>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, onBeforeMount, ref } from 'vue';
|
|
||||||
import { Screen } from 'quasar';
|
|
||||||
import DrinkTypes from 'src/plugins/pricelist/components/DrinkTypes.vue';
|
|
||||||
import CalculationTable from 'src/plugins/pricelist/components/CalculationTable.vue';
|
|
||||||
import ExtraIngredients from 'src/plugins/pricelist/components/ExtraIngredients.vue';
|
|
||||||
import Tags from '../components/Tags.vue';
|
|
||||||
import { usePricelistStore } from 'src/plugins/pricelist/store';
|
|
||||||
import { hasPermissions } from 'src/utils/permission';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'Settings',
|
|
||||||
components: { ExtraIngredients, DrinkTypes, Tags, CalculationTable },
|
|
||||||
setup() {
|
|
||||||
interface Tab {
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
permissions: Array<string>;
|
|
||||||
}
|
|
||||||
const store = usePricelistStore();
|
|
||||||
onBeforeMount(() => {
|
|
||||||
store
|
|
||||||
.getExtraIngredients()
|
|
||||||
.then(() => {
|
|
||||||
console.log(store.extraIngredients);
|
|
||||||
})
|
|
||||||
.catch((err) => console.log(err));
|
|
||||||
void store.getTags();
|
|
||||||
void store.getDrinkTypes();
|
|
||||||
void store.getDrinks();
|
|
||||||
void store.get_min_prices();
|
|
||||||
});
|
|
||||||
|
|
||||||
const drawer = ref<boolean>(false);
|
|
||||||
|
|
||||||
const showDrawer = computed({
|
|
||||||
get: () => {
|
|
||||||
return !Screen.gt.sm && drawer.value;
|
|
||||||
},
|
|
||||||
set: (val: boolean) => {
|
|
||||||
drawer.value = val;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const _tabs: Tab[] = [
|
|
||||||
{ name: 'pricelist', label: 'Getränke', permissions: ['drink_edit'] },
|
|
||||||
{
|
|
||||||
name: 'extra_ingredients',
|
|
||||||
label: 'Zutaten',
|
|
||||||
permissions: ['edit_ingredients', 'delete_ingredients'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'drink_types',
|
|
||||||
label: 'Getränketypen',
|
|
||||||
permissions: ['drink_type_edit', 'drink_type_delete'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tags',
|
|
||||||
label: 'Tags',
|
|
||||||
permissions: ['drink_tag_edit', 'drink_tag_create', 'drink_tag_delete'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const tabs = computed(() => {
|
|
||||||
const retVal: Tab[] = [];
|
|
||||||
_tabs.forEach((tab) => {
|
|
||||||
if (tab.permissions.length > 0) {
|
|
||||||
if (hasPermissions(tab.permissions)) {
|
|
||||||
retVal.push(tab);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (tab.permissions.length === 0) {
|
|
||||||
retVal.push(tab);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return retVal;
|
|
||||||
});
|
|
||||||
|
|
||||||
const tab = ref<string>('pricelist');
|
|
||||||
|
|
||||||
return { tabs, tab, showDrawer };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,33 +0,0 @@
|
||||||
export const PERMISSIONS = {
|
|
||||||
CREATE: 'drink_create',
|
|
||||||
|
|
||||||
EDIT: 'drink_edit',
|
|
||||||
|
|
||||||
DELETE: 'drink_delete',
|
|
||||||
|
|
||||||
CREATE_TAG: 'drink_tag_create',
|
|
||||||
|
|
||||||
EDIT_PRICE: 'edit_price',
|
|
||||||
DELETE_PRICE: 'delete_price',
|
|
||||||
|
|
||||||
EDIT_VOLUME: 'edit_volume',
|
|
||||||
DELETE_VOLUME: 'delete_volume',
|
|
||||||
|
|
||||||
EDIT_INGREDIENTS_DRINK: 'edit_ingredients_drink',
|
|
||||||
DELETE_INGREDIENTS_DRINK: 'delete_ingredients_drink',
|
|
||||||
|
|
||||||
EDIT_INGREDIENTS: 'edit_ingredients',
|
|
||||||
DELETE_INGREDIENTS: 'delete_ingredients',
|
|
||||||
|
|
||||||
EDIT_TAG: 'drink_tag_edit',
|
|
||||||
|
|
||||||
DELETE_TAG: 'drink_tag_delete',
|
|
||||||
|
|
||||||
CREATE_TYPE: 'drink_type_create',
|
|
||||||
|
|
||||||
EDIT_TYPE: 'drink_type_edit',
|
|
||||||
|
|
||||||
DELETE_TYPE: 'drink_type_delete',
|
|
||||||
|
|
||||||
EDIT_MIN_PRICES: 'edit_min_prices',
|
|
||||||
};
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { innerRoutes, outerRoutes } from './routes';
|
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
|
||||||
|
|
||||||
const plugin: FG_Plugin.Plugin = {
|
|
||||||
name: 'Pricelist',
|
|
||||||
innerRoutes,
|
|
||||||
outerRoutes,
|
|
||||||
requiredModules: [],
|
|
||||||
requiredBackendModules: ['pricelist'],
|
|
||||||
version: '0.0.1',
|
|
||||||
widgets: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default plugin;
|
|
|
@ -1,73 +0,0 @@
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
|
||||||
|
|
||||||
export const innerRoutes: FG_Plugin.MenuRoute[] = [
|
|
||||||
{
|
|
||||||
title: 'Getränke',
|
|
||||||
icon: 'mdi-glass-mug-variant',
|
|
||||||
route: {
|
|
||||||
path: 'drinks',
|
|
||||||
name: 'drinks',
|
|
||||||
redirect: { name: 'drinks-pricelist' },
|
|
||||||
},
|
|
||||||
permissions: ['user'],
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
title: 'Preisliste',
|
|
||||||
icon: 'mdi-cash-100',
|
|
||||||
shortcut: true,
|
|
||||||
permissions: ['user'],
|
|
||||||
route: {
|
|
||||||
path: 'pricelist',
|
|
||||||
name: 'drinks-pricelist',
|
|
||||||
component: () => import('../pages/InnerPricelist.vue'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Rezepte',
|
|
||||||
shortcut: false,
|
|
||||||
icon: 'mdi-receipt',
|
|
||||||
permissions: ['user'],
|
|
||||||
route: {
|
|
||||||
path: 'reciepts',
|
|
||||||
name: 'reciepts',
|
|
||||||
component: () => import('../pages/Receipts.vue'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Cocktailbuilder',
|
|
||||||
shortcut: false,
|
|
||||||
icon: 'mdi-glass-cocktail',
|
|
||||||
permissions: ['user'],
|
|
||||||
route: {
|
|
||||||
path: 'cocktail-builder',
|
|
||||||
name: 'cocktail-builder',
|
|
||||||
component: () => import('../pages/CocktailBuilder.vue'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Einstellungen',
|
|
||||||
icon: 'mdi-coffee-to-go',
|
|
||||||
shortcut: false,
|
|
||||||
permissions: ['drink_edit', 'drink_tag_edit'],
|
|
||||||
route: {
|
|
||||||
path: 'settings',
|
|
||||||
name: 'drinks-settings',
|
|
||||||
component: () => import('../pages/Settings.vue'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const outerRoutes: FG_Plugin.MenuRoute[] = [
|
|
||||||
{
|
|
||||||
title: 'Preisliste',
|
|
||||||
icon: 'mdi-glass-mug-variant',
|
|
||||||
shortcut: true,
|
|
||||||
route: {
|
|
||||||
path: 'pricelist',
|
|
||||||
name: 'outter-pricelist',
|
|
||||||
component: () => import('../pages/OuterPricelist.vue'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
|
@ -1,313 +0,0 @@
|
||||||
import { api } from 'src/boot/axios';
|
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
import {
|
|
||||||
calc_volume,
|
|
||||||
calc_cost_per_volume,
|
|
||||||
calc_all_min_prices,
|
|
||||||
} from 'src/plugins/pricelist/utils/utils';
|
|
||||||
|
|
||||||
interface DrinkPriceVolume extends Omit<FG.DrinkPriceVolume, 'volume'> {
|
|
||||||
_volume: number;
|
|
||||||
volume?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Drink extends Omit<Omit<FG.Drink, 'cost_per_volume'>, 'volumes'> {
|
|
||||||
volumes: DrinkPriceVolume[];
|
|
||||||
cost_per_volume?: number;
|
|
||||||
_cost_per_volume?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Pricelist {
|
|
||||||
name: string;
|
|
||||||
type: FG.DrinkType;
|
|
||||||
tags: Array<FG.Tag>;
|
|
||||||
volume: number;
|
|
||||||
price: number;
|
|
||||||
public: boolean;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DrinkPriceVolume implements DrinkPriceVolume {
|
|
||||||
constructor({ id, volume, prices, ingredients }: FG.DrinkPriceVolume) {
|
|
||||||
this.id = id;
|
|
||||||
this._volume = volume;
|
|
||||||
this.prices = prices;
|
|
||||||
this.ingredients = ingredients;
|
|
||||||
this.min_prices = [];
|
|
||||||
this.volume = calc_volume(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Drink {
|
|
||||||
constructor({
|
|
||||||
id,
|
|
||||||
article_id,
|
|
||||||
package_size,
|
|
||||||
name,
|
|
||||||
volume,
|
|
||||||
cost_per_volume,
|
|
||||||
cost_per_package,
|
|
||||||
tags,
|
|
||||||
type,
|
|
||||||
uuid,
|
|
||||||
receipt,
|
|
||||||
}: FG.Drink) {
|
|
||||||
this.id = id;
|
|
||||||
this.article_id = article_id;
|
|
||||||
this.package_size = package_size;
|
|
||||||
this.name = name;
|
|
||||||
this.volume = volume;
|
|
||||||
this.cost_per_package = cost_per_package;
|
|
||||||
this._cost_per_volume = cost_per_volume;
|
|
||||||
this.cost_per_volume = calc_cost_per_volume(this);
|
|
||||||
this.tags = tags;
|
|
||||||
this.type = type;
|
|
||||||
this.volumes = [];
|
|
||||||
this.uuid = uuid;
|
|
||||||
this.receipt = receipt || [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Order {
|
|
||||||
label: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const usePricelistStore = defineStore({
|
|
||||||
id: 'pricelist',
|
|
||||||
|
|
||||||
state: () => ({
|
|
||||||
drinkTypes: [] as Array<FG.DrinkType>,
|
|
||||||
drinks: [] as Array<Drink>,
|
|
||||||
extraIngredients: [] as Array<FG.ExtraIngredient>,
|
|
||||||
min_prices: [] as Array<number>,
|
|
||||||
tags: [] as Array<FG.Tag>,
|
|
||||||
pricecalc_columns: [] as Array<string>,
|
|
||||||
pricelist_view: false as boolean,
|
|
||||||
pricelist_columns_order: [] as Array<Order>,
|
|
||||||
}),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
async getDrinkTypes(force = false) {
|
|
||||||
if (force || this.drinks.length == 0) {
|
|
||||||
const { data } = await api.get<Array<FG.DrinkType>>('/pricelist/drink-types');
|
|
||||||
this.drinkTypes = data;
|
|
||||||
}
|
|
||||||
return this.drinkTypes;
|
|
||||||
},
|
|
||||||
async addDrinkType(name: string) {
|
|
||||||
const { data } = await api.post<FG.DrinkType>('/pricelist/drink-types', { name: name });
|
|
||||||
this.drinkTypes.push(data);
|
|
||||||
},
|
|
||||||
async removeDrinkType(id: number) {
|
|
||||||
await api.delete(`/pricelist/drink-types/${id}`);
|
|
||||||
const idx = this.drinkTypes.findIndex((val) => val.id == id);
|
|
||||||
if (idx >= 0) this.drinkTypes.splice(idx, 1);
|
|
||||||
},
|
|
||||||
async changeDrinkTypeName(drinkType: FG.DrinkType) {
|
|
||||||
await api.put(`/pricelist/drink-types/${drinkType.id}`, drinkType);
|
|
||||||
const itm = this.drinkTypes.filter((val) => val.id == drinkType.id);
|
|
||||||
if (itm.length > 0) itm[0].name = drinkType.name;
|
|
||||||
},
|
|
||||||
async getExtraIngredients() {
|
|
||||||
const { data } = await api.get<Array<FG.ExtraIngredient>>(
|
|
||||||
'pricelist/ingredients/extraIngredients'
|
|
||||||
);
|
|
||||||
this.extraIngredients = data;
|
|
||||||
},
|
|
||||||
async setExtraIngredient(ingredient: FG.ExtraIngredient) {
|
|
||||||
const { data } = await api.post<FG.ExtraIngredient>(
|
|
||||||
'pricelist/ingredients/extraIngredients',
|
|
||||||
ingredient
|
|
||||||
);
|
|
||||||
this.extraIngredients.push(data);
|
|
||||||
},
|
|
||||||
async updateExtraIngredient(ingredient: FG.ExtraIngredient) {
|
|
||||||
const { data } = await api.put<FG.ExtraIngredient>(
|
|
||||||
`pricelist/ingredients/extraIngredients/${ingredient.id}`,
|
|
||||||
ingredient
|
|
||||||
);
|
|
||||||
const index = this.extraIngredients.findIndex((a) => a.id === ingredient.id);
|
|
||||||
if (index > -1) {
|
|
||||||
this.extraIngredients[index] = data;
|
|
||||||
} else {
|
|
||||||
this.extraIngredients.push(data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async deleteExtraIngredient(ingredient: FG.ExtraIngredient) {
|
|
||||||
await api.delete(`pricelist/ingredients/extraIngredients/${ingredient.id}`);
|
|
||||||
const index = this.extraIngredients.findIndex((a) => a.id === ingredient.id);
|
|
||||||
if (index > -1) {
|
|
||||||
this.extraIngredients.splice(index, 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async getDrinks() {
|
|
||||||
const { data } = await api.get<Array<FG.Drink>>('pricelist/drinks');
|
|
||||||
this.drinks = [];
|
|
||||||
data.forEach((drink) => {
|
|
||||||
const _drink = new Drink(drink);
|
|
||||||
drink.volumes.forEach((volume) => {
|
|
||||||
const _volume = new DrinkPriceVolume(volume);
|
|
||||||
_drink.volumes.push(_volume);
|
|
||||||
});
|
|
||||||
this.drinks.push(_drink);
|
|
||||||
});
|
|
||||||
calc_all_min_prices(this.drinks, this.min_prices);
|
|
||||||
},
|
|
||||||
sortPrices(volume: DrinkPriceVolume) {
|
|
||||||
volume.prices.sort((a, b) => {
|
|
||||||
if (a.price > b.price) return 1;
|
|
||||||
if (b.price > a.price) return -1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async deletePrice(price: FG.DrinkPrice) {
|
|
||||||
await api.delete(`pricelist/prices/${price.id}`);
|
|
||||||
},
|
|
||||||
async deleteVolume(volume: DrinkPriceVolume, drink: Drink) {
|
|
||||||
await api.delete(`pricelist/volumes/${volume.id}`);
|
|
||||||
const index = drink.volumes.findIndex((a) => a.id === volume.id);
|
|
||||||
if (index > -1) {
|
|
||||||
drink.volumes.splice(index, 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async deleteIngredient(ingredient: FG.Ingredient) {
|
|
||||||
await api.delete(`pricelist/ingredients/${ingredient.id}`);
|
|
||||||
},
|
|
||||||
async setDrink(drink: Drink) {
|
|
||||||
const { data } = await api.post<FG.Drink>('pricelist/drinks', {
|
|
||||||
...drink,
|
|
||||||
});
|
|
||||||
const _drink = new Drink(data);
|
|
||||||
data.volumes.forEach((volume) => {
|
|
||||||
const _volume = new DrinkPriceVolume(volume);
|
|
||||||
_drink.volumes.push(_volume);
|
|
||||||
});
|
|
||||||
this.drinks.push(_drink);
|
|
||||||
calc_all_min_prices(this.drinks, this.min_prices);
|
|
||||||
return _drink;
|
|
||||||
},
|
|
||||||
async updateDrink(drink: Drink) {
|
|
||||||
const { data } = await api.put<FG.Drink>(`pricelist/drinks/${drink.id}`, {
|
|
||||||
...drink,
|
|
||||||
});
|
|
||||||
const index = this.drinks.findIndex((a) => a.id === data.id);
|
|
||||||
if (index > -1) {
|
|
||||||
const _drink = new Drink(data);
|
|
||||||
data.volumes.forEach((volume) => {
|
|
||||||
const _volume = new DrinkPriceVolume(volume);
|
|
||||||
_drink.volumes.push(_volume);
|
|
||||||
});
|
|
||||||
this.drinks[index] = _drink;
|
|
||||||
}
|
|
||||||
calc_all_min_prices(this.drinks, this.min_prices);
|
|
||||||
},
|
|
||||||
deleteDrink(drink: Drink) {
|
|
||||||
api
|
|
||||||
.delete(`pricelist/drinks/${drink.id}`)
|
|
||||||
.then(() => {
|
|
||||||
const index = this.drinks.findIndex((a) => a.id === drink.id);
|
|
||||||
if (index > -1) {
|
|
||||||
this.drinks.splice(index, 1);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => console.warn(err));
|
|
||||||
},
|
|
||||||
async get_min_prices() {
|
|
||||||
const { data } = await api.get<Array<number>>('pricelist/settings/min_prices');
|
|
||||||
this.min_prices = data;
|
|
||||||
},
|
|
||||||
async set_min_prices() {
|
|
||||||
await api.post<Array<number>>('pricelist/settings/min_prices', this.min_prices);
|
|
||||||
calc_all_min_prices(this.drinks, this.min_prices);
|
|
||||||
},
|
|
||||||
async upload_drink_picture(drink: Drink, file: File) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
const { data } = await api.post<FG.Drink>(`pricelist/drinks/${drink.id}/picture`, formData, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const _drink = this.drinks.find((a) => a.id === drink.id);
|
|
||||||
if (_drink) {
|
|
||||||
_drink.uuid = data.uuid;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async delete_drink_picture(drink: Drink) {
|
|
||||||
await api.delete(`pricelist/drinks/${drink.id}/picture`);
|
|
||||||
drink.uuid = '';
|
|
||||||
},
|
|
||||||
async getTags() {
|
|
||||||
const { data } = await api.get<Array<FG.Tag>>('/pricelist/tags');
|
|
||||||
this.tags = data;
|
|
||||||
},
|
|
||||||
async setTag(tag: FG.Tag) {
|
|
||||||
const { data } = await api.post<FG.Tag>('/pricelist/tags', tag);
|
|
||||||
this.tags.push(data);
|
|
||||||
},
|
|
||||||
async updateTag(tag: FG.Tag) {
|
|
||||||
const { data } = await api.put<FG.Tag>(`/pricelist/tags/${tag.id}`, tag);
|
|
||||||
const index = this.tags.findIndex((a) => a.id === data.id);
|
|
||||||
if (index > -1) {
|
|
||||||
this.tags[index] = data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async deleteTag(tag: FG.Tag) {
|
|
||||||
await api.delete(`/pricelist/tags/${tag.id}`);
|
|
||||||
const index = this.tags.findIndex((a) => a.id === tag.id);
|
|
||||||
if (index > -1) {
|
|
||||||
this.tags.splice(index, 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async getPriceCalcColumn(userid: string) {
|
|
||||||
const { data } = await api.get<Array<string>>(`pricelist/users/${userid}/pricecalc_columns`);
|
|
||||||
this.pricecalc_columns = data;
|
|
||||||
},
|
|
||||||
async updatePriceCalcColumn(userid: string, data: Array<string>) {
|
|
||||||
await api.put<Array<string>>(`pricelist/users/${userid}/pricecalc_columns`, data);
|
|
||||||
this.pricecalc_columns = data;
|
|
||||||
},
|
|
||||||
async getPriceListView(userid: string) {
|
|
||||||
const { data } = await api.get<{ value: boolean }>(`pricelist/users/${userid}/pricelist`);
|
|
||||||
this.pricelist_view = data.value;
|
|
||||||
},
|
|
||||||
async updatePriceListView(userid: string, data: boolean) {
|
|
||||||
await api.put<Array<string>>(`pricelist/users/${userid}/pricelist`, { value: data });
|
|
||||||
this.pricelist_view = data;
|
|
||||||
},
|
|
||||||
async getPriceListColumnOrder(userid: string) {
|
|
||||||
const { data } = await api.get<Array<Order>>(
|
|
||||||
`pricelist/users/${userid}/pricecalc_columns_order`
|
|
||||||
);
|
|
||||||
this.pricelist_columns_order = data;
|
|
||||||
},
|
|
||||||
async updatePriceListColumnOrder(userid: string, data: Array<Order>) {
|
|
||||||
await api.put<Array<string>>(`pricelist/users/${userid}/pricecalc_columns_order`, data);
|
|
||||||
this.pricelist_columns_order = data;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
getters: {
|
|
||||||
pricelist() {
|
|
||||||
const retVal: Array<Pricelist> = [];
|
|
||||||
this.drinks.forEach((drink) => {
|
|
||||||
drink.volumes.forEach((volume) => {
|
|
||||||
volume.prices.forEach((price) => {
|
|
||||||
retVal.push({
|
|
||||||
name: drink.name,
|
|
||||||
type: <FG.DrinkType>drink.type,
|
|
||||||
tags: <Array<FG.Tag>>drink.tags,
|
|
||||||
volume: <number>volume.volume,
|
|
||||||
price: price.price,
|
|
||||||
public: price.public,
|
|
||||||
description: <string>price.description,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return retVal;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export { DrinkPriceVolume, Drink, Order };
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { Drink } from '../store';
|
|
||||||
|
|
||||||
function filter(
|
|
||||||
rows: Array<Drink>,
|
|
||||||
terms: Search,
|
|
||||||
cols: Array<Col>,
|
|
||||||
cellValue: { (col: Col, row: Drink): string }
|
|
||||||
) {
|
|
||||||
if (terms.value) {
|
|
||||||
return rows.filter((row) => {
|
|
||||||
if (!terms.key || terms.key === '') {
|
|
||||||
return cols.some((col) => {
|
|
||||||
const val = cellValue(col, row) + '';
|
|
||||||
const haystack = val === 'undefined' || val === 'null' ? '' : val.toLowerCase();
|
|
||||||
return haystack.indexOf(terms.value) !== -1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const index = cols.findIndex((col) => col.name === terms.key);
|
|
||||||
const val = cellValue(cols[index], row) + '';
|
|
||||||
const haystack = val === 'undefined' || val === 'null' ? '' : val.toLowerCase();
|
|
||||||
return haystack.indexOf(terms.value) !== -1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Search {
|
|
||||||
value: string;
|
|
||||||
label: string | undefined;
|
|
||||||
key: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Col {
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
field: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { filter, Search, Col };
|
|
|
@ -1,7 +0,0 @@
|
||||||
function sort(a: string | number, b: string | number) {
|
|
||||||
if (a > b) return 1;
|
|
||||||
if (b > a) return -1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { sort };
|
|
|
@ -1,84 +0,0 @@
|
||||||
import { Drink, DrinkPriceVolume, usePricelistStore } from 'src/plugins/pricelist/store';
|
|
||||||
|
|
||||||
function calc_volume(volume: DrinkPriceVolume) {
|
|
||||||
if (volume.ingredients.some((ingredient) => !!ingredient.drink_ingredient)) {
|
|
||||||
let retVal = 0;
|
|
||||||
volume.ingredients.forEach((ingredient) => {
|
|
||||||
if (ingredient.drink_ingredient?.volume) {
|
|
||||||
retVal += ingredient.drink_ingredient.volume;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return retVal;
|
|
||||||
} else {
|
|
||||||
return volume._volume;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function calc_cost_per_volume(drink: Drink) {
|
|
||||||
let retVal = drink._cost_per_volume;
|
|
||||||
if (!!drink.volume && !!drink.package_size && !!drink.cost_per_package) {
|
|
||||||
retVal =
|
|
||||||
((drink.cost_per_package || 0) / ((drink.volume || 0) * (drink.package_size || 0))) * 1.19;
|
|
||||||
}
|
|
||||||
|
|
||||||
return retVal ? Math.round(retVal * 1000) / 1000 : retVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calc_all_min_prices(drinks: Array<Drink>, min_prices: Array<number>) {
|
|
||||||
drinks.forEach((drink) => {
|
|
||||||
drink.volumes.forEach((volume) => {
|
|
||||||
volume.min_prices = calc_min_prices(volume, drink.cost_per_volume, min_prices);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function helper(volume: DrinkPriceVolume, min_price: number) {
|
|
||||||
let retVal = 0;
|
|
||||||
let extraIngredientPrice = 0;
|
|
||||||
volume.ingredients.forEach((ingredient) => {
|
|
||||||
if (ingredient.drink_ingredient) {
|
|
||||||
const _drink = usePricelistStore().drinks.find(
|
|
||||||
(a) => a.id === ingredient.drink_ingredient?.ingredient_id
|
|
||||||
);
|
|
||||||
retVal += ingredient.drink_ingredient.volume * <number>(<unknown>_drink?.cost_per_volume);
|
|
||||||
}
|
|
||||||
if (ingredient.extra_ingredient) {
|
|
||||||
extraIngredientPrice += ingredient.extra_ingredient.price;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return (retVal * min_price) / 100 + extraIngredientPrice;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calc_min_prices(
|
|
||||||
volume: DrinkPriceVolume,
|
|
||||||
cost_per_volume: number | undefined,
|
|
||||||
min_prices: Array<number>
|
|
||||||
) {
|
|
||||||
const retVal: Array<FG.MinPrices> = [];
|
|
||||||
volume.min_prices = [];
|
|
||||||
if (min_prices) {
|
|
||||||
min_prices.forEach((min_price) => {
|
|
||||||
let computedMinPrice: number;
|
|
||||||
if (cost_per_volume) {
|
|
||||||
computedMinPrice = (cost_per_volume * <number>volume.volume * min_price) / 100;
|
|
||||||
} else {
|
|
||||||
computedMinPrice = helper(volume, min_price);
|
|
||||||
}
|
|
||||||
retVal.push({ percentage: min_price, price: computedMinPrice });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return retVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clone<T>(o: T): T {
|
|
||||||
return <T>JSON.parse(JSON.stringify(o));
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteObjects {
|
|
||||||
prices: Array<FG.DrinkPrice>;
|
|
||||||
volumes: Array<DrinkPriceVolume>;
|
|
||||||
ingredients: Array<FG.Ingredient>;
|
|
||||||
}
|
|
||||||
export { DeleteObjects };
|
|
||||||
|
|
||||||
export { calc_volume, calc_cost_per_volume, calc_all_min_prices, calc_min_prices, clone };
|
|
|
@ -1,25 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card class="row justify-center content-center" style="text-align: center">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6 col-12">Dienste diesen Monat: {{ jobs }}</div>
|
|
||||||
<!--TODO: Filters are deprecated! -->
|
|
||||||
<!--<div class="text-h6 col-12">Nächster Dienst: {{ nextJob | date }}</div>-->
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'DummyWidget',
|
|
||||||
setup() {
|
|
||||||
function randomNumber(start: number, end: number) {
|
|
||||||
return start + Math.floor(Math.random() * Math.floor(end));
|
|
||||||
}
|
|
||||||
const jobs = randomNumber(0, 5);
|
|
||||||
const nextJob = new Date(2021, randomNumber(1, 12), randomNumber(1, 31));
|
|
||||||
return { jobs, nextJob };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,245 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card>
|
|
||||||
<q-form @submit="save()" @reset="reset">
|
|
||||||
<q-card-section class="fit row justify-start content-center items-center">
|
|
||||||
<div class="text-h6 col-xs-12 col-sm-6 q-pa-sm">
|
|
||||||
Veranstaltung <template v-if="modelValue">bearbeiten</template
|
|
||||||
><template v-else>erstellen</template>
|
|
||||||
</div>
|
|
||||||
<q-select
|
|
||||||
:model-value="template"
|
|
||||||
filled
|
|
||||||
label="Vorlage"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
:options="templates"
|
|
||||||
option-label="name"
|
|
||||||
map-options
|
|
||||||
clearable
|
|
||||||
:disable="templates.length == 0"
|
|
||||||
@update:modelValue="fromTemplate"
|
|
||||||
@clear="reset()"
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-model="event.name"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="Name"
|
|
||||||
type="text"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
<q-select
|
|
||||||
v-model="event.type"
|
|
||||||
filled
|
|
||||||
use-input
|
|
||||||
label="Veranstaltungstyp"
|
|
||||||
input-debounce="0"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
:options="eventtypes"
|
|
||||||
option-label="name"
|
|
||||||
option-value="id"
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
clearable
|
|
||||||
:rules="[notEmpty]"
|
|
||||||
/>
|
|
||||||
<IsoDateInput
|
|
||||||
v-model="event.start"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="Veranstaltungsbeginn"
|
|
||||||
:rules="[notEmpty]"
|
|
||||||
/>
|
|
||||||
<IsoDateInput
|
|
||||||
v-model="event.end"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="Veranstaltungsende"
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-model="event.description"
|
|
||||||
class="col-12 q-pa-sm"
|
|
||||||
label="Beschreibung"
|
|
||||||
type="textarea"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section v-if="event.template_id === undefined && modelValue === undefined">
|
|
||||||
<q-btn-toggle
|
|
||||||
v-model="recurrent"
|
|
||||||
spread
|
|
||||||
no-caps
|
|
||||||
:options="[
|
|
||||||
{ label: 'Einmalig', value: false },
|
|
||||||
{ label: 'Wiederkehrend', value: true },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
<RecurrenceRule v-if="!!recurrent" v-model="recurrenceRule" />
|
|
||||||
</q-card-section>
|
|
||||||
<q-separator />
|
|
||||||
<q-card-section>
|
|
||||||
<q-btn color="primary" label="Schicht hinzufügen" @click="addJob()" />
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section v-for="(job, index) in event.jobs" :key="index">
|
|
||||||
<q-card class="q-my-auto">
|
|
||||||
<job
|
|
||||||
v-model="event.jobs[index]"
|
|
||||||
:job-can-delete="jobDeleteDisabled"
|
|
||||||
@remove-job="removeJob(index)"
|
|
||||||
/>
|
|
||||||
</q-card>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-actions align="around">
|
|
||||||
<q-card-actions align="left">
|
|
||||||
<q-btn v-if="!template" color="secondary" label="Neue Vorlage" @click="save(true)" />
|
|
||||||
<q-btn v-else color="negative" label="Vorlage löschen" @click="removeTemplate" />
|
|
||||||
</q-card-actions>
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<q-btn label="Zurücksetzen" type="reset" />
|
|
||||||
<q-btn color="primary" type="submit" label="Speichern" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, PropType, ref, onBeforeMount } from 'vue';
|
|
||||||
import { date, ModifyDateOptions } from 'quasar';
|
|
||||||
import { useScheduleStore } from '../../store';
|
|
||||||
import { notEmpty } from 'src/utils/validators';
|
|
||||||
import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
|
|
||||||
import Job from './Job.vue';
|
|
||||||
import RecurrenceRule from './RecurrenceRule.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'EditEvent',
|
|
||||||
components: { IsoDateInput, Job, RecurrenceRule },
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
required: false,
|
|
||||||
default: () => undefined,
|
|
||||||
type: Object as PropType<FG.Event | undefined>,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: {
|
|
||||||
done: (val: boolean) => typeof val === 'boolean',
|
|
||||||
},
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const store = useScheduleStore();
|
|
||||||
|
|
||||||
const emptyJob = {
|
|
||||||
id: NaN,
|
|
||||||
start: new Date(),
|
|
||||||
end: date.addToDate(new Date(), { hours: 1 }),
|
|
||||||
services: [],
|
|
||||||
required_services: 2,
|
|
||||||
type: store.jobTypes[0],
|
|
||||||
};
|
|
||||||
|
|
||||||
const emptyEvent = {
|
|
||||||
id: NaN,
|
|
||||||
start: new Date(),
|
|
||||||
jobs: [Object.assign({}, emptyJob)],
|
|
||||||
type: store.eventTypes[0],
|
|
||||||
is_template: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const templates = computed(() => store.templates);
|
|
||||||
const template = ref<FG.Event | undefined>(undefined);
|
|
||||||
const event = ref<FG.Event>(props.modelValue || Object.assign({}, emptyEvent));
|
|
||||||
const eventtypes = computed(() => store.eventTypes);
|
|
||||||
const jobDeleteDisabled = computed(() => event.value.jobs.length < 2);
|
|
||||||
const recurrent = ref(false);
|
|
||||||
const recurrenceRule = ref<FG.RecurrenceRule>({ frequency: 'daily', interval: 1 });
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
void store.getEventTypes();
|
|
||||||
void store.getJobTypes();
|
|
||||||
void store.getTemplates();
|
|
||||||
});
|
|
||||||
|
|
||||||
function addJob() {
|
|
||||||
event.value.jobs.push(Object.assign({}, emptyJob));
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeJob(index: number) {
|
|
||||||
event.value.jobs.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fromTemplate(tpl: FG.Event) {
|
|
||||||
template.value = tpl;
|
|
||||||
event.value = Object.assign({}, tpl);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function save(template = false) {
|
|
||||||
event.value.is_template = template;
|
|
||||||
try {
|
|
||||||
await store.addEvent(event.value);
|
|
||||||
if (props.modelValue === undefined && recurrent.value && !event.value.is_template) {
|
|
||||||
let count = 0;
|
|
||||||
const options: ModifyDateOptions = {};
|
|
||||||
switch (recurrenceRule.value.frequency) {
|
|
||||||
case 'daily':
|
|
||||||
options['days'] = 1 * recurrenceRule.value.interval;
|
|
||||||
break;
|
|
||||||
case 'weekly':
|
|
||||||
options['days'] = 7 * recurrenceRule.value.interval;
|
|
||||||
break;
|
|
||||||
case 'monthly':
|
|
||||||
options['months'] = 1 * recurrenceRule.value.interval;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
while (true) {
|
|
||||||
event.value.start = date.addToDate(event.value.start, options);
|
|
||||||
if (event.value.end) event.value.end = date.addToDate(event.value.end, options);
|
|
||||||
event.value.jobs.forEach((job) => {
|
|
||||||
job.start = date.addToDate(job.start, options);
|
|
||||||
if (job.end) job.end = date.addToDate(job.end, options);
|
|
||||||
});
|
|
||||||
count++;
|
|
||||||
if (
|
|
||||||
count <= 120 &&
|
|
||||||
(!recurrenceRule.value.count || count <= recurrenceRule.value.count) &&
|
|
||||||
(!recurrenceRule.value.until || event.value.start < recurrenceRule.value.until)
|
|
||||||
)
|
|
||||||
await store.addEvent(event.value);
|
|
||||||
else break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reset();
|
|
||||||
emit('done', true);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeTemplate() {
|
|
||||||
if (template.value !== undefined) {
|
|
||||||
await store.removeEvent(template.value.id);
|
|
||||||
template.value = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
event.value = Object.assign({}, props.modelValue || emptyEvent);
|
|
||||||
template.value = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
jobDeleteDisabled,
|
|
||||||
addJob,
|
|
||||||
eventtypes,
|
|
||||||
templates,
|
|
||||||
removeJob,
|
|
||||||
notEmpty,
|
|
||||||
save,
|
|
||||||
reset,
|
|
||||||
recurrent,
|
|
||||||
fromTemplate,
|
|
||||||
removeTemplate,
|
|
||||||
template,
|
|
||||||
recurrenceRule,
|
|
||||||
event,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
|
@ -1,126 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<q-dialog v-model="edittype">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">Editere Diensttyp {{ actualEvent.name }}</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<q-input v-model="newEventName" dense label="name" filled />
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-actions>
|
|
||||||
<q-btn flat color="danger" label="Abbrechen" @click="discardChanges()" />
|
|
||||||
<q-btn flat color="primary" label="Speichern" @click="saveChanges()" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<q-table title="Veranstaltungstypen" :rows="rows" row-key="jobid" :columns="columns">
|
|
||||||
<template #top-right>
|
|
||||||
<q-input v-model="newEventType" dense placeholder="Neuer Typ" />
|
|
||||||
|
|
||||||
<div></div>
|
|
||||||
<q-btn color="primary" icon="mdi-plus" label="Hinzufügen" @click="addType" />
|
|
||||||
</template>
|
|
||||||
<template #body-cell-actions="props">
|
|
||||||
<!-- <q-btn :label="item"> -->
|
|
||||||
<!-- {{ item.row.name }} -->
|
|
||||||
<q-td :props="props" align="right" :auto-width="true">
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
icon="mdi-pencil"
|
|
||||||
@click="editType({ id: props.row.id, name: props.row.name })"
|
|
||||||
/>
|
|
||||||
<q-btn round icon="mdi-delete" @click="deleteType(props.row.id)" />
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, ref, computed, onBeforeMount } from 'vue';
|
|
||||||
import { useScheduleStore } from '../../store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'EventTypes',
|
|
||||||
components: {},
|
|
||||||
setup() {
|
|
||||||
const store = useScheduleStore();
|
|
||||||
const newEventType = ref('');
|
|
||||||
const edittype = ref(false);
|
|
||||||
const emptyEvent: FG.EventType = { id: -1, name: '' };
|
|
||||||
const actualEvent = ref(emptyEvent);
|
|
||||||
const newEventName = ref('');
|
|
||||||
|
|
||||||
onBeforeMount(async () => await store.getEventTypes());
|
|
||||||
|
|
||||||
const rows = computed(() => store.eventTypes);
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
label: 'Veranstaltungstyp',
|
|
||||||
field: 'name',
|
|
||||||
align: 'left',
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'actions',
|
|
||||||
label: 'Aktionen',
|
|
||||||
field: 'actions',
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
async function addType() {
|
|
||||||
await store.addEventType(newEventType.value);
|
|
||||||
// if null then conflict with name
|
|
||||||
newEventType.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function editType(event: FG.EventType) {
|
|
||||||
edittype.value = true;
|
|
||||||
actualEvent.value = event;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveChanges() {
|
|
||||||
try {
|
|
||||||
await store.renameEventType(actualEvent.value.id, newEventName.value);
|
|
||||||
} finally {
|
|
||||||
discardChanges();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function discardChanges() {
|
|
||||||
actualEvent.value = emptyEvent;
|
|
||||||
newEventName.value = '';
|
|
||||||
edittype.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteType(id: number) {
|
|
||||||
await store.removeEventType(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
columns,
|
|
||||||
rows,
|
|
||||||
addType,
|
|
||||||
newEventType,
|
|
||||||
deleteType,
|
|
||||||
edittype,
|
|
||||||
editType,
|
|
||||||
actualEvent,
|
|
||||||
newEventName,
|
|
||||||
discardChanges,
|
|
||||||
saveChanges,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,113 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card-section class="fit row justify-start content-center items-center">
|
|
||||||
<q-card-section class="fit row justify-start content-center items-center">
|
|
||||||
<IsoDateInput
|
|
||||||
v-model="job.start"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="Beginn"
|
|
||||||
type="datetime"
|
|
||||||
:rules="[notEmpty]"
|
|
||||||
/>
|
|
||||||
<IsoDateInput
|
|
||||||
v-model="job.end"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="Ende"
|
|
||||||
type="datetime"
|
|
||||||
:rules="[notEmpty, isAfterDate]"
|
|
||||||
/>
|
|
||||||
<q-select
|
|
||||||
v-model="job.type"
|
|
||||||
filled
|
|
||||||
use-input
|
|
||||||
label="Dienstart"
|
|
||||||
input-debounce="0"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
:options="jobtypes"
|
|
||||||
option-label="name"
|
|
||||||
option-value="id"
|
|
||||||
map-options
|
|
||||||
clearable
|
|
||||||
:rules="[notEmpty]"
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-model="job.required_services"
|
|
||||||
filled
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="Dienstanzahl"
|
|
||||||
type="number"
|
|
||||||
:rules="[notEmpty]"
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-model="job.comment"
|
|
||||||
class="col-12 q-pa-sm"
|
|
||||||
label="Beschreibung"
|
|
||||||
type="textarea"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
<q-btn label="Schicht löschen" color="negative" :disabled="jobCanDelete" @click="removeJob" />
|
|
||||||
</q-card-section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, computed, onBeforeMount, PropType } from 'vue';
|
|
||||||
import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
|
|
||||||
import { notEmpty } from 'src/utils/validators';
|
|
||||||
import { useScheduleStore } from '../../store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'Job',
|
|
||||||
components: { IsoDateInput },
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
required: true,
|
|
||||||
type: Object as PropType<FG.Job>,
|
|
||||||
},
|
|
||||||
jobCanDelete: Boolean,
|
|
||||||
},
|
|
||||||
emits: {
|
|
||||||
'remove-job': () => true,
|
|
||||||
'update:modelValue': (job: FG.Job) => !!job,
|
|
||||||
},
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const store = useScheduleStore();
|
|
||||||
|
|
||||||
onBeforeMount(() => store.getJobTypes());
|
|
||||||
|
|
||||||
const jobtypes = computed(() => store.jobTypes);
|
|
||||||
|
|
||||||
const job = new Proxy(props.modelValue, {
|
|
||||||
get(target, prop) {
|
|
||||||
if (typeof prop === 'string') {
|
|
||||||
return ((props.modelValue as unknown) as Record<string, unknown>)[prop];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
set(obj, prop, value) {
|
|
||||||
if (typeof prop === 'string') {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
emit('update:modelValue', Object.assign({}, props.modelValue, { [prop]: value }));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function removeJob() {
|
|
||||||
emit('remove-job');
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAfterDate(val: string) {
|
|
||||||
return props.modelValue.start < new Date(val) || 'Ende muss hinter dem Start liegen';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
job,
|
|
||||||
jobtypes,
|
|
||||||
removeJob,
|
|
||||||
notEmpty,
|
|
||||||
isAfterDate,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
|
@ -1,125 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<q-dialog v-model="edittype">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">Editere Diensttyp {{ actualJob.name }}</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<q-input v-model="newJobName" dense label="name" filled />
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-actions>
|
|
||||||
<q-btn flat color="danger" label="Abbrechen" @click="discardChanges()" />
|
|
||||||
<q-btn flat color="primary" label="Speichern" @click="saveChanges()" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<q-table title="Diensttypen" :rows="rows" row-key="jobid" :columns="columns">
|
|
||||||
<template #top-right>
|
|
||||||
<q-input v-model="newJob" dense placeholder="Neuer Typ" />
|
|
||||||
|
|
||||||
<div></div>
|
|
||||||
<q-btn color="primary" icon="mdi-plus" label="Hinzufügen" @click="addType" />
|
|
||||||
</template>
|
|
||||||
<template #body-cell-actions="props">
|
|
||||||
<!-- <q-btn :label="item"> -->
|
|
||||||
<!-- {{ item.row.name }} -->
|
|
||||||
<q-td :props="props" align="right" :auto-width="true">
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
icon="mdi-pencil"
|
|
||||||
@click="editType({ id: props.row.id, name: props.row.name })"
|
|
||||||
/>
|
|
||||||
<q-btn round icon="mdi-delete" @click="deleteType(props.row.id)" />
|
|
||||||
</q-td>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, ref, computed, onBeforeMount } from 'vue';
|
|
||||||
import { useScheduleStore } from '../../store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'JobTypes',
|
|
||||||
components: {},
|
|
||||||
setup() {
|
|
||||||
const store = useScheduleStore();
|
|
||||||
const newJob = ref('');
|
|
||||||
const edittype = ref(false);
|
|
||||||
const emptyJob: FG.JobType = { id: -1, name: '' };
|
|
||||||
const actualJob = ref(emptyJob);
|
|
||||||
const newJobName = ref('');
|
|
||||||
|
|
||||||
onBeforeMount(() => store.getJobTypes());
|
|
||||||
|
|
||||||
const rows = computed(() => store.jobTypes);
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'jobname',
|
|
||||||
label: 'Name',
|
|
||||||
field: 'name',
|
|
||||||
align: 'left',
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'actions',
|
|
||||||
label: 'Aktionen',
|
|
||||||
field: 'actions',
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
async function addType() {
|
|
||||||
await store.addJobType(newJob.value);
|
|
||||||
newJob.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function editType(job: FG.JobType) {
|
|
||||||
edittype.value = true;
|
|
||||||
actualJob.value = job;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveChanges() {
|
|
||||||
try {
|
|
||||||
await store.renameJobType(actualJob.value.id, newJobName.value);
|
|
||||||
} finally {
|
|
||||||
discardChanges();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function discardChanges() {
|
|
||||||
actualJob.value = emptyJob;
|
|
||||||
newJobName.value = '';
|
|
||||||
edittype.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteType(id: number) {
|
|
||||||
void store.removeJobType(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
columns,
|
|
||||||
rows,
|
|
||||||
addType,
|
|
||||||
newJob,
|
|
||||||
deleteType,
|
|
||||||
edittype,
|
|
||||||
editType,
|
|
||||||
actualJob,
|
|
||||||
newJobName,
|
|
||||||
discardChanges,
|
|
||||||
saveChanges,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,96 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card class="fit row justify-start content-center items-center">
|
|
||||||
<q-input
|
|
||||||
v-model="rule.interval"
|
|
||||||
filled
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="Interval"
|
|
||||||
type="number"
|
|
||||||
:rules="[notEmpty]"
|
|
||||||
/>
|
|
||||||
<q-select
|
|
||||||
v-model="rule.frequency"
|
|
||||||
filled
|
|
||||||
label="Wiederholung"
|
|
||||||
input-debounce="200"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
:options="freqTypes"
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-model="rule.count"
|
|
||||||
filled
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="Anzahl Wiederholungen"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
<IsoDateInput
|
|
||||||
v-model="rule.until"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="Wiederholen bis"
|
|
||||||
type="date"
|
|
||||||
/>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
|
|
||||||
import { defineComponent, PropType } from 'vue';
|
|
||||||
import { notEmpty } from 'src/utils/validators';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'RecurrenceRule',
|
|
||||||
components: { IsoDateInput },
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
required: true,
|
|
||||||
type: Object as PropType<FG.RecurrenceRule>,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: {
|
|
||||||
'update:modelValue': (rule: FG.RecurrenceRule) => !!rule,
|
|
||||||
},
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const freqTypes = [
|
|
||||||
{ label: 'Täglich', value: 'daily' },
|
|
||||||
{ label: 'Wöchentlich', value: 'weekly' },
|
|
||||||
{ label: 'Monatlich', value: 'monthly' },
|
|
||||||
{ label: 'Jährlich', value: 'yearly' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const rule = new Proxy(props.modelValue, {
|
|
||||||
get(target, prop) {
|
|
||||||
if (typeof prop === 'string') {
|
|
||||||
return ((props.modelValue as unknown) as Record<string, unknown>)[prop];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
set(target, prop, value) {
|
|
||||||
if (typeof prop === 'string') {
|
|
||||||
const obj = Object.assign({}, props.modelValue);
|
|
||||||
if (prop == 'frequency' && typeof value === 'string') obj.frequency = value;
|
|
||||||
else if (prop == 'interval') {
|
|
||||||
obj.interval = typeof value === 'string' ? parseInt(value) : <number>value;
|
|
||||||
} else if (prop == 'count') {
|
|
||||||
obj.until = undefined;
|
|
||||||
obj.count = typeof value === 'string' ? parseInt(value) : <number>value;
|
|
||||||
} else if (prop == 'until' && (value instanceof Date || value === undefined)) {
|
|
||||||
obj.count = undefined;
|
|
||||||
obj.until = <Date | undefined>value;
|
|
||||||
} else return false;
|
|
||||||
emit('update:modelValue', obj);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
rule,
|
|
||||||
notEmpty,
|
|
||||||
freqTypes,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
|
@ -1,241 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-dialog
|
|
||||||
:model-value="editor !== undefined"
|
|
||||||
persistent
|
|
||||||
transition-show="scale"
|
|
||||||
transition-hide="scale"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<div class="column">
|
|
||||||
<div class="col" align="right" style="position: sticky; top: 0; z-index: 999">
|
|
||||||
<q-btn round color="negative" icon="close" dense rounded @click="editDone(false)" />
|
|
||||||
</div>
|
|
||||||
<div class="col" style="margin: 0; padding: 0; margin-top: -2.4em">
|
|
||||||
<edit-event v-model="editor" @done="editDone" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<q-page padding>
|
|
||||||
<q-card>
|
|
||||||
<div style="max-width: 1800px; width: 100%">
|
|
||||||
<q-toolbar class="bg-primary text-white q-my-md shadow-2 items-center row justify-center">
|
|
||||||
<div class="row justify-center items-center">
|
|
||||||
<q-btn flat dense label="Prev" @click="calendarPrev" />
|
|
||||||
<q-separator vertical />
|
|
||||||
<q-btn flat dense
|
|
||||||
>{{ asMonth(selectedDate) }} {{ asYear(selectedDate) }}
|
|
||||||
<q-popup-proxy
|
|
||||||
transition-show="scale"
|
|
||||||
transition-hide="scale"
|
|
||||||
@before-show="updateProxy"
|
|
||||||
>
|
|
||||||
<q-date v-model="proxyDate">
|
|
||||||
<div class="row items-center justify-end q-gutter-sm">
|
|
||||||
<q-btn v-close-popup label="Cancel" color="primary" flat />
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
label="OK"
|
|
||||||
color="primary"
|
|
||||||
flat
|
|
||||||
@click="saveNewSelectedDate(proxyDate)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-date>
|
|
||||||
</q-popup-proxy>
|
|
||||||
</q-btn>
|
|
||||||
<q-separator vertical />
|
|
||||||
<q-btn flat dense label="Next" @click="calendarNext" />
|
|
||||||
</div>
|
|
||||||
<!-- <q-space /> -->
|
|
||||||
|
|
||||||
<q-btn-toggle
|
|
||||||
v-model="calendarView"
|
|
||||||
class="row absolute-right"
|
|
||||||
flat
|
|
||||||
stretch
|
|
||||||
toggle-color=""
|
|
||||||
:options="[
|
|
||||||
{ label: 'Tag', value: 'day' },
|
|
||||||
{ label: 'Woche', value: 'week' },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</q-toolbar>
|
|
||||||
<q-calendar-agenda
|
|
||||||
v-model="selectedDate"
|
|
||||||
:view="calendarRealView"
|
|
||||||
:max-days="calendarDays"
|
|
||||||
:weekdays="[1, 2, 3, 4, 5, 6, 0]"
|
|
||||||
locale="de-de"
|
|
||||||
style="height: 100%; min-height: 400px"
|
|
||||||
>
|
|
||||||
<template #day="{ scope: { timestamp } }">
|
|
||||||
<div itemref="" class="q-pb-sm" style="min-height: 200px">
|
|
||||||
<eventslot
|
|
||||||
v-for="(agenda, index) in events[timestamp.weekday]"
|
|
||||||
:key="index"
|
|
||||||
v-model="events[timestamp.weekday][index]"
|
|
||||||
@removeEvent="remove"
|
|
||||||
@editEvent="edit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</q-calendar-agenda>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, onBeforeMount, ref } from 'vue';
|
|
||||||
import { useScheduleStore } from '../../store';
|
|
||||||
import Eventslot from './slots/EventSlot.vue';
|
|
||||||
import { date } from 'quasar';
|
|
||||||
import { startOfWeek } from 'src/utils/datetime';
|
|
||||||
import EditEvent from '../management/EditEvent.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'AgendaView',
|
|
||||||
components: { Eventslot, EditEvent },
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const store = useScheduleStore();
|
|
||||||
const windowWidth = ref(window.innerWidth);
|
|
||||||
const selectedDate = ref(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
|
||||||
const proxyDate = ref('');
|
|
||||||
const calendarView = ref('week');
|
|
||||||
|
|
||||||
const calendarRealView = computed(() => (calendarDays.value != 7 ? 'day' : 'week'));
|
|
||||||
const calendarDays = computed(() =>
|
|
||||||
// <= 1023 is the breakpoint for sm to md
|
|
||||||
calendarView.value == 'day' ? 1 : windowWidth.value <= 1023 ? 3 : 7
|
|
||||||
);
|
|
||||||
const events = ref<Agendas>({});
|
|
||||||
const editor = ref<FG.Event | undefined>(undefined);
|
|
||||||
|
|
||||||
interface Agendas {
|
|
||||||
[index: number]: FG.Event[];
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
windowWidth.value = window.innerWidth;
|
|
||||||
});
|
|
||||||
|
|
||||||
await loadAgendas();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function edit(id: number) {
|
|
||||||
editor.value = await store.getEvent(id);
|
|
||||||
}
|
|
||||||
function editDone(changed: boolean) {
|
|
||||||
if (changed) void loadAgendas();
|
|
||||||
editor.value = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function remove(id: number) {
|
|
||||||
if (await store.removeEvent(id)) {
|
|
||||||
// Successfull removed
|
|
||||||
for (const idx in events.value) {
|
|
||||||
const i = events.value[idx].findIndex((event) => event.id === id);
|
|
||||||
if (i !== -1) {
|
|
||||||
events.value[idx].splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Not found, this means our eventa are outdated
|
|
||||||
await loadAgendas();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAgendas() {
|
|
||||||
const selected = new Date(selectedDate.value);
|
|
||||||
console.log(selected);
|
|
||||||
const start = calendarRealView.value === 'day' ? selected : startOfWeek(selected);
|
|
||||||
const end = date.addToDate(start, { days: calendarDays.value });
|
|
||||||
|
|
||||||
events.value = {};
|
|
||||||
const list = await store.getEvents({ from: start, to: end });
|
|
||||||
list.forEach((event) => {
|
|
||||||
const day = event.start.getDay();
|
|
||||||
|
|
||||||
if (!events.value[day]) {
|
|
||||||
events.value[day] = [];
|
|
||||||
}
|
|
||||||
events.value[day].push(event);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function calendarNext() {
|
|
||||||
selectedDate.value = date.formatDate(
|
|
||||||
date.addToDate(selectedDate.value, { days: calendarDays.value }),
|
|
||||||
'YYYY-MM-DD'
|
|
||||||
);
|
|
||||||
void loadAgendas();
|
|
||||||
}
|
|
||||||
|
|
||||||
function calendarPrev() {
|
|
||||||
selectedDate.value = date.formatDate(
|
|
||||||
date.subtractFromDate(selectedDate.value, { days: calendarDays.value }),
|
|
||||||
'YYYY-MM-DD'
|
|
||||||
);
|
|
||||||
void loadAgendas();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProxy() {
|
|
||||||
proxyDate.value = selectedDate.value;
|
|
||||||
}
|
|
||||||
function saveNewSelectedDate() {
|
|
||||||
proxyDate.value = date.formatDate(proxyDate.value, 'YYYY-MM-DD');
|
|
||||||
selectedDate.value = proxyDate.value;
|
|
||||||
}
|
|
||||||
function asMonth(value: string) {
|
|
||||||
if (value) {
|
|
||||||
return date.formatDate(new Date(value), 'MMMM', {
|
|
||||||
months: [
|
|
||||||
'Januar',
|
|
||||||
'Februar',
|
|
||||||
'März',
|
|
||||||
'April',
|
|
||||||
'Mai',
|
|
||||||
'Juni',
|
|
||||||
'Juli',
|
|
||||||
'August',
|
|
||||||
'September',
|
|
||||||
'Oktober',
|
|
||||||
'November',
|
|
||||||
'Dezember',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function asYear(value: string) {
|
|
||||||
if (value) {
|
|
||||||
return date.formatDate(new Date(value), 'YYYY');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
asYear,
|
|
||||||
asMonth,
|
|
||||||
selectedDate,
|
|
||||||
edit,
|
|
||||||
editor,
|
|
||||||
editDone,
|
|
||||||
events,
|
|
||||||
calendarNext,
|
|
||||||
calendarPrev,
|
|
||||||
updateProxy,
|
|
||||||
saveNewSelectedDate,
|
|
||||||
proxyDate,
|
|
||||||
remove,
|
|
||||||
calendarDays,
|
|
||||||
calendarView,
|
|
||||||
calendarRealView,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
|
@ -1,99 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card
|
|
||||||
class="q-mx-xs q-mt-sm justify-start content-center items-center rounded-borders shadow-5"
|
|
||||||
bordered
|
|
||||||
>
|
|
||||||
<q-card-section class="text-primary q-pa-xs">
|
|
||||||
<div class="text-weight-bolder text-center" style="font-size: 1.5vw">
|
|
||||||
{{ event.type.name }}
|
|
||||||
<template v-if="event.name"
|
|
||||||
>: <span style="font-size: 1.2vw">{{ event.name }}</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div v-if="event.description" class="text-weight-medium" style="font-size: 1vw">
|
|
||||||
{{ event.description }}
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-separator />
|
|
||||||
<q-card-section class="q-pa-xs">
|
|
||||||
<!-- Jobs -->
|
|
||||||
<JobSlot
|
|
||||||
v-for="(job, index) in event.jobs"
|
|
||||||
:key="index"
|
|
||||||
v-model="event.jobs[index]"
|
|
||||||
class="col q-my-xs"
|
|
||||||
:event-id="event.id"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-actions v-if="canEdit || canDelete" vertical align="center">
|
|
||||||
<q-btn
|
|
||||||
v-if="canEdit"
|
|
||||||
color="secondary"
|
|
||||||
flat
|
|
||||||
label="Bearbeiten"
|
|
||||||
style="min-width: 95%"
|
|
||||||
@click="edit"
|
|
||||||
/>
|
|
||||||
<q-btn
|
|
||||||
v-if="canDelete"
|
|
||||||
color="negative"
|
|
||||||
flat
|
|
||||||
label="Löschen"
|
|
||||||
style="min-width: 95%"
|
|
||||||
@click="remove"
|
|
||||||
/>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, computed, PropType } from 'vue';
|
|
||||||
import { hasPermission } from 'src/utils/permission';
|
|
||||||
import { PERMISSIONS } from 'src/plugins/schedule/permissions';
|
|
||||||
import JobSlot from './JobSlot.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'Eventslot',
|
|
||||||
components: { JobSlot },
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
required: true,
|
|
||||||
type: Object as PropType<FG.Event>,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: {
|
|
||||||
'update:modelValue': (val: FG.Event) => !!val,
|
|
||||||
removeEvent: (val: number) => typeof val === 'number',
|
|
||||||
editEvent: (val: number) => !!val,
|
|
||||||
},
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const canDelete = computed(() => hasPermission(PERMISSIONS.DELETE));
|
|
||||||
const canEdit = computed(
|
|
||||||
() =>
|
|
||||||
hasPermission(PERMISSIONS.EDIT) &&
|
|
||||||
(props.modelValue?.end || props.modelValue.start) > new Date()
|
|
||||||
);
|
|
||||||
const event = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (v) => emit('update:modelValue', v),
|
|
||||||
});
|
|
||||||
|
|
||||||
function remove() {
|
|
||||||
emit('removeEvent', props.modelValue.id);
|
|
||||||
}
|
|
||||||
function edit() {
|
|
||||||
emit('editEvent', props.modelValue.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
canDelete,
|
|
||||||
canEdit,
|
|
||||||
edit,
|
|
||||||
event,
|
|
||||||
remove,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,142 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card bordered>
|
|
||||||
<div class="text-weight-medium q-px-xs">
|
|
||||||
{{ asHour(modelValue.start) }}
|
|
||||||
<template v-if="modelValue.end">- {{ asHour(modelValue.end) }}</template>
|
|
||||||
</div>
|
|
||||||
<div class="q-px-xs">
|
|
||||||
{{ modelValue.type.name }}
|
|
||||||
</div>
|
|
||||||
<div class="col-auto q-px-xs" style="font-size: 10px">
|
|
||||||
{{ modelValue.comment }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<q-select
|
|
||||||
:model-value="modelValue.services"
|
|
||||||
filled
|
|
||||||
:option-label="(opt) => userDisplay(opt)"
|
|
||||||
multiple
|
|
||||||
disable
|
|
||||||
use-chips
|
|
||||||
stack-label
|
|
||||||
label="Dienste"
|
|
||||||
class="col-auto q-px-xs"
|
|
||||||
style="font-size: 6px"
|
|
||||||
counter
|
|
||||||
:max-values="modelValue.required_services"
|
|
||||||
>
|
|
||||||
</q-select>
|
|
||||||
<div class="row col-12 justify-end">
|
|
||||||
<q-btn v-if="canEnroll" flat color="primary" label="Eintragen" @click="enrollForJob" />
|
|
||||||
<q-btn v-if="isEnrolled" flat color="negative" label="Austragen" @click="signOutFromJob" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, onBeforeMount, computed, PropType } from 'vue';
|
|
||||||
import { Notify } from 'quasar';
|
|
||||||
import { asHour } from 'src/utils/datetime';
|
|
||||||
import { useUserStore } from 'src/plugins/user/store';
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
import { useScheduleStore } from 'src/plugins/schedule/store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'JobSlot',
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
required: true,
|
|
||||||
type: Object as PropType<FG.Job>,
|
|
||||||
},
|
|
||||||
eventId: {
|
|
||||||
required: true,
|
|
||||||
type: Number,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: { 'update:modelValue': (v: FG.Job) => !!v },
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const store = useScheduleStore();
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const availableUsers = null;
|
|
||||||
|
|
||||||
onBeforeMount(async () => userStore.getUsers());
|
|
||||||
|
|
||||||
function userDisplay(service: FG.Service) {
|
|
||||||
return userStore.findUser(service.userid)?.display_name || service.userid;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isEnrolled = computed(
|
|
||||||
() =>
|
|
||||||
props.modelValue.services.findIndex(
|
|
||||||
(service) => service.userid == mainStore.currentUser.userid
|
|
||||||
) !== -1
|
|
||||||
);
|
|
||||||
|
|
||||||
const canEnroll = computed(() => {
|
|
||||||
const is = isEnrolled.value;
|
|
||||||
let sum = 0;
|
|
||||||
props.modelValue.services.forEach((s) => (sum += s.value));
|
|
||||||
return sum < props.modelValue.required_services && !is;
|
|
||||||
});
|
|
||||||
|
|
||||||
async function enrollForJob() {
|
|
||||||
const newService: FG.Service = {
|
|
||||||
userid: mainStore.currentUser.userid,
|
|
||||||
is_backup: false,
|
|
||||||
value: 1,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const job = await store.updateJob(props.eventId, props.modelValue.id, { user: newService });
|
|
||||||
emit('update:modelValue', job);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error);
|
|
||||||
Notify.create({
|
|
||||||
group: false,
|
|
||||||
type: 'negative',
|
|
||||||
message: 'Fehler beim Eintragen als Dienst',
|
|
||||||
timeout: 10000,
|
|
||||||
progress: true,
|
|
||||||
actions: [{ icon: 'mdi-close', color: 'white' }],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function signOutFromJob() {
|
|
||||||
const newService: FG.Service = {
|
|
||||||
userid: mainStore.currentUser.userid,
|
|
||||||
is_backup: false,
|
|
||||||
value: -1,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const job = await store.updateJob(props.eventId, props.modelValue.id, {
|
|
||||||
user: newService,
|
|
||||||
});
|
|
||||||
emit('update:modelValue', job);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error);
|
|
||||||
Notify.create({
|
|
||||||
group: false,
|
|
||||||
type: 'negative',
|
|
||||||
message: 'Fehler beim Austragen als Dienst',
|
|
||||||
timeout: 10000,
|
|
||||||
progress: true,
|
|
||||||
actions: [{ icon: 'mdi-close', color: 'white' }],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
availableUsers,
|
|
||||||
enrollForJob,
|
|
||||||
isEnrolled,
|
|
||||||
signOutFromJob,
|
|
||||||
canEnroll,
|
|
||||||
userDisplay,
|
|
||||||
asHour,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,9 +0,0 @@
|
||||||
declare namespace FG {
|
|
||||||
export interface RecurrenceRule {
|
|
||||||
frequency: string;
|
|
||||||
interval: number;
|
|
||||||
count?: number;
|
|
||||||
until?: Date;
|
|
||||||
weekdays?: Array<number>;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
|
|
||||||
<EditEvent v-model="event" />
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { onBeforeMount, defineComponent, ref } from 'vue';
|
|
||||||
import EditEvent from '../components/management/EditEvent.vue';
|
|
||||||
import { useScheduleStore } from '../store';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { EditEvent },
|
|
||||||
setup() {
|
|
||||||
const route = useRoute();
|
|
||||||
const store = useScheduleStore();
|
|
||||||
const event = ref<FG.Event | undefined>(undefined);
|
|
||||||
onBeforeMount(async () => {
|
|
||||||
if ('id' in route.params && typeof route.params.id === 'string')
|
|
||||||
event.value = await store.getEvent(parseInt(route.params.id));
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
event,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,95 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<q-tabs v-if="$q.screen.gt.sm" v-model="tab">
|
|
||||||
<q-tab
|
|
||||||
v-for="(tabindex, index) in tabs"
|
|
||||||
:key="'tab' + index"
|
|
||||||
:name="tabindex.name"
|
|
||||||
:label="tabindex.label"
|
|
||||||
/>
|
|
||||||
</q-tabs>
|
|
||||||
<div v-else class="fit row justify-end">
|
|
||||||
<q-btn flat round icon="mdi-menu" @click="showDrawer = !showDrawer" />
|
|
||||||
</div>
|
|
||||||
<q-drawer v-model="showDrawer" side="right" behavior="mobile" @click="showDrawer = !showDrawer">
|
|
||||||
<q-list v-model="tab">
|
|
||||||
<q-item
|
|
||||||
v-for="(tabindex, index) in tabs"
|
|
||||||
:key="'tab' + index"
|
|
||||||
:active="tab == tabindex.name"
|
|
||||||
clickable
|
|
||||||
@click="tab = tabindex.name"
|
|
||||||
>
|
|
||||||
<q-item-label>{{ tabindex.label }}</q-item-label>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-drawer>
|
|
||||||
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
|
|
||||||
<q-tab-panels
|
|
||||||
v-model="tab"
|
|
||||||
style="background-color: transparent"
|
|
||||||
class="q-ma-none q-pa-none fit row justify-center content-start items-start"
|
|
||||||
animated
|
|
||||||
>
|
|
||||||
<q-tab-panel name="create">
|
|
||||||
<EditEvent />
|
|
||||||
</q-tab-panel>
|
|
||||||
<q-tab-panel name="eventtypes">
|
|
||||||
<EventTypes />
|
|
||||||
</q-tab-panel>
|
|
||||||
<q-tab-panel name="jobtypes">
|
|
||||||
<JobTypes v-if="canEditJobTypes" />
|
|
||||||
</q-tab-panel>
|
|
||||||
</q-tab-panels>
|
|
||||||
</q-page>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, ref } from 'vue';
|
|
||||||
import EventTypes from '../components/management/EventTypes.vue';
|
|
||||||
import JobTypes from '../components/management/JobTypes.vue';
|
|
||||||
import EditEvent from '../components/management/EditEvent.vue';
|
|
||||||
import { hasPermission } from 'src/utils/permission';
|
|
||||||
import { PERMISSIONS } from '../permissions';
|
|
||||||
import { Screen } from 'quasar';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'EventManagement',
|
|
||||||
components: { EditEvent, EventTypes, JobTypes },
|
|
||||||
setup() {
|
|
||||||
const canEditJobTypes = computed(() => hasPermission(PERMISSIONS.JOB_TYPE));
|
|
||||||
|
|
||||||
interface Tab {
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabs: Tab[] = [
|
|
||||||
{ name: 'create', label: 'Veranstaltungen' },
|
|
||||||
{ name: 'eventtypes', label: 'Veranstaltungsarten' },
|
|
||||||
{ name: 'jobtypes', label: 'Dienstarten' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const drawer = ref<boolean>(false);
|
|
||||||
|
|
||||||
const showDrawer = computed({
|
|
||||||
get: () => {
|
|
||||||
return !Screen.gt.sm && drawer.value;
|
|
||||||
},
|
|
||||||
set: (val: boolean) => {
|
|
||||||
drawer.value = val;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const tab = ref<string>('create');
|
|
||||||
|
|
||||||
return {
|
|
||||||
canEditJobTypes,
|
|
||||||
showDrawer,
|
|
||||||
tab,
|
|
||||||
tabs,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,87 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<q-tabs v-if="$q.screen.gt.sm" v-model="tab">
|
|
||||||
<q-tab
|
|
||||||
v-for="(tabindex, index) in tabs"
|
|
||||||
:key="'tab' + index"
|
|
||||||
:name="tabindex.name"
|
|
||||||
:label="tabindex.label"
|
|
||||||
/>
|
|
||||||
</q-tabs>
|
|
||||||
<div v-else class="fit row justify-end">
|
|
||||||
<q-btn flat round icon="mdi-menu" @click="showDrawer = !showDrawer" />
|
|
||||||
</div>
|
|
||||||
<q-drawer v-model="showDrawer" side="right" behavior="mobile" @click="showDrawer = !showDrawer">
|
|
||||||
<q-list v-model="tab">
|
|
||||||
<q-item
|
|
||||||
v-for="(tabindex, index) in tabs"
|
|
||||||
:key="'tab' + index"
|
|
||||||
:active="tab == tabindex.name"
|
|
||||||
clickable
|
|
||||||
@click="tab = tabindex.name"
|
|
||||||
>
|
|
||||||
<q-item-label>{{ tabindex.label }}</q-item-label>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-drawer>
|
|
||||||
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
|
|
||||||
<q-tab-panels
|
|
||||||
v-model="tab"
|
|
||||||
style="background-color: transparent"
|
|
||||||
class="q-ma-none q-pa-none fit row justify-center content-start items-start"
|
|
||||||
animated
|
|
||||||
>
|
|
||||||
<q-tab-panel name="agendaView">
|
|
||||||
<AgendaView />
|
|
||||||
</q-tab-panel>
|
|
||||||
<q-tab-panel name="eventtypes">
|
|
||||||
<EventTypes />
|
|
||||||
</q-tab-panel>
|
|
||||||
</q-tab-panels>
|
|
||||||
</q-page>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, ref } from 'vue';
|
|
||||||
import EventTypes from '../components/management/EventTypes.vue';
|
|
||||||
//import CreateEvent from '../components/management/CreateEvent.vue';
|
|
||||||
import AgendaView from '../components/overview/AgendaView.vue';
|
|
||||||
import { Screen } from 'quasar';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'EventOverview',
|
|
||||||
components: { AgendaView, EventTypes },
|
|
||||||
setup() {
|
|
||||||
interface Tab {
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabs: Tab[] = [
|
|
||||||
{ name: 'agendaView', label: 'Kalendar' },
|
|
||||||
// { name: 'eventtypes', label: 'Veranstaltungsarten' },
|
|
||||||
// { name: 'jobtypes', label: 'Dienstarten' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const drawer = ref<boolean>(false);
|
|
||||||
|
|
||||||
const showDrawer = computed({
|
|
||||||
get: () => {
|
|
||||||
return !Screen.gt.sm && drawer.value;
|
|
||||||
},
|
|
||||||
set: (val: boolean) => {
|
|
||||||
drawer.value = val;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const tab = ref<string>('agendaView');
|
|
||||||
|
|
||||||
return {
|
|
||||||
showDrawer,
|
|
||||||
tab,
|
|
||||||
tabs,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,8 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-page padding>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section class="row"> </q-card-section>
|
|
||||||
<q-card-section> </q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
|
@ -1,16 +0,0 @@
|
||||||
export const PERMISSIONS = {
|
|
||||||
// Can create events
|
|
||||||
CREATE: 'events_create',
|
|
||||||
// Can edit events
|
|
||||||
EDIT: 'events_edit',
|
|
||||||
// Can delete events
|
|
||||||
DELETE: 'events_delete',
|
|
||||||
// Can create and edit EventTypes
|
|
||||||
EVENT_TYPE: 'events_event_type',
|
|
||||||
// Can create and edit JobTypes
|
|
||||||
JOB_TYPE: 'events_job_type',
|
|
||||||
// Can self assign to jobs
|
|
||||||
ASSIGN: 'events_assign',
|
|
||||||
// Can assign other users to jobs
|
|
||||||
ASSIGN_OTHER: 'events_assign_other',
|
|
||||||
};
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { innerRoutes, privateRoutes } from './routes';
|
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
|
||||||
import { defineAsyncComponent } from 'vue';
|
|
||||||
|
|
||||||
const plugin: FG_Plugin.Plugin = {
|
|
||||||
name: 'Schedule',
|
|
||||||
innerRoutes,
|
|
||||||
internalRoutes: privateRoutes,
|
|
||||||
requiredModules: ['User'],
|
|
||||||
requiredBackendModules: ['events'],
|
|
||||||
version: '0.0.1',
|
|
||||||
widgets: [
|
|
||||||
{
|
|
||||||
priority: 0,
|
|
||||||
name: 'stats',
|
|
||||||
permissions: [],
|
|
||||||
widget: defineAsyncComponent(() => import('./components/Widget.vue')),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default plugin;
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { FG_Plugin } from '@flaschengeist/typings';
|
|
||||||
import { PERMISSIONS } from '../permissions';
|
|
||||||
|
|
||||||
export const innerRoutes: FG_Plugin.MenuRoute[] = [
|
|
||||||
{
|
|
||||||
title: 'Dienste',
|
|
||||||
icon: 'mdi-briefcase',
|
|
||||||
permissions: ['user'],
|
|
||||||
route: {
|
|
||||||
path: 'schedule',
|
|
||||||
name: 'schedule',
|
|
||||||
redirect: { name: 'schedule-overview' },
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
title: 'Dienstübersicht',
|
|
||||||
icon: 'mdi-account-group',
|
|
||||||
shortcut: true,
|
|
||||||
route: {
|
|
||||||
path: 'schedule-overview',
|
|
||||||
name: 'schedule-overview',
|
|
||||||
component: () => import('../pages/Overview.vue'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Dienstverwaltung',
|
|
||||||
icon: 'mdi-account-details',
|
|
||||||
shortcut: false,
|
|
||||||
permissions: [PERMISSIONS.CREATE],
|
|
||||||
route: {
|
|
||||||
path: 'schedule-management',
|
|
||||||
name: 'schedule-management',
|
|
||||||
component: () => import('../pages/Management.vue'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Dienstanfragen',
|
|
||||||
icon: 'mdi-account-switch',
|
|
||||||
shortcut: false,
|
|
||||||
route: {
|
|
||||||
path: 'schedule-requests',
|
|
||||||
name: 'schedule-requests',
|
|
||||||
component: () => import('../pages/Requests.vue'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const privateRoutes: FG_Plugin.NamedRouteRecordRaw[] = [
|
|
||||||
{
|
|
||||||
name: 'events-edit',
|
|
||||||
path: 'schedule/:id/edit',
|
|
||||||
component: () => import('../pages/Event.vue'),
|
|
||||||
},
|
|
||||||
];
|
|
|
@ -1,165 +0,0 @@
|
||||||
import { api } from 'src/boot/axios';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
|
|
||||||
interface UserService {
|
|
||||||
user: FG.Service;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fixJob(job: FG.Job) {
|
|
||||||
job.start = new Date(job.start);
|
|
||||||
if (job.end) job.end = new Date(job.end);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fixEvent(event: FG.Event) {
|
|
||||||
event.start = new Date(event.start);
|
|
||||||
if (event.end) event.end = new Date(event.end);
|
|
||||||
|
|
||||||
event.jobs.forEach((job) => fixJob(job));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useScheduleStore = defineStore({
|
|
||||||
id: 'schedule',
|
|
||||||
|
|
||||||
state: () => ({
|
|
||||||
jobTypes: [] as FG.JobType[],
|
|
||||||
eventTypes: [] as FG.EventType[],
|
|
||||||
templates: [] as FG.Event[],
|
|
||||||
}),
|
|
||||||
|
|
||||||
getters: {},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
async getJobTypes(force = false) {
|
|
||||||
if (force || this.jobTypes.length == 0)
|
|
||||||
try {
|
|
||||||
const { data } = await api.get<FG.JobType[]>('/events/job-types');
|
|
||||||
this.jobTypes = data;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return this.jobTypes;
|
|
||||||
},
|
|
||||||
|
|
||||||
async addJobType(name: string) {
|
|
||||||
await api.post<FG.JobType>('/events/job-types', { name: name });
|
|
||||||
//TODO: HAndle new JT
|
|
||||||
},
|
|
||||||
|
|
||||||
async removeJobType(id: number) {
|
|
||||||
await api.delete(`/events/job-types/${id}`);
|
|
||||||
//Todo Handle delete JT
|
|
||||||
},
|
|
||||||
|
|
||||||
async renameJobType(id: number, newName: string) {
|
|
||||||
await api.put(`/events/job-types/${id}`, { name: newName });
|
|
||||||
// TODO handle rename
|
|
||||||
},
|
|
||||||
|
|
||||||
async getEventTypes(force = false) {
|
|
||||||
if (force || this.eventTypes.length == 0)
|
|
||||||
try {
|
|
||||||
const { data } = await api.get<FG.EventType[]>('/events/event-types');
|
|
||||||
this.eventTypes = data;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return this.eventTypes;
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Add new EventType
|
|
||||||
*
|
|
||||||
* @param name Name of new EventType
|
|
||||||
* @returns EventType object or null if name already exists
|
|
||||||
* @throws Exception if requests fails because of an other reason
|
|
||||||
*/
|
|
||||||
async addEventType(name: string) {
|
|
||||||
try {
|
|
||||||
const { data } = await api.post<FG.EventType>('/events/event-types', { name: name });
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
if ('response' in error) {
|
|
||||||
const ae = <AxiosError>error;
|
|
||||||
if (ae.response && ae.response.status == 409 /* CONFLICT */) return null;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async removeEvent(id: number) {
|
|
||||||
try {
|
|
||||||
await api.delete(`/events/${id}`);
|
|
||||||
const idx = this.templates.findIndex((v) => v.id === id);
|
|
||||||
if (idx !== -1) this.templates.splice(idx, 1);
|
|
||||||
} catch (e) {
|
|
||||||
const error = <AxiosError>e;
|
|
||||||
if (error.response && error.response.status === 404) return false;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
async removeEventType(id: number) {
|
|
||||||
await api.delete(`/events/event-types/${id}`);
|
|
||||||
// TODO handle delete
|
|
||||||
},
|
|
||||||
|
|
||||||
async renameEventType(id: number, newName: string) {
|
|
||||||
try {
|
|
||||||
await api.put(`/events/event-types/${id}`, { name: newName });
|
|
||||||
// TODO handle rename
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
if ('response' in error) {
|
|
||||||
const ae = <AxiosError>error;
|
|
||||||
if (ae.response && ae.response.status == 409 /* CONFLICT */) return false;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async getTemplates(force = false) {
|
|
||||||
if (force || this.templates.length == 0) {
|
|
||||||
const { data } = await api.get<FG.Event[]>('/events/templates');
|
|
||||||
data.forEach((element) => fixEvent(element));
|
|
||||||
this.templates = data;
|
|
||||||
}
|
|
||||||
return this.templates;
|
|
||||||
},
|
|
||||||
|
|
||||||
async getEvents(filter: { from?: Date; to?: Date } | undefined = undefined) {
|
|
||||||
try {
|
|
||||||
const { data } = await api.get<FG.Event[]>('/events', { params: filter });
|
|
||||||
data.forEach((element) => fixEvent(element));
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async getEvent(id: number) {
|
|
||||||
try {
|
|
||||||
const { data } = await api.get<FG.Event>(`/events/${id}`);
|
|
||||||
fixEvent(data);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateJob(eventId: number, jobId: number, service: FG.Service | UserService) {
|
|
||||||
try {
|
|
||||||
const { data } = await api.put<FG.Job>(`/events/${eventId}/jobs/${jobId}`, service);
|
|
||||||
fixJob(data);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async addEvent(event: FG.Event) {
|
|
||||||
const { data } = await api.post<FG.Event>('/events', event);
|
|
||||||
if (data.is_template) this.templates.push(data);
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,39 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card class="12">
|
|
||||||
<q-card-section class="fit row justify-start content-center items-center">
|
|
||||||
<div class="col-xs-12 col-sm-6 text-center text-h6">Neues Mitglied</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<MainUserSettings :user="user" :new-user="true" @update:user="setUser" />
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, ref } from 'vue';
|
|
||||||
import MainUserSettings from 'src/plugins/user/components/settings/MainUserSettings.vue';
|
|
||||||
import { useUserStore } from '../store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'NewUser',
|
|
||||||
components: { MainUserSettings },
|
|
||||||
setup() {
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const user = ref<FG.User>({
|
|
||||||
userid: '',
|
|
||||||
display_name: '',
|
|
||||||
firstname: '',
|
|
||||||
lastname: '',
|
|
||||||
mail: '',
|
|
||||||
roles: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
async function setUser(value: FG.User) {
|
|
||||||
await userStore.createUser(value);
|
|
||||||
}
|
|
||||||
return { user, setUser };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,40 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card class="col-12">
|
|
||||||
<q-card-section class="fit row justify-start content-center items-center">
|
|
||||||
<div class="col-xs-12 col-sm-6 text-center text-h6">Benutzereinstellungen</div>
|
|
||||||
<div class="col-xs-12 col-sm-6 q-pa-sm">
|
|
||||||
<UserSelector v-model="user" />
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<MainUserSettings :user="user" @update:user="updateUser" />
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, ref } from 'vue';
|
|
||||||
import UserSelector from '../components/UserSelector.vue';
|
|
||||||
import MainUserSettings from '../components/settings/MainUserSettings.vue';
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
import { useUserStore } from '../store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'UpdateUser',
|
|
||||||
components: { UserSelector, MainUserSettings },
|
|
||||||
setup() {
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const user = ref(mainStore.currentUser);
|
|
||||||
|
|
||||||
async function updateUser(value: FG.User) {
|
|
||||||
await userStore.updateUser(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
user,
|
|
||||||
updateUser,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,43 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-select
|
|
||||||
v-model="selected"
|
|
||||||
filled
|
|
||||||
:label="label"
|
|
||||||
:options="users"
|
|
||||||
option-label="display_name"
|
|
||||||
option-value="userid"
|
|
||||||
map-options
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, PropType, onBeforeMount } from 'vue';
|
|
||||||
import { useUserStore } from '../store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'UserSelector',
|
|
||||||
props: {
|
|
||||||
label: { type: String, default: 'Benutzer' },
|
|
||||||
modelValue: { default: undefined, type: Object as PropType<FG.User | undefined> },
|
|
||||||
},
|
|
||||||
emits: { 'update:modelValue': (user: FG.User) => !!user },
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
void userStore.getUsers(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
const users = computed(() => userStore.users);
|
|
||||||
const selected = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (value: FG.User | undefined) => (value ? emit('update:modelValue', value) : undefined),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
selected,
|
|
||||||
users,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,72 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card style="text-align: center">
|
|
||||||
<q-card-section class="row justify-center content-stretch">
|
|
||||||
<div v-if="avatar" class="col-4">
|
|
||||||
<div style="width: 100%; padding-bottom: 100%; position: relative">
|
|
||||||
<q-avatar style="position: absolute; top: 0; left: 0; width: 100%; height: 100%">
|
|
||||||
<img :src="avatarLink" :onerror="error" />
|
|
||||||
</q-avatar>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-8">
|
|
||||||
<span class="text-h6">Hallo {{ name }}</span
|
|
||||||
><br />
|
|
||||||
<span v-if="hasBirthday">Herzlichen Glückwunsch zum Geburtstag!<br /></span>
|
|
||||||
<span v-if="birthday.length > 0"
|
|
||||||
>Heute <span v-if="birthday.length === 1">hat </span><span v-else>haben </span
|
|
||||||
><span v-for="(user, index) in birthday" :key="index"
|
|
||||||
>{{ user.display_name }}<span v-if="index < birthday.length - 1">, </span></span
|
|
||||||
>
|
|
||||||
Geburtstag.</span
|
|
||||||
>
|
|
||||||
<span v-else>Heute stehen keine Geburtstage an</span>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
import { computed, defineComponent, onMounted, ref } from 'vue';
|
|
||||||
import { useUserStore } from '../store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'Greeting',
|
|
||||||
setup() {
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
// Ensure users are loaded,so we can query birthdays
|
|
||||||
onMounted(() => userStore.getUsers(false));
|
|
||||||
|
|
||||||
const avatar = ref(true);
|
|
||||||
const name = ref(mainStore.currentUser.firstname);
|
|
||||||
const avatarLink = ref(mainStore.currentUser.avatar_url);
|
|
||||||
|
|
||||||
function error() {
|
|
||||||
avatar.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function userHasBirthday(user: FG.User) {
|
|
||||||
const today = new Date();
|
|
||||||
return (
|
|
||||||
user.birthday &&
|
|
||||||
user.birthday.getMonth() === today.getMonth() &&
|
|
||||||
user.birthday.getDate() === today.getDate()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasBirthday = computed(() => {
|
|
||||||
return userHasBirthday(mainStore.currentUser);
|
|
||||||
});
|
|
||||||
|
|
||||||
const birthday = computed(() =>
|
|
||||||
userStore.users
|
|
||||||
.filter(userHasBirthday)
|
|
||||||
.filter((user) => user.userid !== mainStore.currentUser.userid)
|
|
||||||
);
|
|
||||||
|
|
||||||
return { avatar, avatarLink, error, name, hasBirthday, birthday };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,218 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-form @submit="save" @reset="reset">
|
|
||||||
<q-card-section class="fit row justify-start content-center items-center">
|
|
||||||
<q-input
|
|
||||||
v-model="userModel.firstname"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="Vorname"
|
|
||||||
:rules="[notEmpty]"
|
|
||||||
autocomplete="given-name"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-model="userModel.lastname"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="Nachname"
|
|
||||||
:rules="[notEmpty]"
|
|
||||||
autocomplete="family-name"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-model="userModel.display_name"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="Angezeigter Name"
|
|
||||||
:rules="[notEmpty]"
|
|
||||||
autocomplete="nickname"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-model="userModel.mail"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="E-Mail"
|
|
||||||
:rules="[isEmail, notEmpty]"
|
|
||||||
autocomplete="email"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
<q-input
|
|
||||||
v-model="userModel.userid"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="Benutzername"
|
|
||||||
:readonly="!newUser"
|
|
||||||
:rules="newUser ? [isFreeUID, notEmpty] : []"
|
|
||||||
autocomplete="username"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
<q-select
|
|
||||||
v-model="userModel.roles"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="Rollen"
|
|
||||||
filled
|
|
||||||
multiple
|
|
||||||
use-chips
|
|
||||||
:readonly="!canSetRoles"
|
|
||||||
:options="allRoles"
|
|
||||||
option-label="name"
|
|
||||||
option-value="name"
|
|
||||||
/>
|
|
||||||
<IsoDateInput
|
|
||||||
v-model="userModel.birthday"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
label="Geburtstag"
|
|
||||||
autocomplete="bday"
|
|
||||||
/>
|
|
||||||
<q-file
|
|
||||||
v-model="avatar"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
filled
|
|
||||||
label="Avatar"
|
|
||||||
accept=".jpg, image/*"
|
|
||||||
max-file-size="204800"
|
|
||||||
hint="Bilddateien, max. 200 KiB"
|
|
||||||
@rejected="onAvatarRejected"
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<q-icon name="mdi-file-image" @click.stop />
|
|
||||||
</template>
|
|
||||||
</q-file>
|
|
||||||
</q-card-section>
|
|
||||||
<q-separator v-if="!newUser" />
|
|
||||||
<q-card-section v-if="!newUser" class="fit row justify-start content-center items-center">
|
|
||||||
<PasswordInput
|
|
||||||
v-if="isCurrentUser"
|
|
||||||
v-model="password"
|
|
||||||
:rules="[notEmpty]"
|
|
||||||
filled
|
|
||||||
label="Passwort"
|
|
||||||
autocomplete="current-password"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
hint="Passwort muss immer eingetragen werden"
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
v-model="newPassword"
|
|
||||||
filled
|
|
||||||
label="Neues Password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-actions align="right">
|
|
||||||
<q-btn label="Reset" type="reset" />
|
|
||||||
<q-btn color="primary" type="submit" label="Speichern" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Notify } from 'quasar';
|
|
||||||
import { hasPermission } from 'src/utils/permission';
|
|
||||||
import { notEmpty, isEmail } from 'src/utils/validators';
|
|
||||||
import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
|
|
||||||
import PasswordInput from 'src/components/utils/PasswordInput.vue';
|
|
||||||
import { defineComponent, computed, ref, onBeforeMount, PropType, watch } from 'vue';
|
|
||||||
import { useUserStore } from '../../store';
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'MainUserSettings',
|
|
||||||
components: { IsoDateInput, PasswordInput },
|
|
||||||
props: {
|
|
||||||
user: {
|
|
||||||
required: true,
|
|
||||||
type: Object as PropType<FG.User>,
|
|
||||||
},
|
|
||||||
newUser: { type: Boolean, required: true },
|
|
||||||
},
|
|
||||||
emits: {
|
|
||||||
'update:user': (payload: FG.User) => !!payload,
|
|
||||||
},
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
void userStore.getRoles(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
const password = ref('');
|
|
||||||
const newPassword = ref('');
|
|
||||||
const avatar = ref<File | FileList | string[]>();
|
|
||||||
const userModel = ref(props.user);
|
|
||||||
|
|
||||||
const canSetRoles = computed(() => hasPermission('users_set_roles'));
|
|
||||||
const allRoles = computed(() => userStore.roles.map((role) => role.name));
|
|
||||||
const isCurrentUser = computed(() => userModel.value.userid === mainStore.currentUser.userid);
|
|
||||||
|
|
||||||
/* Reset model if props changed */
|
|
||||||
watch(
|
|
||||||
() => props.user,
|
|
||||||
() => (userModel.value = props.user)
|
|
||||||
);
|
|
||||||
|
|
||||||
function onAvatarRejected() {
|
|
||||||
Notify.create({
|
|
||||||
group: false,
|
|
||||||
type: 'negative',
|
|
||||||
message: 'Datei zu groß oder keine gültige Bilddatei.',
|
|
||||||
timeout: 10000,
|
|
||||||
progress: true,
|
|
||||||
actions: [{ icon: 'mdi-close', color: 'white' }],
|
|
||||||
});
|
|
||||||
avatar.value = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
let changed = userModel.value;
|
|
||||||
if (typeof changed.birthday === 'string') changed.birthday = new Date(changed.birthday);
|
|
||||||
changed = Object.assign(changed, {
|
|
||||||
password: password.value,
|
|
||||||
});
|
|
||||||
if (newPassword.value != '') {
|
|
||||||
changed = Object.assign(changed, {
|
|
||||||
new_password: newPassword.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('update:user', changed);
|
|
||||||
|
|
||||||
if (avatar.value)
|
|
||||||
userStore
|
|
||||||
.uploadAvatar(changed, avatar.value instanceof File ? avatar.value : avatar.value[0])
|
|
||||||
.catch((response: Response) => {
|
|
||||||
if (response && response.status == 400) {
|
|
||||||
onAvatarRejected();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
userModel.value = props.user;
|
|
||||||
password.value = '';
|
|
||||||
newPassword.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFreeUID(val: string) {
|
|
||||||
return (
|
|
||||||
userStore.users.findIndex((user) => user.userid === val) === -1 ||
|
|
||||||
'Benutzername ist schon vergeben'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
allRoles,
|
|
||||||
avatar,
|
|
||||||
canSetRoles,
|
|
||||||
isCurrentUser,
|
|
||||||
isEmail,
|
|
||||||
isFreeUID,
|
|
||||||
newPassword,
|
|
||||||
notEmpty,
|
|
||||||
onAvatarRejected,
|
|
||||||
password,
|
|
||||||
reset,
|
|
||||||
save,
|
|
||||||
userModel,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,160 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<q-card class="col-12">
|
|
||||||
<q-form @submit="save" @reset="reset">
|
|
||||||
<q-card-section class="fit row justify-start content-center items-center">
|
|
||||||
<span class="col-xs-12 col-sm-6 text-center text-h6"> Rollen und Berechtigungen </span>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
use-input
|
|
||||||
label="Rolle"
|
|
||||||
input-debounce="0"
|
|
||||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
|
||||||
:model-value="role"
|
|
||||||
:options="roles"
|
|
||||||
option-label="name"
|
|
||||||
option-value="name"
|
|
||||||
map-options
|
|
||||||
clearable
|
|
||||||
@new-value="createRole"
|
|
||||||
@update:modelValue="updateRole"
|
|
||||||
@clear="removeRole"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
<q-separator />
|
|
||||||
<q-card-section v-if="role">
|
|
||||||
<q-input v-if="role.id !== -1" v-model="newRoleName" filled label="neuer Name" />
|
|
||||||
<q-scroll-area style="height: 40vh; width: 100%" class="background-like-input">
|
|
||||||
<q-option-group
|
|
||||||
:model-value="role.permissions"
|
|
||||||
:options="permissions"
|
|
||||||
color="primary"
|
|
||||||
type="checkbox"
|
|
||||||
@update:modelValue="updatePermissions"
|
|
||||||
/>
|
|
||||||
</q-scroll-area>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-actions v-if="role" align="right">
|
|
||||||
<q-btn label="Löschen" color="negative" @click="remove" />
|
|
||||||
<q-btn label="Reset" type="reset" />
|
|
||||||
<q-btn color="primary" type="submit" label="Speichern" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-form>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, ref, onBeforeMount } from 'vue';
|
|
||||||
import { useUserStore } from '../../store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'RoleSettings',
|
|
||||||
setup() {
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
void userStore.getRoles();
|
|
||||||
void userStore.getPermissions();
|
|
||||||
});
|
|
||||||
|
|
||||||
const role = ref<FG.Role | null>(null);
|
|
||||||
const roles = computed(() => userStore.roles);
|
|
||||||
const permissions = computed(() =>
|
|
||||||
userStore.permissions.map((perm) => {
|
|
||||||
return {
|
|
||||||
value: perm,
|
|
||||||
label: perm,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const newRoleName = ref<string>('');
|
|
||||||
|
|
||||||
function createRole(name: string, done: (arg0: string, arg1: string) => void): void {
|
|
||||||
role.value = { name: name, permissions: [], id: -1 };
|
|
||||||
done(name, 'add-unique');
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeRole(): void {
|
|
||||||
role.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePermissions(permissions: string[]) {
|
|
||||||
if (role.value) {
|
|
||||||
role.value.permissions = permissions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateRole(rl: FG.Role | string | null) {
|
|
||||||
if (typeof rl === 'string' || rl === null) return;
|
|
||||||
role.value = {
|
|
||||||
id: rl.id,
|
|
||||||
name: rl.name,
|
|
||||||
permissions: Array.from(rl.permissions),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
if (role.value) {
|
|
||||||
if (role.value.id === -1)
|
|
||||||
void userStore.newRole(role.value).then((createdRole: FG.Role) => {
|
|
||||||
role.value = createdRole;
|
|
||||||
});
|
|
||||||
else {
|
|
||||||
if (newRoleName.value !== '') role.value.name = newRoleName.value;
|
|
||||||
void userStore.updateRole(role.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
if (role.value && role.value.id !== -1) {
|
|
||||||
const original = roles.value.find((value) => value.name === role.value?.name);
|
|
||||||
if (original) updateRole(original);
|
|
||||||
} else {
|
|
||||||
role.value = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove() {
|
|
||||||
if (role.value) {
|
|
||||||
if (role.value.id === -1) {
|
|
||||||
role.value = null;
|
|
||||||
} else {
|
|
||||||
void userStore.deleteRole(role.value).then(() => (role.value = null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
roles,
|
|
||||||
role,
|
|
||||||
permissions,
|
|
||||||
createRole,
|
|
||||||
updateRole,
|
|
||||||
updatePermissions,
|
|
||||||
save,
|
|
||||||
reset,
|
|
||||||
removeRole,
|
|
||||||
remove,
|
|
||||||
newRoleName,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="sass" scoped>
|
|
||||||
// Same colors like qinput with filled attribute set
|
|
||||||
.body--light .background-like-input
|
|
||||||
background-color: rgba(0, 0, 0, 0.05)
|
|
||||||
&:hover
|
|
||||||
background: rgba(0,0,0,.1)
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.42)
|
|
||||||
|
|
||||||
.body--dark .background-like-input
|
|
||||||
background-color: rgba(255, 255, 255, 0.07)
|
|
||||||
&:hover
|
|
||||||
background: rgba(255, 255, 255, 0.14)
|
|
||||||
border-bottom: 1px solid #fff
|
|
||||||
</style>
|
|
|
@ -1,168 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-card class="col-12" height="">
|
|
||||||
<q-card-section v-if="isThisSession(modelValue.token)" class="text-caption">
|
|
||||||
Diese Session.
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12 col-sm-6">
|
|
||||||
Browser:
|
|
||||||
<q-icon :name="getBrowserIcon(modelValue.browser)" size="24px" />
|
|
||||||
{{ modelValue.browser }}
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-12 col-sm-6">
|
|
||||||
Plattform:
|
|
||||||
<q-icon :name="getPlatformIcon(modelValue.platform)" size="24px" />
|
|
||||||
{{ modelValue.platform }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="!isEdit" class="row">
|
|
||||||
<div class="col-xs-12 col-sm-6">
|
|
||||||
Lebenszeit:
|
|
||||||
{{ modelValue.lifetime }}
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-12 col-sm-6">Läuft aus: {{ dateTime(modelValue.expires) }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="row q-my-sm">
|
|
||||||
<q-input
|
|
||||||
v-model="computedLifetime"
|
|
||||||
class="col-xs-12 col-sm-6 q-px-sm"
|
|
||||||
type="number"
|
|
||||||
label="Zeit"
|
|
||||||
filled
|
|
||||||
/>
|
|
||||||
<q-select v-model="option" class="col-xs-12 col-sm-6 q-px-sm" :options="options" filled />
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-actions v-if="!isEdit" align="right">
|
|
||||||
<q-btn flat round dense icon="mdi-pencil" @click="edit(true)" />
|
|
||||||
<q-btn flat round dense icon="mdi-delete" @click="deleteSession(modelValue.token)" />
|
|
||||||
</q-card-actions>
|
|
||||||
<q-card-actions v-else align="right">
|
|
||||||
<q-btn flat dense label="Abbrechen" @click="edit(false)" />
|
|
||||||
<q-btn flat dense label="Speichern" @click="save" />
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, ref, computed, PropType } from 'vue';
|
|
||||||
import { formatDateTime } from 'src/utils/datetime';
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
import { useSessionStore } from '../../store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'Session',
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
required: true,
|
|
||||||
type: Object as PropType<FG.Session>,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: {
|
|
||||||
'update:modelValue': (s: FG.Session) => !!s,
|
|
||||||
delete: () => true,
|
|
||||||
},
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const sessionStore = useSessionStore();
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
|
|
||||||
const dateTime = (date: Date) => formatDateTime(date, true);
|
|
||||||
|
|
||||||
const options = ref(['Minuten', 'Stunden', 'Tage']);
|
|
||||||
const option = ref<string>(options.value[0]);
|
|
||||||
const lifetime = ref(0);
|
|
||||||
function getBrowserIcon(browser: string) {
|
|
||||||
return browser == 'firefox'
|
|
||||||
? 'mdi-firefox'
|
|
||||||
: browser == 'chrome'
|
|
||||||
? 'mdi-google-chrome'
|
|
||||||
: browser == 'safari'
|
|
||||||
? 'mdi-apple-safari'
|
|
||||||
: 'mdi-help';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPlatformIcon(platform: string) {
|
|
||||||
return platform == 'linux'
|
|
||||||
? 'mdi-linux'
|
|
||||||
: platform == 'windows'
|
|
||||||
? 'mdi-microsoft-windows'
|
|
||||||
: platform == 'macos'
|
|
||||||
? 'mdi-apple'
|
|
||||||
: platform == 'iphone'
|
|
||||||
? 'mdi-cellphone-iphone'
|
|
||||||
: platform == 'android'
|
|
||||||
? 'mdi-cellphone-android'
|
|
||||||
: 'mdi-help';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteSession(token: string) {
|
|
||||||
await sessionStore.deleteSession(token);
|
|
||||||
emit('delete');
|
|
||||||
}
|
|
||||||
function isThisSession(token: string) {
|
|
||||||
return mainStore.session?.token === token;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isEdit = ref(false);
|
|
||||||
|
|
||||||
const computedLifetime = computed({
|
|
||||||
get: () => {
|
|
||||||
switch (option.value) {
|
|
||||||
case options.value[0]:
|
|
||||||
return (lifetime.value / 60).toFixed(2);
|
|
||||||
case options.value[1]:
|
|
||||||
return (lifetime.value / (60 * 60)).toFixed(2);
|
|
||||||
case options.value[2]:
|
|
||||||
return (lifetime.value / (60 * 60 * 24)).toFixed(2);
|
|
||||||
}
|
|
||||||
throw 'Invalid option';
|
|
||||||
},
|
|
||||||
set: (val) => {
|
|
||||||
if (val) {
|
|
||||||
switch (option.value) {
|
|
||||||
case options.value[0]:
|
|
||||||
lifetime.value = parseFloat(val) * 60;
|
|
||||||
break;
|
|
||||||
case options.value[1]:
|
|
||||||
lifetime.value = parseFloat(val) * 60 * 60;
|
|
||||||
break;
|
|
||||||
case options.value[2]:
|
|
||||||
lifetime.value = parseFloat(val) * 60 * 60 * 24;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function edit(value: boolean) {
|
|
||||||
lifetime.value = props.modelValue.lifetime;
|
|
||||||
isEdit.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function save() {
|
|
||||||
isEdit.value = false;
|
|
||||||
await sessionStore.updateSession(lifetime.value, props.modelValue.token);
|
|
||||||
emit(
|
|
||||||
'update:modelValue',
|
|
||||||
Object.assign(Object.assign({}, props.modelValue), { lifetime: lifetime.value })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getBrowserIcon,
|
|
||||||
getPlatformIcon,
|
|
||||||
isThisSession,
|
|
||||||
deleteSession,
|
|
||||||
isEdit,
|
|
||||||
dateTime,
|
|
||||||
edit,
|
|
||||||
options,
|
|
||||||
option,
|
|
||||||
lifetime,
|
|
||||||
computedLifetime,
|
|
||||||
save,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,14 +0,0 @@
|
||||||
export interface LoginData {
|
|
||||||
userid: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LoginResponse {
|
|
||||||
user: FG.User;
|
|
||||||
session: FG.Session;
|
|
||||||
permissions: FG.Permission[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CurrentUserResponse extends FG.User {
|
|
||||||
permissions: FG.Permission[];
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
|
|
||||||
<q-tabs v-if="$q.screen.gt.sm" v-model="tab">
|
|
||||||
<q-tab
|
|
||||||
v-for="(tabindex, index) in tabs"
|
|
||||||
:key="'tab' + index"
|
|
||||||
:name="tabindex.name"
|
|
||||||
:label="tabindex.label"
|
|
||||||
/>
|
|
||||||
</q-tabs>
|
|
||||||
<div v-else class="fit row justify-end">
|
|
||||||
<q-btn flat round icon="mdi-menu" @click="showDrawer = !showDrawer" />
|
|
||||||
</div>
|
|
||||||
<q-drawer v-model="showDrawer" side="right" behavior="mobile" @click="showDrawer = !showDrawer">
|
|
||||||
<q-list v-model="tab">
|
|
||||||
<q-item
|
|
||||||
v-for="(tabindex, index) in tabs"
|
|
||||||
:key="'tab' + index"
|
|
||||||
:active="tab == tabindex.name"
|
|
||||||
clickable
|
|
||||||
@click="tab = tabindex.name"
|
|
||||||
>
|
|
||||||
<q-item-label>{{ tabindex.label }}</q-item-label>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-drawer>
|
|
||||||
<q-tab-panels
|
|
||||||
v-model="tab"
|
|
||||||
style="background-color: transparent"
|
|
||||||
class="q-ma-none q-pa-none fit row justify-center content-start items-start"
|
|
||||||
animated
|
|
||||||
>
|
|
||||||
<q-tab-panel name="user">
|
|
||||||
<UpdateUser />
|
|
||||||
</q-tab-panel>
|
|
||||||
<q-tab-panel name="newUser">
|
|
||||||
<NewUser />
|
|
||||||
</q-tab-panel>
|
|
||||||
<q-tab-panel name="roles">
|
|
||||||
<RoleSettings v-if="canEditRoles" />
|
|
||||||
</q-tab-panel>
|
|
||||||
</q-tab-panels>
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Screen } from 'quasar';
|
|
||||||
import { PERMISSIONS } from '../permissions';
|
|
||||||
import NewUser from '../components/NewUser.vue';
|
|
||||||
import { computed, defineComponent, ref } from 'vue';
|
|
||||||
import { hasPermission } from 'src/utils/permission';
|
|
||||||
import UpdateUser from '../components/UpdateUser.vue';
|
|
||||||
import RoleSettings from '../components/settings/RoleSettings.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'AdminSettings',
|
|
||||||
components: { RoleSettings, UpdateUser, NewUser },
|
|
||||||
setup() {
|
|
||||||
const canEditRoles = computed(() => hasPermission(PERMISSIONS.ROLES_EDIT));
|
|
||||||
|
|
||||||
interface Tab {
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabs: Tab[] = [
|
|
||||||
{ name: 'user', label: 'Mitglieder' },
|
|
||||||
{ name: 'newUser', label: 'Neues Mitglied' },
|
|
||||||
{ name: 'roles', label: 'Rollen' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const drawer = ref<boolean>(false);
|
|
||||||
|
|
||||||
const showDrawer = computed({
|
|
||||||
get: () => {
|
|
||||||
return !Screen.gt.sm && drawer.value;
|
|
||||||
},
|
|
||||||
set: (val: boolean) => {
|
|
||||||
drawer.value = val;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const tab = ref<string>('user');
|
|
||||||
|
|
||||||
return {
|
|
||||||
canEditRoles,
|
|
||||||
showDrawer,
|
|
||||||
tab,
|
|
||||||
tabs,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -1,57 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<q-page padding class="fit row justify-center content-center items-center q-gutter-sm">
|
|
||||||
<q-card class="col-12">
|
|
||||||
<q-card-section class="fit row justify-start content-center items-center">
|
|
||||||
<div class="col-12 text-center text-h6">Benutzereinstellungen</div>
|
|
||||||
</q-card-section>
|
|
||||||
<MainUserSettings :user="currentUser" @update:user="updateUser" />
|
|
||||||
</q-card>
|
|
||||||
<div class="col-12 text-left text-h6">Aktive Sessions:</div>
|
|
||||||
<Session
|
|
||||||
v-for="(session, index) in sessions"
|
|
||||||
:key="'session' + index"
|
|
||||||
v-model="sessions[index]"
|
|
||||||
@delete="removeSession(session)"
|
|
||||||
/>
|
|
||||||
</q-page>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, onBeforeMount, ref } from 'vue';
|
|
||||||
import Session from '../components/settings/Session.vue';
|
|
||||||
import MainUserSettings from '../components/settings/MainUserSettings.vue';
|
|
||||||
import { useMainStore } from 'src/stores';
|
|
||||||
import { useSessionStore } from '../store';
|
|
||||||
import { useUserStore } from '../store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
// name: 'PageName'
|
|
||||||
components: { Session, MainUserSettings },
|
|
||||||
setup() {
|
|
||||||
const mainStore = useMainStore();
|
|
||||||
const sessionStore = useSessionStore();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
onBeforeMount(() => sessionStore.getSessions().then((s) => (sessions.value = s)));
|
|
||||||
const currentUser = ref(mainStore.currentUser);
|
|
||||||
const sessions = ref([] as FG.Session[]);
|
|
||||||
|
|
||||||
async function updateUser(value: FG.User) {
|
|
||||||
await userStore.updateUser(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeSession(s: FG.Session) {
|
|
||||||
sessions.value = sessions.value.filter((ss) => ss.token !== s.token);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentUser,
|
|
||||||
sessions,
|
|
||||||
updateUser,
|
|
||||||
removeSession,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue