Compare commits
488 Commits
Author | SHA1 | Date |
---|---|---|
Tim Gröger | b7735e2924 | |
Tim Gröger | d35cc8e8d1 | |
Tim Gröger | d34898e1e9 | |
Tim Gröger | 93669d66dc | |
Tim Gröger | ec5458bf7e | |
Tim Gröger | efc7c49a0b | |
Tim Gröger | b1e4879881 | |
Tim Gröger | ee7e03ce28 | |
Tim Gröger | 2928c241ad | |
Tim Gröger | fe9ec96ce1 | |
Tim Gröger | 417689b725 | |
Tim Gröger | 847e923265 | |
Tim Gröger | 4cb0362bb7 | |
Tim Gröger | a46c41cb5b | |
Tim Gröger | 3d55f2d2ae | |
Tim Gröger | 3689da810c | |
Tim Gröger | e6d9054256 | |
Tim Gröger | ab45bf3667 | |
Tim Gröger | 857d07040b | |
groegert | e07df08822 | |
Ferdinand Thiessen | ec28857af5 | |
Ferdinand Thiessen | 1c452e23fe | |
Ferdinand Thiessen | 195593ddc5 | |
Ferdinand Thiessen | 2425e6cf2f | |
Ferdinand Thiessen | a9edc12494 | |
Ferdinand Thiessen | 1525de1469 | |
Ferdinand Thiessen | 6a75d1bf51 | |
Ferdinand Thiessen | f9f66e7172 | |
Ferdinand Thiessen | e9c0086859 | |
Ferdinand Thiessen | b2c70a6657 | |
Ferdinand Thiessen | f27212f60e | |
Ferdinand Thiessen | 9eb5412c14 | |
Ferdinand Thiessen | 8e552ba508 | |
Ferdinand Thiessen | 49c3ec74ba | |
Ferdinand Thiessen | 83f32ea82a | |
Ferdinand Thiessen | 656d7a9e3c | |
Ferdinand Thiessen | 8fca175d39 | |
Ferdinand Thiessen | 6c219c5226 | |
Ferdinand Thiessen | cb43b8a39b | |
Tim Gröger | acf1816b55 | |
Ferdinand Thiessen | 02f60335d4 | |
Ferdinand Thiessen | d9267bcc0a | |
Ferdinand Thiessen | 6732921ff7 | |
Ferdinand Thiessen | 7a705d5f9a | |
Ferdinand Thiessen | 368ca23c56 | |
Tim Gröger | d82e025700 | |
Tim Gröger | 07e1966471 | |
Ferdinand Thiessen | 29c085bd2c | |
Ferdinand Thiessen | 1b152b52f5 | |
Ferdinand Thiessen | 6769e18ffa | |
Ferdinand Thiessen | 88dd96c937 | |
Ferdinand Thiessen | 4887bc261b | |
Ferdinand Thiessen | d516839ad4 | |
Ferdinand Thiessen | c9df257bbf | |
Tim Gröger | 053fdae384 | |
Ferdinand Thiessen | 6fd3f045f8 | |
Ferdinand Thiessen | 2d167ebbae | |
Ferdinand Thiessen | a8578e2803 | |
Ferdinand Thiessen | e4889ddac2 | |
Ferdinand Thiessen | 664def40fc | |
Ferdinand Thiessen | ade6d06eb6 | |
Tim Gröger | 53f053a294 | |
Tim Gröger | cc29893e04 | |
Ferdinand Thiessen | 42800a9d99 | |
Ferdinand Thiessen | f4650ffdeb | |
Ferdinand Thiessen | 1158525abb | |
Ferdinand Thiessen | 04e3c57397 | |
Tim Gröger | 2fc411d51d | |
Tim Gröger | 6061b37887 | |
Tim Gröger | efde9a2ee7 | |
Tim Gröger | e96d15bc66 | |
Tim Gröger | dad88ec766 | |
Ferdinand Thiessen | 3e091fd02b | |
Ferdinand Thiessen | fca79c36ef | |
Ferdinand Thiessen | 34fcdbdb7f | |
Ferdinand Thiessen | d84796b09d | |
Ferdinand Thiessen | 62af4f5026 | |
Tim Gröger | 36d6fdfb94 | |
Ferdinand Thiessen | a30da50b1d | |
Ferdinand Thiessen | 53951daa25 | |
Ferdinand Thiessen | 59920e23a5 | |
Ferdinand Thiessen | 5952c9b7f2 | |
Ferdinand Thiessen | 73f50e9f4f | |
Ferdinand Thiessen | dfb924bb3f | |
Ferdinand Thiessen | bc9dba1c7b | |
Ferdinand Thiessen | 1132bfd129 | |
Ferdinand Thiessen | fe1fae10f5 | |
Ferdinand Thiessen | 1c36399ac0 | |
Tim Gröger | a38954cf70 | |
Ferdinand Thiessen | 35d8433e23 | |
groegert | 731cc20a06 | |
groegert | c38032085f | |
Ferdinand Thiessen | 22f47bd34e | |
Tim Gröger | 16fd9201ae | |
Ferdinand Thiessen | 873fee3301 | |
Ferdinand Thiessen | 1802081ad2 | |
Ferdinand Thiessen | 631e78acb3 | |
Ferdinand Thiessen | d422380adc | |
Ferdinand Thiessen | 0c279289b2 | |
Ferdinand Thiessen | 979eab05af | |
Ferdinand Thiessen | fd918f5bb7 | |
Ferdinand Thiessen | 068dbdcc7b | |
Ferdinand Thiessen | 8c9db67b95 | |
groegert | 86bad83e53 | |
groegert | 625ac55b0a | |
groegert | 9940589d1a | |
groegert | f2b7f3a3b4 | |
Ferdinand Thiessen | f9c9f6efbe | |
Ferdinand Thiessen | 0873b2da22 | |
groegert | 734a3e51c9 | |
Ferdinand Thiessen | cf1a5cc922 | |
Ferdinand Thiessen | 6e503ed38f | |
Ferdinand Thiessen | 4cbff6b077 | |
Ferdinand Thiessen | 2b42dad617 | |
Ferdinand Thiessen | 8b6c400689 | |
Tim Gröger | 5174e7fea3 | |
Tim Gröger | 95867428a8 | |
Tim Gröger | 3c7d711f59 | |
Tim Gröger | 81c5b10101 | |
Tim Gröger | a8a6cd8814 | |
Tim Gröger | 18d0098bb3 | |
Tim Gröger | be347225c4 | |
Tim Gröger | e2b46d96b9 | |
Tim Gröger | 6e74105f38 | |
Tim Gröger | ae5212bbfc | |
Tim Gröger | 2a11964c4b | |
Tim Gröger | e0bf8f77bf | |
Tim Gröger | c76da59290 | |
Tim Gröger | 52ae8fce29 | |
Tim Gröger | f7a6f3fbe1 | |
Tim Gröger | 8ee5c891a5 | |
Tim Gröger | 479dfd658a | |
Tim Gröger | 97dcc8e602 | |
Tim Gröger | a376fd7cc2 | |
Tim Gröger | 8b21ccf978 | |
Tim Gröger | cee6eda585 | |
Tim Gröger | dc948e6f11 | |
Tim Gröger | 86bb722623 | |
Tim Gröger | 0df2677b1b | |
Tim Gröger | 43397fe3a7 | |
Tim Gröger | 851c5a0588 | |
Tim Gröger | 0626cf993f | |
Tim Gröger | 9b0679278c | |
Tim Gröger | b40b1064e7 | |
Tim Gröger | 06256f651a | |
Tim Gröger | e90dc4c306 | |
Tim Gröger | 8c321fb8ca | |
Tim Gröger | 459a9fea08 | |
Tim Gröger | 7a3a151688 | |
Tim Gröger | 1c64dbbaf6 | |
Ferdinand Thiessen | 4e340e5bea | |
Tim Gröger | 89cd109587 | |
Ferdinand Thiessen | 010ad6e107 | |
Ferdinand Thiessen | 6f053d849a | |
Tim Gröger | f5b370e743 | |
Tim Gröger | 741216ac3e | |
Ferdinand Thiessen | 6a6afcb2d4 | |
Ferdinand Thiessen | ee3cb0ba40 | |
Ferdinand Thiessen | ecb0649594 | |
Ferdinand Thiessen | 66dd33dc25 | |
Tim Gröger | 284652b002 | |
Tim Gröger | 736ea04b4a | |
Tim Gröger | 5d1df48b9a | |
Ferdinand Thiessen | 7fb689b31f | |
Tim Gröger | 7289a1724d | |
Ferdinand Thiessen | 927a5214b1 | |
Ferdinand Thiessen | 679d98a2af | |
Ferdinand Thiessen | f787e314ef | |
Tim Gröger | 909275727a | |
Ferdinand Thiessen | fc35e2ecec | |
Ferdinand Thiessen | 7d914d065e | |
Ferdinand Thiessen | 48d6792fa0 | |
Ferdinand Thiessen | c61b5fcc0c | |
Tim Gröger | 8ecbddc6ca | |
Tim Gröger | f25651a71e | |
Ferdinand Thiessen | 1b1888d4fd | |
Tim Gröger | fb14df0c43 | |
Tim Gröger | 6cdc143aa9 | |
Tim Gröger | 7089ee4d62 | |
Tim Gröger | aadfca2d31 | |
Ferdinand Thiessen | 6891a3ffba | |
Tim Gröger | a7d32d6f7c | |
Ferdinand Thiessen | b7aeea0a23 | |
Tim Gröger | 1b478d7680 | |
Ferdinand Thiessen | e564901d2e | |
Tim Gröger | d185b84823 | |
Tim Gröger | 0d044b505a | |
Tim Gröger | 3eea079871 | |
Tim Gröger | 30e101c364 | |
Ferdinand Thiessen | 534d5e3034 | |
Ferdinand Thiessen | 5731fc9d6d | |
Ferdinand Thiessen | 852b1dad03 | |
Ferdinand Thiessen | c362843c8e | |
Tim Gröger | cf0f453b7c | |
Tim Gröger | 24aec1a98c | |
Tim Gröger | 8eecb70df0 | |
Tim Gröger | 59d6023462 | |
Tim Gröger | f58e0c382c | |
Tim Gröger | 3d20292898 | |
Tim Gröger | 4bd2d24e9e | |
Ferdinand Thiessen | e9333c4af4 | |
Ferdinand Thiessen | 718c6eaf9d | |
Tim Gröger | aa82b9029a | |
Tim Gröger | 5e2a60ead4 | |
Tim Gröger | d42c6dcce1 | |
Ferdinand Thiessen | 8c6036c686 | |
Ferdinand Thiessen | f28e597d53 | |
Ferdinand Thiessen | 61ed9e70e2 | |
Ferdinand Thiessen | 27ad9984d4 | |
Ferdinand Thiessen | fb2febdadd | |
Ferdinand Thiessen | b15ff0f5e3 | |
Ferdinand Thiessen | 6da4f67d68 | |
Ferdinand Thiessen | 6dc70d12f6 | |
Ferdinand Thiessen | 87139077da | |
Tim Gröger | 827fb1aadd | |
Tim Gröger | e0046aa7d2 | |
Ferdinand Thiessen | f2610b8e84 | |
Ferdinand Thiessen | 851ce3aa8b | |
Tim Gröger | 6097e510c5 | |
Tim Gröger | 14206d9117 | |
Tim Gröger | cd937f111c | |
Ferdinand Thiessen | 36bbc2dbf1 | |
Ferdinand Thiessen | 84fe321ecc | |
Ferdinand Thiessen | ade1c984c6 | |
Ferdinand Thiessen | 218e41e94a | |
Ferdinand Thiessen | 77bb463e5e | |
Ferdinand Thiessen | 17460a8543 | |
Ferdinand Thiessen | c5a34ce63f | |
Ferdinand Thiessen | 0b255c481a | |
Ferdinand Thiessen | b8531ad816 | |
Tim Gröger | 73a5de021d | |
Tim Gröger | c3e3a272dc | |
Ferdinand Thiessen | 7e7f9c943d | |
Ferdinand Thiessen | a4ce273bb1 | |
Ferdinand Thiessen | 575090552f | |
Tim Gröger | e2d2a5cf9d | |
Ferdinand Thiessen | 1316c47706 | |
Ferdinand Thiessen | 42b43aa56c | |
Ferdinand Thiessen | 9469cda4b0 | |
Ferdinand Thiessen | 51fcc6f9be | |
Ferdinand Thiessen | 26148f8827 | |
Ferdinand Thiessen | dbcd1b2c5f | |
Tim Gröger | 9b273c2501 | |
Ferdinand Thiessen | ccf13eae9c | |
Ferdinand Thiessen | f18897caf4 | |
Ferdinand Thiessen | 98375f81be | |
Ferdinand Thiessen | 76978e8883 | |
Ferdinand Thiessen | e347129ba9 | |
Ferdinand Thiessen | fd45a46c01 | |
Ferdinand Thiessen | 4a7ed50281 | |
Ferdinand Thiessen | bc3c15e3bc | |
Ferdinand Thiessen | 0922d468d9 | |
Ferdinand Thiessen | 544d58889b | |
Ferdinand Thiessen | 4b198b6472 | |
Ferdinand Thiessen | 5153f074b5 | |
Ferdinand Thiessen | 62aa627f0c | |
Ferdinand Thiessen | c60f171285 | |
Ferdinand Thiessen | e4394db93b | |
Ferdinand Thiessen | 78427aa5d2 | |
Ferdinand Thiessen | d54b398c14 | |
Ferdinand Thiessen | 50f1f028eb | |
Ferdinand Thiessen | b2d54a046f | |
Ferdinand Thiessen | 9967296698 | |
Ferdinand Thiessen | 17ffd19c5b | |
Ferdinand Thiessen | d3d8c1e5f2 | |
Ferdinand Thiessen | cb68f9ff7e | |
Ferdinand Thiessen | 34312cca96 | |
Ferdinand Thiessen | 68fa8fa1a8 | |
Tim Gröger | ee2a6a71eb | |
Tim Gröger | 8f8da5ffd1 | |
Ferdinand Thiessen | 152b86fb4f | |
Ferdinand Thiessen | 6be07b1001 | |
Ferdinand Thiessen | ba0696c3c0 | |
Ferdinand Thiessen | 6e50a510eb | |
Ferdinand Thiessen | 897c98c53a | |
Ferdinand Thiessen | 117d8256be | |
Ferdinand Thiessen | b7db5ea3a6 | |
Ferdinand Thiessen | d147e538d1 | |
Ferdinand Thiessen | 0efe445864 | |
Ferdinand Thiessen | 074fae4da3 | |
Tim Gröger | fb8fc09e8d | |
Tim Gröger | b141c2e5c4 | |
Tim Gröger | 73f16d6cbb | |
Ferdinand Thiessen | caa09a3c2c | |
Tim Gröger | 20191be5dc | |
Tim Gröger | 57f21936c0 | |
Tim Gröger | c272c9e4a5 | |
Tim Gröger | 7e01ffc507 | |
Tim Gröger | e4851bd178 | |
Tim Gröger | f6951bdf0b | |
Tim Gröger | 724ae66dd7 | |
Tim Gröger | c0d57c6a71 | |
Ferdinand Thiessen | cd74612e6d | |
Dominik | 567e994b71 | |
Dominik | af79a30497 | |
Ferdinand Thiessen | 7fe59f67f5 | |
Dominik | cc47e21a31 | |
Dominik | e03b2f20ff | |
Ferdinand Thiessen | 3283d8a862 | |
Ferdinand Thiessen | cf2fcc0664 | |
Dominik | 8dbd6cc5eb | |
Tim Gröger | 2de1eaf29e | |
Tim Gröger | 44b50edf82 | |
Dominik | 0a2be0e5ff | |
Dominik | e2b4550411 | |
Dominik | ac30659aeb | |
Ferdinand Thiessen | 008d40b56a | |
Ferdinand Thiessen | c9d8365def | |
Ferdinand Thiessen | bda5602e9f | |
Tim Gröger | b4c080fec6 | |
Tim Gröger | 006e7e4048 | |
Tim Gröger | 059142c506 | |
Tim Gröger | 6e406c9b2c | |
Ferdinand Thiessen | 3ace1e43da | |
Ferdinand Thiessen | 2630da3ca4 | |
Ferdinand Thiessen | 99d3acaef5 | |
Tim Gröger | 284533742d | |
Tim Gröger | 9f53cb6cab | |
Tim Gröger | feaeb3f4e4 | |
Tim Gröger | 797f7dd67a | |
Ferdinand Thiessen | d396940071 | |
Ferdinand Thiessen | 502c40329c | |
Ferdinand Thiessen | 17e640892a | |
Ferdinand Thiessen | 4ff63b458d | |
Ferdinand Thiessen | 9734dc41a4 | |
Ferdinand Thiessen | a8ad2f1da5 | |
Dominik | 1dc0603df3 | |
Dominik | 663d1d3e4d | |
Dominik | 0844b0997d | |
Tim Gröger | 69e68b92f9 | |
Tim Gröger | 693b6a11d3 | |
Ferdinand Thiessen | 91200f277c | |
Ferdinand Thiessen | a787abdbc0 | |
Ferdinand Thiessen | e366a25838 | |
Dominik | 45bf4aa223 | |
Tim Gröger | 51240dd98b | |
Dominik | aba0046c84 | |
Dominik | 1ccddb228d | |
Ferdinand Thiessen | 61316dcd9f | |
Ferdinand Thiessen | 887262ae5a | |
Ferdinand Thiessen | 270df75fc8 | |
Ferdinand Thiessen | 78857522ea | |
Dominik | c05877fa46 | |
Tim Gröger | a861129e1b | |
Dominik | d6261d8a0d | |
Dominik | f5f9d2af61 | |
Ferdinand Thiessen | 04237246fa | |
Ferdinand Thiessen | ba485f87c5 | |
Tim Gröger | 6e90075db3 | |
Ferdinand Thiessen | 5028d46900 | |
Ferdinand Thiessen | 01143e08e8 | |
Ferdinand Thiessen | 7748d2d8a3 | |
Ferdinand Thiessen | 08c29c1cd6 | |
Dominik | 5a97bfa413 | |
Ferdinand Thiessen | eabc520762 | |
Ferdinand Thiessen | 17e203b5c9 | |
Ferdinand Thiessen | d4795a549f | |
Ferdinand Thiessen | c05fc5d877 | |
Ferdinand Thiessen | 0b7c6feeb3 | |
Ferdinand Thiessen | 7612ccde7b | |
Ferdinand Thiessen | bda58426e3 | |
Ferdinand Thiessen | 4be0f56820 | |
Ferdinand Thiessen | 06b259cd74 | |
Ferdinand Thiessen | 4c9fb07f7d | |
Ferdinand Thiessen | 82d88f50d0 | |
Ferdinand Thiessen | 5061d18956 | |
Ferdinand Thiessen | 967458a51b | |
Ferdinand Thiessen | 1471f1a660 | |
Ferdinand Thiessen | 939dde3651 | |
Ferdinand Thiessen | 01826fbc8b | |
Ferdinand Thiessen | 9b19dc225b | |
Ferdinand Thiessen | c8708be39d | |
Tim Gröger | 97b60298ec | |
Tim Gröger | 63b25bb3d6 | |
Ferdinand Thiessen | e4378af76e | |
Tim Gröger | 9f2f632a67 | |
Tim Gröger | 9cdc041b13 | |
Tim Gröger | 306ae7648d | |
Tim Gröger | 60417f6585 | |
Tim Gröger | d5e4571b73 | |
Tim Gröger | 5f7c515228 | |
Ferdinand Thiessen | fde2682681 | |
Ferdinand Thiessen | 19f91d2abf | |
Tim Gröger | 7b1a1c3656 | |
Tim Gröger | 4ea0bce19d | |
Tim Gröger | 338fbb97b3 | |
Tim Gröger | 1ce02a67a9 | |
Tim Gröger | 70575c94c3 | |
Ferdinand Thiessen | b069361c1a | |
Ferdinand Thiessen | e26dc6c3a9 | |
Ferdinand Thiessen | 296245457d | |
Ferdinand Thiessen | 390e0fc95b | |
Ferdinand Thiessen | cfc46dddd3 | |
Ferdinand Thiessen | 31620f9681 | |
Ferdinand Thiessen | 63e9de01e2 | |
Ferdinand Thiessen | b479e3ad48 | |
Ferdinand Thiessen | 8c1dffc003 | |
Ferdinand Thiessen | e566a89860 | |
Ferdinand Thiessen | 458cf81a91 | |
Ferdinand Thiessen | d4bc385833 | |
Ferdinand Thiessen | edf56c1094 | |
Ferdinand Thiessen | 8689e84d47 | |
Ferdinand Thiessen | 5c11e02b2c | |
Ferdinand Thiessen | bdcf9668b7 | |
Ferdinand Thiessen | 1d598b5787 | |
Ferdinand Thiessen | 27b34e36e2 | |
Ferdinand Thiessen | 4061d84ace | |
Ferdinand Thiessen | 245944b6a9 | |
Ferdinand Thiessen | 7b710f0bf4 | |
Tim Gröger | 5d1409b735 | |
Tim Gröger | 3247a5bb01 | |
Tim Gröger | ef71481931 | |
Tim Gröger | a1f1be7fb6 | |
Tim Gröger | 09c6a806c9 | |
Tim Gröger | 555d2a871b | |
Tim Gröger | caedb5a9d2 | |
Tim Gröger | 22ca9b03a0 | |
Ferdinand Thiessen | 45da05901b | |
Ferdinand Thiessen | c306f96bb8 | |
Tim Gröger | c9a5b6d165 | |
Tim Gröger | cc27307835 | |
Tim Gröger | fd71f08430 | |
Tim Gröger | 4c8f72603e | |
Ferdinand Thiessen | e3398c3fa5 | |
Ferdinand Thiessen | 7c33a71c4d | |
Ferdinand Thiessen | 3f756437ee | |
Ferdinand Thiessen | 5a4f6939d1 | |
Ferdinand Thiessen | 9992ed6f2b | |
Ferdinand Thiessen | dc0107bcc9 | |
Tim Gröger | 789cf89603 | |
Tim Gröger | 27d44b350f | |
Tim Gröger | 925982d700 | |
Tim Gröger | c6ef18b009 | |
Tim Gröger | d097231dc1 | |
Ferdinand Thiessen | 05fd255a51 | |
Ferdinand Thiessen | e6da94ad0e | |
Ferdinand Thiessen | 2383e28cd8 | |
Tim Gröger | 4e5509fcde | |
Tim Gröger | cb9ede5b27 | |
Tim Gröger | 52dc3057ad | |
Ferdinand Thiessen | 3c8748f044 | |
Ferdinand Thiessen | ed41acfdd9 | |
Tim Gröger | 1e64cc3f60 | |
Tim Gröger | 0cdfe7f11c | |
Tim Gröger | 704f6fd3fe | |
Tim Gröger | 2411fc86cd | |
Tim Gröger | 644f225428 | |
Tim Gröger | 8409e09f19 | |
Tim Gröger | 1ad39f386e | |
Ferdinand Thiessen | ef3fcc48a7 | |
Ferdinand Thiessen | 82d4b52e24 | |
Ferdinand Thiessen | bea9f9f5dc | |
Tim Gröger | 01afa232c4 | |
Tim Gröger | 4324681b75 | |
Ferdinand Thiessen | ee67f691d3 | |
Tim Gröger | a23a17285b | |
Tim Gröger | 10c1b57c64 | |
Tim Gröger | 61a679dfb1 | |
Tim Gröger | c5799967af | |
Tim Gröger | dddafef3a1 | |
Tim Gröger | 6d56d5847f | |
Tim Gröger | 2ee3cb0dbc | |
Dominik | 09f72a2893 | |
Tim Gröger | 4f64933555 | |
Tim Gröger | f23be34a77 | |
Tim Gröger | 0563740e9c | |
Tim Gröger | f212c5962c | |
Tim Gröger | 3d311d3677 | |
Tim Gröger | 10eccba914 | |
Tim Gröger | 61dd94c523 | |
Tim Gröger | b92a94adb0 | |
Tim Gröger | affaa639d8 | |
Ferdinand Thiessen | c379656f3e | |
Ferdinand Thiessen | c83ba2d20d | |
Tim Gröger | f8e486bad9 | |
Tim Gröger | 3ea93fb800 | |
Tim Gröger | c8a6ab7d35 | |
Tim Gröger | 6c5c67f45b | |
Tim Gröger | 0fcbbe23c2 | |
Tim Gröger | 8a442d029b | |
Tim Gröger | a28bbe8e0a | |
Tim Gröger | 6e4d3a8a01 | |
Tim Gröger | 450f691b9c | |
Tim Gröger | 72e2606ed2 | |
Tim Gröger | a91384546b | |
Tim Gröger | 8ac74c9f64 | |
Snowmee | 22c5ebce1b | |
Snowmee | d5393d75c5 |
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
|
@ -0,0 +1,8 @@
|
|||
/dist
|
||||
/src-bex/www
|
||||
/src-capacitor
|
||||
/src-cordova
|
||||
/.quasar
|
||||
/node_modules
|
||||
/src-ssr
|
||||
.*
|
|
@ -0,0 +1,91 @@
|
|||
const { resolve } = require('path');
|
||||
module.exports = {
|
||||
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
|
||||
// This option interrupts the configuration hierarchy at this file
|
||||
// Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
|
||||
root: true,
|
||||
|
||||
// https://eslint.vuejs.org/user-guide/#how-to-use-custom-parser
|
||||
// Must use parserOptions instead of "parser" to allow vue-eslint-parser to keep working
|
||||
// `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted
|
||||
parserOptions: {
|
||||
// https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser#configuration
|
||||
// https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#eslint
|
||||
// Needed to make the parser take into account 'vue' files
|
||||
extraFileExtensions: ['.vue'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
project: resolve(__dirname, './tsconfig.json'),
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features
|
||||
sourceType: 'module', // Allows for the use of imports
|
||||
},
|
||||
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
|
||||
// Rules order is important, please avoid shuffling them
|
||||
extends: [
|
||||
// Base ESLint recommended rules
|
||||
// 'eslint:recommended',
|
||||
|
||||
// https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage
|
||||
// ESLint typescript rules
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
// consider disabling this class of rules if linting takes too long
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
|
||||
// Uncomment any of the lines below to choose desired strictness,
|
||||
// but leave only one uncommented!
|
||||
// See https://eslint.vuejs.org/rules/#available-rules
|
||||
// 'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
|
||||
// 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
|
||||
'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
|
||||
|
||||
// https://github.com/prettier/eslint-config-prettier#installation
|
||||
// usage with Prettier, provided by 'eslint-config-prettier'.
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
|
||||
plugins: [
|
||||
// required to apply rules which need type information
|
||||
'@typescript-eslint',
|
||||
|
||||
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file
|
||||
// required to lint *.vue files
|
||||
'vue',
|
||||
],
|
||||
|
||||
globals: {
|
||||
ga: true, // Google Analytics
|
||||
cordova: true,
|
||||
__statics: true,
|
||||
__QUASAR_SSR__: true,
|
||||
__QUASAR_SSR_SERVER__: true,
|
||||
__QUASAR_SSR_CLIENT__: true,
|
||||
__QUASAR_SSR_PWA__: true,
|
||||
process: true,
|
||||
Capacitor: true,
|
||||
chrome: true,
|
||||
},
|
||||
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
// VueStuff
|
||||
// Defaults to error on eslint-plugin-vue 8.0.3, but let us be not too strict with names
|
||||
'vue/multi-word-component-names': 'off',
|
||||
|
||||
// Rejects on promises should always be of the Error type (and allow empty rejects as well)
|
||||
'prefer-promise-reject-errors': ['error', { allowEmptyReject: true }],
|
||||
|
||||
// Allow " if ' is contained inside the string, so we can avoid escaping
|
||||
quotes: ['error', 'single', { avoidEscape: true }],
|
||||
|
||||
// TypeScript, let us be not too strict
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
|
||||
// allow debugger during development only
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
},
|
||||
};
|
|
@ -1,10 +1,30 @@
|
|||
.DS_Store
|
||||
.thumbs.db
|
||||
node_modules
|
||||
|
||||
# We use yarn, so ignore npm
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Quasar core related directories
|
||||
.quasar
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
# Cordova related directories and files
|
||||
/src-cordova/node_modules
|
||||
/src-cordova/platforms
|
||||
/src-cordova/plugins
|
||||
/src-cordova/www
|
||||
|
||||
# Capacitor related directories and files
|
||||
/src-capacitor/www
|
||||
/src-capacitor/android
|
||||
/src-capacitor/ios
|
||||
/src-capacitor/node_modules
|
||||
|
||||
# BEX related directories and files
|
||||
/src-bex/www
|
||||
/src-bex/js/core
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
|
@ -13,9 +33,7 @@ yarn-error.log*
|
|||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
yarn-error.log
|
||||
.woodpecker/
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
// to edit target browsers: use "browserslist" field in package.json
|
||||
require('autoprefixer'),
|
||||
],
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
module.exports = {
|
||||
singleQuote: true,
|
||||
semi: false
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"octref.vetur"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"hookyqr.beautify",
|
||||
"dbaeumer.jshint",
|
||||
"ms-vscode.vscode-typescript-tslint-plugin"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
},
|
||||
"javascript.format.insertSpaceBeforeFunctionParenthesis": true,
|
||||
"javascript.format.placeOpenBraceOnNewLineForControlBlocks": false,
|
||||
"javascript.format.placeOpenBraceOnNewLineForFunctions": false,
|
||||
"typescript.format.insertSpaceBeforeFunctionParenthesis": true,
|
||||
"typescript.format.placeOpenBraceOnNewLineForControlBlocks": false,
|
||||
"typescript.format.placeOpenBraceOnNewLineForFunctions": false,
|
||||
"vetur.format.defaultFormatter.html": "prettier",
|
||||
"vetur.format.defaultFormatter.js": "prettier-eslint",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"vetur.format.defaultFormatterOptions": {
|
||||
|
||||
"js-beautify-html": {
|
||||
"wrap_attributes": "force-expand-multiline"
|
||||
},
|
||||
"prettyhtml": {
|
||||
"printWidth": 100,
|
||||
"singleQuote": false,
|
||||
"wrapAttributes": false,
|
||||
"sortAttributes": false
|
||||
}
|
||||
},
|
||||
"vetur.format.defaultFormatter.ts": "prettier-tslint",
|
||||
"typescript.format.enable": false,
|
||||
"prettier.configPath": "./package.json"
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
pipeline:
|
||||
deploy:
|
||||
when:
|
||||
event: tag
|
||||
tag: "@flaschengeist/api-v*"
|
||||
image: node:lts-alpine
|
||||
commands:
|
||||
- cd api
|
||||
- echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" > .npmrc
|
||||
- yarn publish --non-interactive
|
||||
secrets: [ node_auth_token ]
|
||||
|
||||
depends_on:
|
||||
- lint
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
pipeline:
|
||||
lint:
|
||||
when:
|
||||
branch: [main, develop]
|
||||
image: node:lts-alpine
|
||||
commands:
|
||||
- yarn install
|
||||
- yarn lint
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright 2021 Tim Gröger | Flaschengeist Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
92
README.md
|
@ -1,24 +1,88 @@
|
|||
# newgruecht-vue
|
||||
# Flaschengeist (frontend)
|
||||
![status-badge](https://ci.os-sc.org/api/badges/Flaschengeist/flaschengeist-frontend/status.svg)
|
||||
|
||||
|
||||
Modular student club administration system, licensed under the MIT license.
|
||||
|
||||
## Installation
|
||||
|
||||
### Requirements
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
"engines": {
|
||||
"node": ">= 14.18.1",
|
||||
"npm": ">= 6.14.12",
|
||||
"yarn": ">= 1.22.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
So on debian (buster and bullseye) you will need to install node.js and yarn beside the debian packages to meet the needed versions.
|
||||
|
||||
```bash
|
||||
pushd ~/opt
|
||||
wget https://nodejs.org/dist/latest-v16.x/node-v16.13.0-linux-x64.tar.xz
|
||||
tar -xJf node-v16.13.0-linux-x64.tar.xz
|
||||
export PATH="$(pwd)/node-v16.13.0-linux-x64/bin":"$PATH"
|
||||
npm i -g yarn
|
||||
npm i -g @quasar/cli
|
||||
popd
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
### Install the dependencies
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
Be aware npm might not work.
|
||||
|
||||
### Configure Plugins
|
||||
|
||||
#### Installing a plugin
|
||||
|
||||
Simply add it as a dependency and install it, for example installing the `pricelist`-plugin:
|
||||
|
||||
```sh
|
||||
yarn add '@flaschengeist/pricelist'
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
#### Enable / Disable a plugin
|
||||
|
||||
After installing a plugin you will have to enable it,
|
||||
this is done by adding it to the `plugin.config.js` file.
|
||||
For the example above the file should look like:
|
||||
|
||||
```js
|
||||
module.exports = [
|
||||
// pricelist plugin:
|
||||
'@flaschengeist/pricelist',
|
||||
];
|
||||
```
|
||||
|
||||
Remember to rebuild the project
|
||||
|
||||
### Configure Backend
|
||||
|
||||
The application is using the API of [the backend](https://flaschengeist.dev/Flaschengeist/flaschengeist)
|
||||
This access needs to be configured in `src/config.ts'->config.baseURL
|
||||
|
||||
- either you do have a proxy webserver that maps the '/api' to the backend (http://localhost:5000) or
|
||||
- you do directly configure the backend there:`baseURL: 'http://localhost:5000'`. Be aware not committing this configuration.
|
||||
|
||||
### Build the application
|
||||
|
||||
```sh
|
||||
yarn quasar build
|
||||
```
|
||||
|
||||
### Notes on mobile apps (Cordova)
|
||||
|
||||
For mobile applications older web engines should or must be supported,
|
||||
as manufaturer often do not update their phones, so for building cordova apps set the `BROWSERSLIST_ENV` environment variable to
|
||||
`BROWSERSLIST_ENV=cordova`.
|
||||
This will produce ECDMAscript compatible with iOS 13+ and Android Webview 76 (relased October 2019).
|
||||
|
||||
## Development
|
||||
|
||||
Please refer to our [development wiki](https://flaschengeist.dev/Flaschengeist/flaschengeist/wiki/Development).
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
<template>
|
||||
<q-input
|
||||
v-model="dateTime"
|
||||
filled
|
||||
:readonly="readonly"
|
||||
:label="label"
|
||||
:placeholder="placeholder"
|
||||
:rules="customRules"
|
||||
:clearable="clearable"
|
||||
v-bind="attrs"
|
||||
@clear="dateTime = ''"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon v-if="'date' || type == 'datetime'" name="mdi-calendar" class="cursor-pointer">
|
||||
<q-popup-proxy ref="qDateProxy" transition-show="scale" transition-hide="scale">
|
||||
<q-date v-model="date" mask="YYYY-MM-DD">
|
||||
<div class="row items-center justify-end">
|
||||
<q-btn v-close-popup label="Schließen" color="primary" flat />
|
||||
</div>
|
||||
</q-date>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
<q-icon
|
||||
v-if="type == 'time' || type == 'datetime'"
|
||||
name="mdi-clock-outline"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<q-popup-proxy ref="qTimeProxy" transition-show="scale" transition-hide="scale">
|
||||
<q-time v-model="time" mask="HH:mm">
|
||||
<div class="row items-center justify-end">
|
||||
<q-btn v-close-popup label="Schließen" color="primary" flat />
|
||||
</div>
|
||||
</q-time>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from 'vue';
|
||||
import { date as q_date } from 'quasar';
|
||||
import { stringIsDate, stringIsTime, stringIsDateTime, Validator } from '..';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'IsoDateInput',
|
||||
props: {
|
||||
modelValue: { type: Object as PropType<Date | undefined>, default: undefined },
|
||||
type: {
|
||||
type: String,
|
||||
default: 'date',
|
||||
validator: (value: string) => ['date', 'time', 'datetime'].indexOf(value) !== -1,
|
||||
},
|
||||
label: { type: String, default: 'Datum' },
|
||||
readonly: Boolean,
|
||||
rules: {
|
||||
type: Array as PropType<Validator<Date>[]>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: { 'update:modelValue': (date?: Date) => !!date || !date },
|
||||
setup(props, { emit, attrs }) {
|
||||
const customRules = computed(() => [
|
||||
props.type == 'date' ? stringIsDate : props.type == 'time' ? stringIsTime : stringIsDateTime,
|
||||
(value?: string) => {
|
||||
if (props.rules.length > 0 && !!value) {
|
||||
let date: Date | undefined = undefined;
|
||||
if (props.type == 'date') date = modifyDate(value);
|
||||
else if (props.type == 'time') date = modifyTime(value);
|
||||
else {
|
||||
const split = value.split(' ');
|
||||
date = modifyTime(split[1], modifyDate(split[0]));
|
||||
}
|
||||
for (const rule of props.rules) {
|
||||
const r = rule(date);
|
||||
if (typeof r === 'string') return r;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
const clearable = computed(() =>
|
||||
customRules.value.every((r) => (<Validator>r)(undefined) === true)
|
||||
);
|
||||
|
||||
const placeholder = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'date':
|
||||
return 'YYYY-MM-DD';
|
||||
case 'time':
|
||||
return 'HH:mm';
|
||||
case 'datetime':
|
||||
return 'YYYY-MM-DD HH:mm';
|
||||
}
|
||||
throw 'Invalid type given';
|
||||
});
|
||||
|
||||
const date = computed({
|
||||
get: () => q_date.formatDate(props.modelValue, 'YYYY-MM-DD'),
|
||||
set: (v: string) => {
|
||||
const d = modifyDate(v);
|
||||
if (d) emit('update:modelValue', d);
|
||||
},
|
||||
});
|
||||
|
||||
const time = computed({
|
||||
get: () => q_date.formatDate(props.modelValue, 'HH:mm'),
|
||||
set: (v: string) => {
|
||||
const d = modifyTime(v);
|
||||
if (d) emit('update:modelValue', d);
|
||||
},
|
||||
});
|
||||
|
||||
const dateTime = computed({
|
||||
get: () => (props.modelValue ? q_date.formatDate(props.modelValue, placeholder.value) : ''),
|
||||
set: (v: string) => {
|
||||
if (!v) emit('update:modelValue', undefined);
|
||||
switch (props.type) {
|
||||
case 'date':
|
||||
date.value = v;
|
||||
break;
|
||||
case 'time':
|
||||
time.value = v;
|
||||
break;
|
||||
case 'datetime':
|
||||
const split = v.split(' ').filter((c) => c !== '');
|
||||
if (split.length == 2) {
|
||||
const d = modifyTime(split[1], modifyDate(split[0]));
|
||||
if (d) emit('update:modelValue', d);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function modifyTime(v: string, d: Date | undefined = props.modelValue) {
|
||||
if (d && /^\d\d:\d\d$/.test(v)) {
|
||||
const split = v.split(':');
|
||||
return q_date.adjustDate(d, { hours: +split[0], minutes: +split[1] });
|
||||
}
|
||||
}
|
||||
|
||||
function modifyDate(v: string, d: Date | undefined = props.modelValue) {
|
||||
if (!d) d = q_date.buildDate({ hours: 0, minutes: 0, seconds: 0 });
|
||||
if (/^\d{4}-\d\d-\d\d$/.test(v)) {
|
||||
const split = v.split('-');
|
||||
return q_date.adjustDate(d, {
|
||||
year: +split[0],
|
||||
month: +split[1],
|
||||
date: +split[2],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
attrs,
|
||||
clearable,
|
||||
customRules,
|
||||
date,
|
||||
dateTime,
|
||||
placeholder,
|
||||
time,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<q-input v-model="password" v-bind="attrs" :label="label" :type="type">
|
||||
<template #append><q-icon :name="name" class="cursor-pointer" @click="toggle" /></template
|
||||
></q-input>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PasswordInput',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
'update:modelValue': (value: string) => !!value,
|
||||
},
|
||||
setup(props, { emit, attrs }) {
|
||||
const isPassword = ref(true);
|
||||
const type = computed(() => (isPassword.value ? 'password' : 'text'));
|
||||
const name = computed(() => (isPassword.value ? 'mdi-eye-off' : 'mdi-eye'));
|
||||
const password = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: string) => emit('update:modelValue', value),
|
||||
});
|
||||
function toggle() {
|
||||
isPassword.value = !isPassword.value;
|
||||
}
|
||||
|
||||
return {
|
||||
attrs,
|
||||
isPassword,
|
||||
name,
|
||||
password,
|
||||
toggle,
|
||||
type,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<q-avatar>
|
||||
<slot :avatar-u-r-l="avatarURL(modelValue)">
|
||||
<q-img :src="avatarURL(modelValue)" style="min-width: 100%; min-height: 100%">
|
||||
<template #error>
|
||||
<img :src="fallback" style="height: 100%" />
|
||||
</template>
|
||||
</q-img>
|
||||
</slot>
|
||||
</q-avatar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType, defineComponent } from 'vue';
|
||||
import { avatarURL } from '@flaschengeist/api';
|
||||
|
||||
/**
|
||||
* Display an avatar for an user
|
||||
*
|
||||
* Slots:
|
||||
* default - scope: {avatarURL}
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'UserAvatar',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [Object, String] as PropType<FG.User | string>,
|
||||
required: true,
|
||||
},
|
||||
showZoom: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
fallback: {
|
||||
type: String,
|
||||
default: 'no-image.svg',
|
||||
},
|
||||
},
|
||||
emits: ['error'],
|
||||
setup() {
|
||||
return {
|
||||
avatarURL,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,5 @@
|
|||
import IsoDateInput from './IsoDateInput.vue';
|
||||
import PasswordInput from './PasswordInput.vue';
|
||||
import UserAvatar from './UserAvatar.vue';
|
||||
|
||||
export { IsoDateInput, PasswordInput, UserAvatar };
|
|
@ -0,0 +1,10 @@
|
|||
export { api, pinia } from './src/internal';
|
||||
|
||||
export * from './src/stores/';
|
||||
|
||||
export * from './src/utils/datetime';
|
||||
export * from './src/utils/permission';
|
||||
export * from './src/utils/persistent';
|
||||
export * from './src/utils/user';
|
||||
export * from './src/utils/validators';
|
||||
export * from './src/utils/misc';
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"name": "@flaschengeist/api",
|
||||
"author": "Tim Gröger <flaschengeist@wu5.de>",
|
||||
"homepage": "https://flaschengeist.dev/Flaschengeist",
|
||||
"description": "Modular student club administration system",
|
||||
"bugs": {
|
||||
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"peerDependencies": {
|
||||
"@quasar/app-webpack": "^3.7.2",
|
||||
"flaschengeist": "^2.0.0",
|
||||
"pinia": "^2.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@flaschengeist/types": "^1.0.0",
|
||||
"@types/node": "^14.18.0",
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"printWidth": 100,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
}
|
|
@ -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,23 @@
|
|||
import { AxiosError } from 'axios';
|
||||
|
||||
/**
|
||||
* Check if error is an AxiosError, and optional if a specific status was returned
|
||||
*
|
||||
* @param error Thrown error to check
|
||||
* @param status If set, check if this error has set thouse status code
|
||||
*/
|
||||
export function isAxiosError(error: unknown, status?: number) {
|
||||
// Check if it is an axios error (with axios 1.0 `error instanceof AxiosError` will be possible)
|
||||
if (typeof error !== 'object' || !error || !('isAxiosError' in error)) return false;
|
||||
// Check status code if status was given
|
||||
if (status !== undefined)
|
||||
return (
|
||||
(<AxiosError>error).response !== undefined && (<AxiosError>error).response?.status === status
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export * from './main';
|
||||
export * from './session';
|
||||
export * from './user';
|
|
@ -0,0 +1,163 @@
|
|||
import { FG_Plugin } from '@flaschengeist/types';
|
||||
import { fixSession, useSessionStore, useUserStore } from '.';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { api } from '../internal';
|
||||
import { defineStore } from 'pinia';
|
||||
import { PersistentStorage } from '../utils/persistent';
|
||||
import { LocalStorage, SessionStorage } from 'quasar';
|
||||
function reviveSession() {
|
||||
return PersistentStorage.get<FG.Session>('fg_session').then((s) => fixSession(s || undefined));
|
||||
}
|
||||
|
||||
function clearPersistant() {
|
||||
void PersistentStorage.remove('fg_session');
|
||||
}
|
||||
|
||||
export function saveSession(session?: FG.Session) {
|
||||
if (session === undefined) return clearPersistant();
|
||||
PersistentStorage.set('fg_session', session).catch(() =>
|
||||
console.error('Could not save token to storage')
|
||||
);
|
||||
}
|
||||
|
||||
export const useMainStore = defineStore({
|
||||
id: 'main',
|
||||
|
||||
state: () => ({
|
||||
session: undefined as FG.Session | undefined,
|
||||
user: undefined as FG.User | undefined,
|
||||
notifications: [] as Array<FG_Plugin.Notification>,
|
||||
shortcuts: [] as Array<FG_Plugin.MenuLink>,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
loggedIn(): boolean {
|
||||
return this.session !== undefined;
|
||||
},
|
||||
currentUser(): FG.User {
|
||||
if (this.user === undefined) throw 'Not logged in, this should not be called';
|
||||
return this.user;
|
||||
},
|
||||
currentSession(): FG.Session {
|
||||
if (this.session === undefined) throw 'Not logged in, this should not be called';
|
||||
return this.session;
|
||||
},
|
||||
permissions(): string[] {
|
||||
return this.user?.permissions || [];
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
/** Ininitalize store from saved session
|
||||
* Updates session and loads current user
|
||||
*/
|
||||
async init() {
|
||||
const sessionStore = useSessionStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
try {
|
||||
this.session = await reviveSession();
|
||||
if (this.session !== undefined) {
|
||||
this.session = await sessionStore.getSession(this.session.token);
|
||||
if (this.session !== undefined) this.user = await userStore.getUser(this.session.userid);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not load token from storage', error);
|
||||
}
|
||||
},
|
||||
|
||||
async login(userid: string, password: string) {
|
||||
const userStore = useUserStore();
|
||||
try {
|
||||
const { data } = await api.post<FG.Session>('/auth', { userid, password });
|
||||
this.session = fixSession(data);
|
||||
this.user = await userStore.getUser(data.userid, true);
|
||||
return true;
|
||||
} catch ({ response }) {
|
||||
this.handleLoggedOut();
|
||||
return (<AxiosResponse | undefined>response)?.status || false;
|
||||
}
|
||||
},
|
||||
|
||||
async logout() {
|
||||
if (!this.session || !this.session.token) return false;
|
||||
|
||||
try {
|
||||
const token = this.session.token;
|
||||
await api.delete(`/auth/${token}`);
|
||||
} catch (error) {
|
||||
return false;
|
||||
} finally {
|
||||
this.handleLoggedOut();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
async requestReset(userid: string) {
|
||||
return await api
|
||||
.post('/auth/reset', { userid })
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
},
|
||||
|
||||
async resetPassword(token: string, password: string) {
|
||||
return await api
|
||||
.post('/auth/reset', { token, password })
|
||||
.then(() => true)
|
||||
.catch(({ response }) =>
|
||||
response && 'status' in response ? (<AxiosResponse>response).status : false
|
||||
);
|
||||
},
|
||||
|
||||
async loadNotifications(flaschengeist: FG_Plugin.Flaschengeist) {
|
||||
const { data } = await api.get<FG.Notification[]>('/notifications', {
|
||||
params:
|
||||
this.notifications.length > 0
|
||||
? { from: this.notifications[this.notifications.length - 1].time }
|
||||
: {},
|
||||
});
|
||||
|
||||
const notes = [] as FG_Plugin.Notification[];
|
||||
data.forEach((n) => {
|
||||
n.time = new Date(n.time);
|
||||
const plugin = flaschengeist?.plugins.filter((p) => p.id === n.plugin)[0];
|
||||
if (!plugin) console.debug('Could not find a parser for this notification', n);
|
||||
else notes.push(plugin.notification(n));
|
||||
});
|
||||
this.notifications.push(...notes);
|
||||
return notes;
|
||||
},
|
||||
|
||||
async removeNotification(id: number) {
|
||||
const idx = this.notifications.findIndex((n) => n.id === id);
|
||||
if (idx >= 0)
|
||||
try {
|
||||
this.notifications.splice(idx, 1);
|
||||
await api.delete(`/notifications/${id}`);
|
||||
} catch (error) {
|
||||
if (this.notifications.length > idx)
|
||||
this.notifications.splice(idx, this.notifications.length - idx - 1);
|
||||
}
|
||||
},
|
||||
|
||||
async getShortcuts() {
|
||||
const { data } = await api.get<Array<FG_Plugin.MenuLink>>(
|
||||
`users/${this.currentUser.userid}/shortcuts`
|
||||
);
|
||||
this.shortcuts = data;
|
||||
},
|
||||
|
||||
async setShortcuts() {
|
||||
await api.put(`users/${this.currentUser.userid}/shortcuts`, this.shortcuts);
|
||||
},
|
||||
|
||||
handleLoggedOut() {
|
||||
this.$reset();
|
||||
void clearPersistant();
|
||||
LocalStorage.clear();
|
||||
SessionStorage.clear();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default () => useMainStore;
|
|
@ -0,0 +1,71 @@
|
|||
import { AxiosResponse } from 'axios';
|
||||
import { defineStore } from 'pinia';
|
||||
import { api } from '../internal';
|
||||
import { isAxiosError, useMainStore } from '.';
|
||||
|
||||
export function fixSession(s?: FG.Session) {
|
||||
return !s ? s : Object.assign(s, { expires: new Date(s.expires) });
|
||||
}
|
||||
|
||||
export const useSessionStore = defineStore({
|
||||
id: 'sessions',
|
||||
|
||||
state: () => ({}),
|
||||
|
||||
getters: {},
|
||||
|
||||
actions: {
|
||||
async getSession(token: string) {
|
||||
return await api
|
||||
.get(`/auth/${token}`)
|
||||
.then(({ data }: AxiosResponse<FG.Session>) => data)
|
||||
.catch(() => undefined);
|
||||
},
|
||||
|
||||
async getSessions() {
|
||||
try {
|
||||
const { data } = await api.get<FG.Session[]>('/auth');
|
||||
data.forEach(fixSession);
|
||||
|
||||
const mainStore = useMainStore();
|
||||
const currentSession = data.find((session) => {
|
||||
return session.token === mainStore.session?.token;
|
||||
});
|
||||
if (currentSession) {
|
||||
mainStore.session = currentSession;
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
return [] as FG.Session[];
|
||||
}
|
||||
},
|
||||
|
||||
async deleteSession(token: string) {
|
||||
const mainStore = useMainStore();
|
||||
if (token === mainStore.session?.token) return mainStore.logout();
|
||||
|
||||
try {
|
||||
await api.delete(`/auth/${token}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Ignore 401, as this means we are already logged out, throw all other
|
||||
if (!isAxiosError(error, 401)) throw error;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
async updateSession(lifetime: number, token: string) {
|
||||
try {
|
||||
const { data } = await api.put<FG.Session>(`auth/${token}`, { value: lifetime });
|
||||
fixSession(data);
|
||||
|
||||
const mainStore = useMainStore();
|
||||
if (mainStore.session?.token == data.token) mainStore.session = data;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,278 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { api } from '../internal';
|
||||
import { isAxiosError, useMainStore } from '.';
|
||||
import { DisplayNameMode } from '@flaschengeist/users';
|
||||
|
||||
export function fixUser(u?: FG.User) {
|
||||
return !u ? u : Object.assign(u, { birthday: u.birthday ? new Date(u.birthday) : undefined });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if state is outdated / dirty
|
||||
* Value is considered outdated after 15 minutes
|
||||
* @param updated Time of last updated (in milliseconds see Date.now())
|
||||
* @returns True if outdated, false otherwise
|
||||
*/
|
||||
function isDirty(updated: number) {
|
||||
return Date.now() - updated > 15 * 60 * 1000;
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore({
|
||||
id: 'users',
|
||||
|
||||
state: () => ({
|
||||
roles: [] as FG.Role[],
|
||||
permissions: [] as FG.Permission[],
|
||||
userSettings: {} as FG.UserSettings,
|
||||
// list of all users, include deleted ones, use `users` getter for list of active ones
|
||||
_users: [] as FG.User[],
|
||||
// Internal flags for deciding if lists need to force-loaded
|
||||
_dirty_users: 0,
|
||||
_dirty_roles: 0,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
users(state) {
|
||||
const u = state._users.filter((u) => !u.deleted);
|
||||
|
||||
switch (this.userSettings['display_name']) {
|
||||
case DisplayNameMode.FIRSTNAME_LASTNAME || DisplayNameMode.FIRSTNAME:
|
||||
u.sort((a, b) => {
|
||||
const a_lastname = a.lastname.toLowerCase();
|
||||
const b_lastname = b.lastname.toLowerCase();
|
||||
const a_firstname = a.firstname.toLowerCase();
|
||||
const b_firstname = b.firstname.toLowerCase();
|
||||
if (a_firstname === b_firstname) {
|
||||
return a_lastname < b_lastname ? -1 : 1;
|
||||
}
|
||||
return a_firstname < b_firstname ? -1 : 1;
|
||||
});
|
||||
break;
|
||||
case <string>DisplayNameMode.DISPLAYNAME:
|
||||
u.sort((a, b) => {
|
||||
const a_displayname = a.display_name.toLowerCase();
|
||||
const b_displayname = b.display_name.toLowerCase();
|
||||
return a_displayname < b_displayname ? -1 : 1;
|
||||
});
|
||||
break;
|
||||
default:
|
||||
u.sort((a, b) => {
|
||||
const a_lastname = a.lastname.toLowerCase();
|
||||
const b_lastname = b.lastname.toLowerCase();
|
||||
const a_firstname = a.firstname.toLowerCase();
|
||||
const b_firstname = b.firstname.toLowerCase();
|
||||
if (a_lastname === b_lastname) {
|
||||
return a_firstname < b_firstname ? -1 : 1;
|
||||
}
|
||||
return a_lastname < b_lastname ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
return u;
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
/** Simply filter all users by ID */
|
||||
findUser(userid: string) {
|
||||
return this._users.find((user) => user.userid === userid);
|
||||
},
|
||||
|
||||
/** Retrieve user by ID
|
||||
* @param userid ID of user to retrieve
|
||||
* @param force If set to true the user is loaded from backend even when a local copy is available
|
||||
* @returns Retrieved user (Promise) or raise an error
|
||||
* @throws Probably an AxiosError if loading failed
|
||||
*/
|
||||
async getUser(userid: string, force = false) {
|
||||
const idx = this._users.findIndex((user) => user.userid === userid);
|
||||
if (force || idx === -1 || isDirty(this._dirty_users)) {
|
||||
try {
|
||||
const { data } = await api.get<FG.User>(`/users/${userid}`);
|
||||
fixUser(data);
|
||||
if (idx === -1) this._users.push(data);
|
||||
else this._users[idx] = data;
|
||||
return data;
|
||||
} catch (error) {
|
||||
// Ignore 404, throw all other
|
||||
if (!isAxiosError(error, 404)) throw error;
|
||||
}
|
||||
} else {
|
||||
return this._users[idx];
|
||||
}
|
||||
},
|
||||
|
||||
/** Retrieve list of all users
|
||||
* @param force If set to true a fresh users list is loaded from backend even when a local copy is available
|
||||
* @returns Array of retrieved users (Promise)
|
||||
* @throws Probably an AxiosError if loading failed
|
||||
*/
|
||||
async getUsers(force = false) {
|
||||
if (force || isDirty(this._dirty_users)) {
|
||||
const { data } = await api.get<FG.User[]>('/users');
|
||||
data.forEach(fixUser);
|
||||
this._users = data;
|
||||
this._dirty_users = Date.now();
|
||||
}
|
||||
return this._users;
|
||||
},
|
||||
|
||||
/** Save modifications of user on backend
|
||||
* @param user Modified user to save
|
||||
* @throws Probably an AxiosError if request failed (404 = Invalid userid, 400 = Invalid data)
|
||||
*/
|
||||
async updateUser(user: FG.User) {
|
||||
await api.put(`/users/${user.userid}`, user);
|
||||
// Modifcation accepted by backend
|
||||
// Save modifications back to our users list
|
||||
const idx = this._users.findIndex((u) => u.userid === user.userid);
|
||||
if (idx > -1) this._users[idx] = user;
|
||||
// If user was current user, save modifications back to the main store
|
||||
const mainStore = useMainStore();
|
||||
if (user.userid === mainStore.user?.userid) mainStore.user = user;
|
||||
},
|
||||
|
||||
/** Register a new user
|
||||
* @param user User to register (id not set)
|
||||
* @returns The registered user (id set)
|
||||
* @throws Probably an AxiosError if request failed
|
||||
*/
|
||||
async createUser(user: FG.User) {
|
||||
const { data } = await api.post<FG.User>('/users', user);
|
||||
this._users.push(<FG.User>fixUser(data));
|
||||
return data;
|
||||
},
|
||||
|
||||
/** Delete an user
|
||||
* Throws if failed and resolves void if succeed
|
||||
*
|
||||
* @param user User or ID of user to delete
|
||||
* @throws Probably an AxiosError if request failed
|
||||
*/
|
||||
async deleteUser(user: FG.User | string) {
|
||||
if (typeof user === 'object') user = user.userid;
|
||||
|
||||
await api.delete(`/users/${user}`);
|
||||
this._users = this._users.filter((u) => u.userid != user);
|
||||
},
|
||||
|
||||
/** Upload an avatar for an user
|
||||
* Throws if failed and resolves void if succeed
|
||||
*
|
||||
* @param user User or ID of user
|
||||
* @param file Avatar file to upload
|
||||
* @throws Probably an AxiosError if request failed
|
||||
*/
|
||||
async uploadAvatar(user: FG.User | string, file: string | File) {
|
||||
if (typeof user === 'object') user = user.userid;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
await api.post(`/users/${user}/avatar`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/** Delete avatar of an user
|
||||
* @param user User or ID of user
|
||||
* @throws Probably an AxiosError if request failed
|
||||
*/
|
||||
async deleteAvatar(user: FG.User | string) {
|
||||
if (typeof user === 'object') user = user.userid;
|
||||
|
||||
await api.delete(`/users/${user}/avatar`);
|
||||
},
|
||||
|
||||
/** Retrieve list of all permissions
|
||||
* @param force If set to true a fresh list is loaded from backend even when a local copy is available
|
||||
* @returns Array of retrieved permissions (Promise)
|
||||
* @throws Probably an AxiosError if request failed
|
||||
*/
|
||||
async getPermissions(force = false) {
|
||||
if (force || this.permissions.length === 0) {
|
||||
const { data } = await api.get<FG.Permission[]>('/roles/permissions');
|
||||
this.permissions = data;
|
||||
}
|
||||
return this.permissions;
|
||||
},
|
||||
|
||||
/** Retrieve list of all roles
|
||||
* @param force If set to true a fresh list is loaded from backend even when a local copy is available
|
||||
* @returns Array of retrieved roles (Promise)
|
||||
* @throws Probably an AxiosError if request failed
|
||||
*/
|
||||
async getRoles(force = false) {
|
||||
if (force || isDirty(this._dirty_roles)) {
|
||||
const { data } = await api.get<FG.Role[]>('/roles');
|
||||
this.roles = data;
|
||||
this._dirty_roles = Date.now();
|
||||
}
|
||||
return this.roles;
|
||||
},
|
||||
|
||||
/** Save modifications of role on the backend
|
||||
* @param role role to save
|
||||
* @throws Probably an AxiosError if request failed
|
||||
*/
|
||||
async updateRole(role: FG.Role) {
|
||||
await api.put(`/roles/${role.id}`, role);
|
||||
|
||||
const idx = this.roles.findIndex((r) => r.id === role.id);
|
||||
if (idx != -1) this.roles[idx] = role;
|
||||
else this._dirty_roles = 0;
|
||||
},
|
||||
|
||||
/** Create a new role
|
||||
* @param role Role to create (ID not set)
|
||||
* @returns Created role (ID set)
|
||||
* @throws Probably an AxiosError if request failed
|
||||
*/
|
||||
async newRole(role: FG.Role) {
|
||||
const { data } = await api.post<FG.Role>('/roles', role);
|
||||
this.roles.push(data);
|
||||
return data;
|
||||
},
|
||||
|
||||
/** Delete a role
|
||||
* @param role Role or ID of role to delete
|
||||
* @throws Probably an AxiosError if request failed (409 if role still in use)
|
||||
*/
|
||||
async deleteRole(role: FG.Role | number) {
|
||||
if (typeof role === 'object') role = role.id;
|
||||
await api.delete(`/roles/${role}`);
|
||||
this.roles = this.roles.filter((r) => r.id !== role);
|
||||
},
|
||||
|
||||
/** Get Settings for display name mode
|
||||
* @param force If set to true a fresh list is loaded from backend even when a local copy is available
|
||||
* @throws Probably an AxiosError if request failed
|
||||
* @returns Settings for display name mode
|
||||
*/
|
||||
async getDisplayNameModeSetting(force = false): Promise<string> {
|
||||
const mainStore = useMainStore();
|
||||
if (force) {
|
||||
const { data } = await api.get<{ data: string }>(
|
||||
`users/${mainStore.currentUser.userid}/setting/display_name_mode`
|
||||
);
|
||||
this.userSettings['display_name'] = data.data;
|
||||
}
|
||||
return this.userSettings['display_name'];
|
||||
},
|
||||
|
||||
/** Set Settings for display name mode
|
||||
* @param mode New display name mode
|
||||
* @throws Probably an AxiosError if request failed
|
||||
* @returns Settings for display name mode
|
||||
*/
|
||||
async setDisplayNameModeSetting(mode: string): Promise<string> {
|
||||
const mainStore = useMainStore();
|
||||
await api.put(`users/${mainStore.currentUser.userid}/setting/display_name_mode`, {
|
||||
data: mode,
|
||||
});
|
||||
this.userSettings['display_name'] = mode;
|
||||
return mode;
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
export function formatDateTime(
|
||||
date: Date,
|
||||
useDate = true,
|
||||
useTime = false,
|
||||
useSeconds = false,
|
||||
useWeekday = false
|
||||
) {
|
||||
const dateTimeFormat = new Intl.DateTimeFormat([], {
|
||||
year: useDate ? 'numeric' : undefined,
|
||||
month: useDate ? '2-digit' : undefined,
|
||||
day: useDate ? '2-digit' : undefined,
|
||||
weekday: useWeekday ? 'long' : undefined,
|
||||
hour: useTime ? '2-digit' : undefined,
|
||||
minute: useTime ? '2-digit' : undefined,
|
||||
second: useTime && useSeconds ? '2-digit' : undefined,
|
||||
});
|
||||
return dateTimeFormat.format(date);
|
||||
}
|
||||
|
||||
export function asDate(date?: Date, placeholder = '') {
|
||||
return date ? formatDateTime(date, true) : placeholder;
|
||||
}
|
||||
|
||||
export function asHour(date?: Date, placeholder = '') {
|
||||
return date ? formatDateTime(date, false, true) : placeholder;
|
||||
}
|
||||
|
||||
export function formatStartEnd(start: Date, end?: Date) {
|
||||
const today = asDate(new Date());
|
||||
const startDate = asDate(start);
|
||||
const endDate = end ? asDate(end) : '';
|
||||
return (
|
||||
(today !== startDate ? `${startDate}, ` : '') +
|
||||
asHour(start) +
|
||||
(end ? ' - ' + (endDate !== startDate ? `${endDate}, ` : '') + asHour(end) : '')
|
||||
);
|
||||
}
|
||||
|
||||
export function startOfWeek(date: Date, startMonday = true) {
|
||||
const start = new Date(date);
|
||||
const day = date.getDay() || 7;
|
||||
if (startMonday && day !== 1) start.setHours(-24 * (day - 1));
|
||||
else if (!startMonday && day !== 7) start.setHours(-24 * day);
|
||||
return start;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { watch, WatchSource } from 'vue';
|
||||
import { LoadingBar } from 'quasar';
|
||||
|
||||
function setLoadingBar(loading: WatchSource<boolean>) {
|
||||
return watch<boolean>(loading, (loading) => {
|
||||
if (loading) LoadingBar.start(10000);
|
||||
if (!loading) LoadingBar.stop();
|
||||
});
|
||||
}
|
||||
|
||||
export default setLoadingBar;
|
|
@ -0,0 +1,3 @@
|
|||
export function clone<T>(o: T): T {
|
||||
return <T>JSON.parse(JSON.stringify(o));
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { useMainStore } from '../stores';
|
||||
|
||||
export function hasPermission(permission: string) {
|
||||
const store = useMainStore();
|
||||
return store.permissions.includes(permission);
|
||||
}
|
||||
|
||||
export function hasPermissions(needed: string[]) {
|
||||
const store = useMainStore();
|
||||
return needed.every((value) => store.permissions.includes(value));
|
||||
}
|
||||
|
||||
export function hasSomePermissions(needed: string[]) {
|
||||
const store = useMainStore();
|
||||
return needed.some((value) => store.permissions.includes(value));
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { LocalStorage, Platform } from 'quasar';
|
||||
import { Preferences } from '@capacitor/preferences';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type PersitentTypes = Date | RegExp | number | boolean | string | object;
|
||||
|
||||
export class PersistentStorage {
|
||||
static clear() {
|
||||
if (Platform.is.capacitor) return Preferences.clear();
|
||||
else return Promise.resolve(LocalStorage.clear());
|
||||
}
|
||||
|
||||
static remove(key: string) {
|
||||
if (Platform.is.capacitor) return Preferences.remove({ key: key });
|
||||
else return Promise.resolve(LocalStorage.remove(key));
|
||||
}
|
||||
|
||||
static set(key: string, value: PersitentTypes) {
|
||||
if (Platform.is.capacitor) return Preferences.set({ key, value: JSON.stringify(value) });
|
||||
else return Promise.resolve(LocalStorage.set(key, value));
|
||||
}
|
||||
|
||||
static get<T extends PersitentTypes>(key: string) {
|
||||
if (Platform.is.capacitor)
|
||||
return Preferences.get({ key }).then((v) =>
|
||||
v.value === null ? null : (JSON.parse(v.value) as T)
|
||||
);
|
||||
else return Promise.resolve(LocalStorage.getItem<T>(key));
|
||||
}
|
||||
|
||||
static keys() {
|
||||
if (Platform.is.capacitor) return Preferences.keys().then((v) => v.keys);
|
||||
else return Promise.resolve(LocalStorage.getAllKeys());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { api } from '../internal';
|
||||
|
||||
export function avatarURL(user: FG.User | string, thumbnail = true) {
|
||||
if (typeof user === 'object') user = user.userid;
|
||||
return `${api.defaults?.baseURL || ''}/users/${user}/avatar${thumbnail ? '?thumbnail' : ''}`;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
export type Validator<T = unknown> = (value?: T | null) => boolean | string;
|
||||
|
||||
export function notEmpty(val: unknown) {
|
||||
return !!val || 'Feld darf nicht leer sein!';
|
||||
}
|
||||
|
||||
export function stringIsDate(val: string) {
|
||||
return !val || /^\d{4}-\d\d-\d\d$/.test(val) || 'Datum ist nicht gültig.';
|
||||
}
|
||||
|
||||
export function stringIsTime(val: string) {
|
||||
return !val || /^\d\d:\d\d$/.test(val) || 'Zeit ist nicht gültig.';
|
||||
}
|
||||
|
||||
export function stringIsDateTime(val: string) {
|
||||
return !val || /^\d{4}-\d\d-\d\d \d\d:\d\d$/.test(val) || 'Datum und Zeit ist nicht gültig.';
|
||||
}
|
||||
|
||||
export function isEmail(val: string) {
|
||||
return (
|
||||
!val || /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w\w+)+$/.test(val) || 'E-Mail ist nicht gültig.'
|
||||
);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "@quasar/app/tsconfig-preset",
|
||||
"target": "esnext",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"lib": ["es2020", "dom"],
|
||||
"types": ["@flaschengeist/types", "@quasar/app", "node"]
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
/* eslint-env node */
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
presets: ['@quasar/babel-preset-app'],
|
||||
};
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDazCCAlOgAwIBAgIJAJGH2ozWvd1RMA0GCSqGSIb3DQEBCwUAMFMxCzAJBgNV
|
||||
BAYTAkRFMQ8wDQYDVQQIDAZTYXhvbnkxEDAOBgNVBAcMB0RyZXNkZW4xITAfBgNV
|
||||
BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDAxMTcwOTA0MDFaFw0z
|
||||
MDAxMDQwOTA0MDFaMEQxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZTYXhvbnkxEDAO
|
||||
BgNVBAcMB0RyZXNkZW4xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcN
|
||||
AQEBBQADggEPADCCAQoCggEBALlkr1UOQypLKicESRnse52d5mAX9MjZQpH0/Y5u
|
||||
V5WxpPSasmOpt4MRj5MWTfTK2ukj/jLtPAMsggUh7wMXb1uytHj7T5mtiahXBM0H
|
||||
1sUi2nScXR6doQZlmqKWDGrVS7WHULM01WhirsnxI8S8e6Evpk4F5/RafKA8FgYI
|
||||
Ongg6S1B16+7T0e/FnILoMjKr1jpgzXnVkPFIneu/qVevSNco5/aw+bc6sjeS/ZA
|
||||
65dXFGpDlw0lPRHLT5/CgNyMyiLYov7KwMycZw7uxa1ynO+73tqe5tvO/DiMpAPJ
|
||||
EkrSz/StYBsGJxDhwq5RT31tHVtHhTf0rk1BmaoQJ0Aq7iECAwEAAaNRME8wHwYD
|
||||
VR0jBBgwFoAUt8P5gBfN9hCUAiWhtPH5fTWnctAwCQYDVR0TBAIwADALBgNVHQ8E
|
||||
BAMCBPAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQCD
|
||||
fBByVq8AbV1DMrY+MElb/nZA5/cuGnUpBpjSlk5OnYHWtywuQk6veiiJ0S2fNfqf
|
||||
RzwOFuZDHKmIcH0574VssLfUynMKP3w3xb2ZNic3AxAdhzZ6LXLx6+qF5tYcL7oC
|
||||
UWmj5Mo9SkX5HZLEGamQlVyGOGKNatxep4liyoSeKXr0AOHYfB4AkDhVZn7yQc/v
|
||||
But42fLBg4mE+rk4UBYOHA4XdoFwqgTCNZq2RxKzvG9LIcok6lOc6gDnfTsH8GqE
|
||||
byGpfIIQAXF8aftCm4dGXxtzMh8C5d0t2Ell9g+Rr8i/enebT2nJ9B9ptldDjhcZ
|
||||
7I0ywGsXwrh0EwFsX74/
|
||||
-----END CERTIFICATE-----
|
|
@ -1,28 +0,0 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC5ZK9VDkMqSyon
|
||||
BEkZ7HudneZgF/TI2UKR9P2ObleVsaT0mrJjqbeDEY+TFk30ytrpI/4y7TwDLIIF
|
||||
Ie8DF29bsrR4+0+ZrYmoVwTNB9bFItp0nF0enaEGZZqilgxq1Uu1h1CzNNVoYq7J
|
||||
8SPEvHuhL6ZOBef0WnygPBYGCDp4IOktQdevu09HvxZyC6DIyq9Y6YM151ZDxSJ3
|
||||
rv6lXr0jXKOf2sPm3OrI3kv2QOuXVxRqQ5cNJT0Ry0+fwoDcjMoi2KL+ysDMnGcO
|
||||
7sWtcpzvu97anubbzvw4jKQDyRJK0s/0rWAbBicQ4cKuUU99bR1bR4U39K5NQZmq
|
||||
ECdAKu4hAgMBAAECggEABoMQ3Y34sf2d52zxHGYAGZM4SlvND1kCS5otZdleXjW1
|
||||
M5pTdci6V3JAdswrxNNzSQkonqVSnFHt5zw/5v3lvXTTfgRl0WIVGcKkuobx9k65
|
||||
Gat8YdzrkQv0mI1otj/zvtaX8ROEA3yj4xgDR5/PP+QqlUcD1MNw6TfzFhcn5pxB
|
||||
/RDPmvarMhzMdDW60Uub6Z7e/kVPuXWrW4bDyULd1d1NoSibnFZi+vGY0Lc1ctDW
|
||||
2Vl7A8RFTcQi6Cjx/FwgPGJTBE4UMjIBO3wnoPQBMrsSxeGhcarerqIlEafgT4XN
|
||||
p9BMtRyaXE7TTb1BXc35ZYNJLDLJKQxABhrEHtFreQKBgQDpiGwuKAFK8BLPlbAx
|
||||
zkShhKd9fhlwm2bfRv3cojPQZsxn0BjefmtrISbKCD79Ivyn7TnOyYAoKAxdp2q9
|
||||
wtz94aAXV2lfhUw2lhcb/aw4sXuY/s1XnVyoglOO8pYRCUN0o80pKuWFsaDyy/uL
|
||||
LhINff1oMNCa7vmMdu8Ccz0o/wKBgQDLOqdTQhSFs4f1yhlDDH3pqT6eKvtFNeRJ
|
||||
usxYDnAyRXHRqwhQ86z1nBZIgwXqq7PfO9V5Y/l6/2HmmA2ufjS8aBTNpCUMuvJk
|
||||
y98Z4hTjKRdnVlMUjHq9ahCixJVQ8pcCnWRFdeAwSKhHQiJEFLYeYOIrUeCIYJI4
|
||||
FiCshSPI3wKBgGU0ErWZ7p18FprRIs8itYlNhIwUxo+POPCPwloIDO5GblSa0Pwy
|
||||
yvhdIIMzOaDXtahMXN3pYtmEKX+4msBrnvuC+K7E2cxkZtfNCWy+7RCQkaCG45QR
|
||||
hOMdv3pWVIRDgHEevz0U8uySQs6VaYgySe6A5/1sEiriX1DpBcEJEbsfAoGAKUCb
|
||||
rGvSbJ1XsM24OQL1IBQJsON6o77fuxOe3RT5M0sjYnL8OipsZmKrp0ZpUgxOc7ba
|
||||
i0x+3LewMLWWuV/G5qOd7WwvVRkxkMJNZByfLskthf1g2d/2HjLEc7XBtW+4tYAr
|
||||
VWoq+sIU3noPKJCnsxzpa++vyx8HLzlWoo5YCDMCgYBJvGH2zMgInlQNO/2XY5nl
|
||||
E53EZMex+RDq8Wzr4tRM3IrCGc2t8WKEQ/9teKNH0tg9xib0vhqqmiGl1xNfqJVo
|
||||
ePJyfgFabeUx9goG3mgTdV9woSRlBJso62dM0DAC/jsJoHnVzgokysR4/BfW9Da+
|
||||
AYTxRZSNbfmsTHawXqG8Fw==
|
||||
-----END PRIVATE KEY-----
|
115
package.json
|
@ -1,61 +1,78 @@
|
|||
{
|
||||
"name": "newgruecht-vue",
|
||||
"version": "1.0.1",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"version": "2.1.0",
|
||||
"productName": "flaschengeist-frontend",
|
||||
"name": "flaschengeist",
|
||||
"author": "Tim Gröger <flaschengeist@wu5.de>",
|
||||
"homepage": "https://flaschengeist.dev/Flaschengeist",
|
||||
"description": "Modular student club administration system",
|
||||
"bugs": {
|
||||
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
"format": "prettier --config ./package.json --write '{,!(node_modules|dist|.*)/**/}*.{js,ts,vue}'",
|
||||
"lint": "eslint --ext .js,.ts,.vue ./src ./api"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^4.9.95",
|
||||
"@mdi/js": "^4.9.95",
|
||||
"core-js": "^3.6.5",
|
||||
"vue": "^2.6.10",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuetify": "^2.2.29",
|
||||
"vuex": "^3.4.0"
|
||||
"@flaschengeist/api": "^1.0.0",
|
||||
"@flaschengeist/balance": "^1.0.0",
|
||||
"@flaschengeist/pricelist-old": "^1.0.0",
|
||||
"@flaschengeist/schedule": "^1.0.0",
|
||||
"@flaschengeist/users": "^1.0.0",
|
||||
"axios": "^1.4.0",
|
||||
"pinia": "^2.0.8",
|
||||
"quasar": "^2.11.10",
|
||||
"vue": "^3.0.0",
|
||||
"vue-router": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.3.1",
|
||||
"@vue/cli-plugin-eslint": "^4.3.1",
|
||||
"@vue/cli-plugin-router": "^4.3.1",
|
||||
"@vue/cli-plugin-vuex": "^4.3.1",
|
||||
"@vue/cli-service": "^4.3.1",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"axios": "^0.19.2",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"material-design-icons-iconfont": "^5.0.1",
|
||||
"prettier": "^1.19.1",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"vue-cli-plugin-vuetify": "^2.0.5",
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
"vuetify-loader": "^1.4.4"
|
||||
"@capacitor/core": "^5.0.0",
|
||||
"@capacitor/preferences": "^5.0.0",
|
||||
"@flaschengeist/types": "^1.0.0",
|
||||
"@quasar/app-webpack": "^3.7.2",
|
||||
"@quasar/extras": "^1.16.3",
|
||||
"@types/node": "^14.18.0",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@types/webpack-env": "^1.16.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.8.0",
|
||||
"@typescript-eslint/parser": "^5.8.0",
|
||||
"@vue/devtools": "^6.5.0",
|
||||
"eslint": "^8.5.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^9.14.1",
|
||||
"eslint-webpack-plugin": "^4.0.1",
|
||||
"modify-source-webpack-plugin": "^4.1.0",
|
||||
"prettier": "^2.5.1",
|
||||
"typescript": "^4.5.4",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"printWidth": 100,
|
||||
"arrowParens": "always"
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"plugin:prettier/recommended",
|
||||
"@vue/prettier",
|
||||
"eslint:recommended"
|
||||
"browserslist": {
|
||||
"defaults": [
|
||||
"Firefox esr",
|
||||
"last 6 Chrome versions",
|
||||
"last 4 Firefox versions",
|
||||
"last 4 Edge versions",
|
||||
"last 4 Safari versions",
|
||||
"last 4 ChromeAndroid versions",
|
||||
"last 1 FirefoxAndroid versions"
|
||||
],
|
||||
"rules": {
|
||||
"no-console": "off"
|
||||
},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions"
|
||||
"cordova": [
|
||||
"iOS >= 13.0",
|
||||
"Android >= 76",
|
||||
"ChromeAndroid >= 76"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.18.1",
|
||||
"npm": ">= 6.14.12",
|
||||
"yarn": ">= 1.22.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
// You can add your plugins here
|
||||
module.exports = [
|
||||
// '@flaschengeist/balance',
|
||||
// '@flaschengeist/schedule',
|
||||
// '@flaschengeist/pricelist',
|
||||
'@flaschengeist/schedule',
|
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 49 KiB |
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="1024"
|
||||
height="1024"
|
||||
viewBox="0 0 270.93333 270.93334"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
|
||||
sodipodi:docname="flaschengeist-logo-white.svg"
|
||||
inkscape:export-filename="/Users/crimsen/git/flaschengeist-frontend/public/flaschengeist-logo.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.7"
|
||||
inkscape:cx="391.55984"
|
||||
inkscape:cy="526.94717"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="1680"
|
||||
inkscape:window-height="997"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:document-rotation="0" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Ebene 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-26.06665)">
|
||||
<circle
|
||||
id="path10"
|
||||
cx="135.46666"
|
||||
cy="161.53333"
|
||||
style="stroke-width:0.26506463;fill:#1976d2;fill-opacity:1;opacity:0"
|
||||
r="135.46666" />
|
||||
<path
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.44061947;stroke-opacity:1"
|
||||
d="m 154.04963,46.75413 c -2.62014,0.516924 -5.22545,1.168424 -7.80617,1.95206 -42.75163,12.981828 -43.91253,68.68501 -54.129684,109.64785 -13.512037,49.65839 -58.120549,32.45922 -53.364321,57.25551 4.247764,22.14545 69.262455,44.34715 71.908285,44.72513 7.43909,1.06272 -52.780019,-26.79368 -40.437456,-42.16974 10.871821,-13.54384 54.907216,-1.28617 101.792266,-18.34148 41.23972,-15.24969 76.0405,-52.05406 67.35884,-95.66274 -8.0739,-40.556134 -44.95534,-65.37088 -85.32176,-57.40659 z m 2.80071,29.231473 5.1e-4,-9.9e-5 c 2.13365,-0.334266 3.95652,0.01931 5.31987,1.031879 4.77648,3.547266 2.89647,14.166887 -4.19904,23.719127 -7.09532,9.55196 -16.7189,14.41932 -21.49476,10.87151 -4.77606,-3.54747 -2.89605,-14.166665 4.19913,-23.718615 4.83297,-6.506353 11.08277,-11.106027 16.17429,-11.903802 z m 47.56936,23.874148 c 2.13386,-0.334401 3.95691,0.01915 5.32037,1.031789 4.77577,3.54775 2.89554,14.16692 -4.19964,23.7187 -7.09514,9.55189 -16.7186,14.41945 -21.49466,10.87203 -4.77578,-3.54775 -2.89554,-14.16693 4.19965,-23.71872 4.83296,-6.50635 11.08277,-11.10602 16.17428,-11.903799 z M 151.12801,132.2755 c 2.17877,-0.55401 4.13861,-0.53197 5.77959,0.065 7.31131,2.65972 6.76636,15.88363 -1.21719,29.5365 -7.98356,13.65282 -20.38249,22.56458 -27.69389,19.90504 -7.31132,-2.65972 -6.76637,-15.88364 1.21719,-29.53651 5.96208,-10.19594 14.66479,-18.12654 21.9143,-19.97007 z"
|
||||
id="path897"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccsssccccccscccccccccccccccc" />
|
||||
<ellipse
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332;stroke-opacity:1"
|
||||
id="path962"
|
||||
cx="128.25729"
|
||||
cy="26.431171"
|
||||
rx="17.575893"
|
||||
ry="21.733631"
|
||||
transform="rotate(16.845913)" />
|
||||
<ellipse
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.29560357;stroke-opacity:1"
|
||||
id="path964"
|
||||
cx="245.22121"
|
||||
cy="-179.49768"
|
||||
rx="18.099041"
|
||||
ry="22.821976"
|
||||
transform="matrix(0.33166949,0.94339565,-0.88087771,0.47334392,0,0)" />
|
||||
<path
|
||||
id="path568"
|
||||
style="fill:#ffffff;stroke:#fafdff;stroke-width:0.356;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.97295,259.61975 c 0.0134,-0.25385 0.0292,-0.51409 0.0479,-0.78105 0.59479,-8.54288 2.97751,-9.88474 2.97751,-9.88474 l 5.54253,1.04351 c 0,0 1.48321,-0.66332 2.12354,-0.41427 0.64035,0.24904 1.37204,1.54389 1.37204,1.54389 l 19.02927,1.6087 c 0,0 0.94865,-0.94047 1.49023,-0.87767 0.54159,0.0628 1.11451,1.0657 1.11451,1.0657 l 5.49906,0.38814 c 0,0 0.7213,-0.79744 1.26976,-0.81461 0.54847,-0.0172 0.96423,0.87635 1.55867,0.86729 5.38159,0.0341 6.75238,-5.59098 8.60298,-8.67755 2.71811,-4.53347 7.42673,-7.64228 12.37681,-7.81427 13.99182,-0.48614 40.20387,8.31062 59.56072,9.9243 0,0 1.79066,-1.55292 6.57665,0.26655 4.78599,1.81946 9.12249,14.44555 8.18358,22.71047 -0.93889,8.26491 -3.47489,19.11211 -13.95088,21.6659 -3.19793,0.77959 -5.68428,-1.12259 -5.68428,-1.12259 -19.43608,0.50444 -53.00574,1.66081 -58.30446,0.46928 -8.87322,-1.99535 -10.84377,-6.76824 -12.19967,-11.35447 -1.60897,-5.44215 -2.79837,-6.93993 -6.77895,-7.55802 -0.51289,-0.0791 -0.87857,0.7764 -1.40831,0.66359 -0.52994,-0.11191 -1.10453,-1.28294 -1.10453,-1.28294 l -5.72273,-0.25317 c 0,0 -0.78608,0.71794 -1.26373,0.67 -0.47765,-0.048 -1.12057,-0.92034 -1.12057,-0.92034 l -18.5064,-1.6898 c 0,0 -0.84969,1.03174 -1.57251,1.09172 -0.72282,0.06 -2.20406,-1.04108 -2.20406,-1.04108 l -5.80786,0.0336 c 0,0 -2.10591,-1.65583 -1.69677,-9.52605 z m 49.4674,-3.83693 c 0,0 11.61657,-9.2335 15.58779,-9.09888 10.08861,0.34198 53.22418,5.36627 53.22418,5.36627 0,0 -51.7418,-11.56961 -56.92412,-9.67165 -3.70211,1.35585 -11.88785,13.40426 -11.88785,13.40426 z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 70 KiB |
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="1024"
|
||||
height="1024"
|
||||
viewBox="0 0 270.93333 270.93334"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
|
||||
sodipodi:docname="flaschengeist-logo.svg"
|
||||
inkscape:export-filename="/Users/crimsen/git/flaschengeist-frontend/public/flaschengeist-logo.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.7"
|
||||
inkscape:cx="391.55984"
|
||||
inkscape:cy="526.94717"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="1680"
|
||||
inkscape:window-height="997"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:document-rotation="0" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Ebene 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-26.06665)">
|
||||
<circle
|
||||
id="path10"
|
||||
cx="135.46666"
|
||||
cy="161.53333"
|
||||
style="stroke-width:0.26506463;fill:#1976d2;fill-opacity:1"
|
||||
r="135.46666" />
|
||||
<path
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.44061947;stroke-opacity:1"
|
||||
d="m 154.04963,46.75413 c -2.62014,0.516924 -5.22545,1.168424 -7.80617,1.95206 -42.75163,12.981828 -43.91253,68.68501 -54.129684,109.64785 -13.512037,49.65839 -58.120549,32.45922 -53.364321,57.25551 4.247764,22.14545 69.262455,44.34715 71.908285,44.72513 7.43909,1.06272 -52.780019,-26.79368 -40.437456,-42.16974 10.871821,-13.54384 54.907216,-1.28617 101.792266,-18.34148 41.23972,-15.24969 76.0405,-52.05406 67.35884,-95.66274 -8.0739,-40.556134 -44.95534,-65.37088 -85.32176,-57.40659 z m 2.80071,29.231473 5.1e-4,-9.9e-5 c 2.13365,-0.334266 3.95652,0.01931 5.31987,1.031879 4.77648,3.547266 2.89647,14.166887 -4.19904,23.719127 -7.09532,9.55196 -16.7189,14.41932 -21.49476,10.87151 -4.77606,-3.54747 -2.89605,-14.166665 4.19913,-23.718615 4.83297,-6.506353 11.08277,-11.106027 16.17429,-11.903802 z m 47.56936,23.874148 c 2.13386,-0.334401 3.95691,0.01915 5.32037,1.031789 4.77577,3.54775 2.89554,14.16692 -4.19964,23.7187 -7.09514,9.55189 -16.7186,14.41945 -21.49466,10.87203 -4.77578,-3.54775 -2.89554,-14.16693 4.19965,-23.71872 4.83296,-6.50635 11.08277,-11.10602 16.17428,-11.903799 z M 151.12801,132.2755 c 2.17877,-0.55401 4.13861,-0.53197 5.77959,0.065 7.31131,2.65972 6.76636,15.88363 -1.21719,29.5365 -7.98356,13.65282 -20.38249,22.56458 -27.69389,19.90504 -7.31132,-2.65972 -6.76637,-15.88364 1.21719,-29.53651 5.96208,-10.19594 14.66479,-18.12654 21.9143,-19.97007 z"
|
||||
id="path897"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccsssccccccscccccccccccccccc" />
|
||||
<ellipse
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332;stroke-opacity:1"
|
||||
id="path962"
|
||||
cx="128.25729"
|
||||
cy="26.431171"
|
||||
rx="17.575893"
|
||||
ry="21.733631"
|
||||
transform="rotate(16.845913)" />
|
||||
<ellipse
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.29560357;stroke-opacity:1"
|
||||
id="path964"
|
||||
cx="245.22121"
|
||||
cy="-179.49768"
|
||||
rx="18.099041"
|
||||
ry="22.821976"
|
||||
transform="matrix(0.33166949,0.94339565,-0.88087771,0.47334392,0,0)" />
|
||||
<path
|
||||
id="path568"
|
||||
style="fill:#ffffff;stroke:#1976d2;stroke-width:0.356;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.97295,259.61975 c 0.0134,-0.25385 0.0292,-0.51409 0.0479,-0.78105 0.59479,-8.54288 2.97751,-9.88474 2.97751,-9.88474 l 5.54253,1.04351 c 0,0 1.48321,-0.66332 2.12354,-0.41427 0.64035,0.24904 1.37204,1.54389 1.37204,1.54389 l 19.02927,1.6087 c 0,0 0.94865,-0.94047 1.49023,-0.87767 0.54159,0.0628 1.11451,1.0657 1.11451,1.0657 l 5.49906,0.38814 c 0,0 0.7213,-0.79744 1.26976,-0.81461 0.54847,-0.0172 0.96423,0.87635 1.55867,0.86729 5.38159,0.0341 6.75238,-5.59098 8.60298,-8.67755 2.71811,-4.53347 7.42673,-7.64228 12.37681,-7.81427 13.99182,-0.48614 40.20387,8.31062 59.56072,9.9243 0,0 1.79066,-1.55292 6.57665,0.26655 4.78599,1.81946 9.12249,14.44555 8.18358,22.71047 -0.93889,8.26491 -3.47489,19.11211 -13.95088,21.6659 -3.19793,0.77959 -5.68428,-1.12259 -5.68428,-1.12259 -19.43608,0.50444 -53.00574,1.66081 -58.30446,0.46928 -8.87322,-1.99535 -10.84377,-6.76824 -12.19967,-11.35447 -1.60897,-5.44215 -2.79837,-6.93993 -6.77895,-7.55802 -0.51289,-0.0791 -0.87857,0.7764 -1.40831,0.66359 -0.52994,-0.11191 -1.10453,-1.28294 -1.10453,-1.28294 l -5.72273,-0.25317 c 0,0 -0.78608,0.71794 -1.26373,0.67 -0.47765,-0.048 -1.12057,-0.92034 -1.12057,-0.92034 l -18.5064,-1.6898 c 0,0 -0.84969,1.03174 -1.57251,1.09172 -0.72282,0.06 -2.20406,-1.04108 -2.20406,-1.04108 l -5.80786,0.0336 c 0,0 -2.10591,-1.65583 -1.69677,-9.52605 z m 49.4674,-3.83693 c 0,0 11.61657,-9.2335 15.58779,-9.09888 10.08861,0.34198 53.22418,5.36627 53.22418,5.36627 0,0 -51.7418,-11.56961 -56.92412,-9.67165 -3.70211,1.35585 -11.88785,13.40426 -11.88785,13.40426 z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.8 KiB |
|
@ -1,19 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>wuicon.ico">
|
||||
<title>Flaschengeist</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but newgruecht-vue doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,168 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="1024"
|
||||
height="1024"
|
||||
viewBox="0 0 270.93333 270.93334"
|
||||
version="1.1"
|
||||
id="svg37"
|
||||
inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
|
||||
sodipodi:docname="no-image.svg">
|
||||
<defs
|
||||
id="defs31">
|
||||
<rect
|
||||
x="-328.72475"
|
||||
y="24.854798"
|
||||
width="167.56944"
|
||||
height="62.537879"
|
||||
id="rect1100" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.66"
|
||||
inkscape:cx="-222.85714"
|
||||
inkscape:cy="248.57143"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1303"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata34">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Ebene 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<circle
|
||||
id="path10"
|
||||
cx="135.46666"
|
||||
cy="135.46666"
|
||||
style="fill:#1976d2;fill-opacity:1;stroke-width:0.265065;opacity:1"
|
||||
r="135.46666" />
|
||||
<path
|
||||
id="path897"
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#1976d2;stroke-width:2.4226772;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 443.15234 56.630859 A 66.428573 82.142858 16.845913 0 0 371.42188 118.32031 A 66.428573 82.142858 16.845913 0 0 411.19531 216.18945 A 66.428573 82.142858 16.845913 0 0 417.72266 217.72852 C 381.34438 302.48672 370.45756 410.53422 348.14648 499.98438 C 297.07736 687.66963 128.47878 622.66455 146.45508 716.38281 C 160.50823 789.6481 350.53291 863.07431 404.40039 881.20117 C 404.36927 881.70781 404.3327 882.22702 404.30664 882.7207 C 402.76028 912.46642 410.7207 918.72461 410.7207 918.72461 L 432.67188 918.59766 C 432.67187 918.59766 438.27004 922.75802 441.00195 922.53125 C 443.73387 922.30455 446.94531 918.40625 446.94531 918.40625 L 516.89062 924.79297 C 516.89062 924.79297 519.31971 928.09007 521.125 928.27148 C 522.93029 928.45267 525.90234 925.73828 525.90234 925.73828 L 547.53125 926.69531 C 547.53125 926.69531 549.70216 931.12195 551.70508 931.54492 C 553.70725 931.97129 555.09081 928.73815 557.0293 929.03711 C 572.07401 931.3732 576.56924 937.03281 582.65039 957.60156 C 587.77505 974.93535 595.22123 992.9761 628.75781 1000.5176 C 648.78447 1005.021 775.66385 1000.6487 849.12305 998.74219 C 849.12305 998.74219 858.5188 1005.9328 870.60547 1002.9863 C 910.19976 993.33421 919.78542 952.33706 923.33398 921.09961 C 926.88262 889.86212 910.49308 842.14037 892.4043 835.26367 C 874.31552 828.38693 867.54688 834.25781 867.54688 834.25781 C 794.38713 828.15886 695.31802 794.91067 642.43555 796.74805 C 623.72658 797.39809 605.92942 809.14688 595.65625 826.28125 C 588.66186 837.94703 583.48245 859.20701 563.14258 859.07812 C 560.89588 859.11237 559.32296 855.73577 557.25 855.80078 C 555.17708 855.86568 552.45117 858.87891 552.45117 858.87891 L 531.66797 857.41211 C 531.66797 857.41211 529.50203 853.62212 527.45508 853.38477 C 525.40816 853.14741 521.82422 856.70312 521.82422 856.70312 L 449.90234 850.62305 C 449.90234 850.62305 447.13702 845.72836 444.7168 844.78711 C 442.29665 843.84582 436.68945 846.35352 436.68945 846.35352 L 415.74219 842.4082 C 415.74219 842.4082 407.89779 846.84558 405.02539 873.69922 C 358.98121 845.1153 229.07948 771.28872 265.40039 726.04102 C 306.49077 674.8517 472.92361 721.17976 650.12695 656.71875 C 726.82706 628.35646 797.61671 580.25753 845.84766 519.08398 A 66.376657 87.827348 48.964508 0 0 927.6875 519.24805 A 66.376657 87.827348 48.964508 0 0 980.98047 413.88477 A 66.376657 87.827348 48.964508 0 0 907.37109 380.61328 C 911.18565 353.15829 910.56576 324.56748 904.71094 295.1582 C 874.19541 141.87518 734.80037 48.0882 582.23438 78.189453 C 572.33148 80.143182 562.48437 82.604632 552.73047 85.566406 C 533.35813 91.448951 516.25153 99.658419 501.06836 109.80859 A 66.428573 82.142858 16.845913 0 0 458.80469 58.953125 A 66.428573 82.142858 16.845913 0 0 443.15234 56.630859 z M 598.64062 188.20703 C 604.22194 188.22929 609.06312 189.70004 612.92773 192.57031 C 630.98057 205.9773 623.87627 246.11384 597.05859 282.2168 C 570.24164 318.31869 533.86885 336.71569 515.81836 323.30664 C 497.76711 309.89888 504.87302 269.76201 531.68945 233.66016 C 549.9558 209.06922 573.57677 191.68513 592.82031 188.66992 L 592.82227 188.66992 C 594.83831 188.35408 596.78019 188.19961 598.64062 188.20703 z M 778.42969 278.44141 C 784.01155 278.46343 788.85382 279.93226 792.71875 282.80273 C 810.7689 296.21155 803.66213 336.34605 776.8457 372.44727 C 750.02943 408.54893 713.65672 426.94663 695.60547 413.53906 C 677.55528 400.13024 684.66205 359.99578 711.47852 323.89453 C 729.74482 299.3036 753.36587 281.91757 772.60938 278.90234 C 774.62562 278.58637 776.56907 278.43406 778.42969 278.44141 z M 582.87695 399.91016 C 586.53338 399.95128 589.93604 400.53593 593.03711 401.66406 C 620.67041 411.71655 618.60959 461.69743 588.43555 513.29883 C 558.26146 564.90004 511.39926 598.58305 483.76562 588.53125 C 456.13229 578.47876 458.1931 528.49788 488.36719 476.89648 C 510.90103 438.36065 543.79364 408.38759 571.19336 401.41992 C 575.31072 400.37297 579.22053 399.86903 582.87695 399.91016 z M 643.83594 816.76172 C 685.1496 816.94824 851.34766 854.11133 851.34766 854.11133 C 851.34766 854.11133 688.31573 835.12065 650.18555 833.82812 C 635.17621 833.31932 591.27148 868.21875 591.27148 868.21875 C 591.27148 868.21875 622.20895 822.68111 636.20117 817.55664 C 637.73138 816.99622 640.33478 816.74591 643.83594 816.76172 z "
|
||||
transform="scale(0.26458333)" />
|
||||
<path
|
||||
id="path10-8"
|
||||
style="fill-opacity:1;stroke-width:3.64724414;fill:#ffffff;stroke-miterlimit:4;stroke-dasharray:none;stroke:#ffffff;stroke-opacity:1;opacity:0.80057803"
|
||||
d="M 512 0 A 511.99997 511.99997 0 0 0 0 512 A 511.99997 511.99997 0 0 0 512 1024 A 511.99997 511.99997 0 0 0 659.25 1002.3672 C 706.36552 1003.0651 793.17503 1000.1943 849.12305 998.74219 C 849.12305 998.74219 858.5188 1005.9328 870.60547 1002.9863 C 910.19976 993.33421 919.78542 952.33706 923.33398 921.09961 C 926.20746 895.80534 916.007 859.7095 902.41797 843.23633 A 511.99997 511.99997 0 0 0 1024 512 A 511.99997 511.99997 0 0 0 512 0 z "
|
||||
transform="scale(0.26458333)" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.33699;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect942-1"
|
||||
width="76.028526"
|
||||
height="73.388649"
|
||||
x="-21.278112"
|
||||
y="109.32571"
|
||||
ry="3.5350549"
|
||||
transform="rotate(-30.00892)" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.391977;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect944-7"
|
||||
width="69.4505"
|
||||
height="56.151112"
|
||||
x="-17.909185"
|
||||
y="113.14103"
|
||||
ry="0"
|
||||
transform="rotate(-30.00892)" />
|
||||
<path
|
||||
id="path10-3-94"
|
||||
style="fill:#1976d2;fill-opacity:1;stroke-width:0.0683297"
|
||||
d="m 68.884108,90.871044 a 34.92124,34.92124 0 0 0 -11.74769,43.865396 l 2.70637,4.68588 a 34.92124,34.92124 0 0 0 15.52651,12.5468 L 123.2496,124.31547 a 34.92124,34.92124 0 0 0 -2.38462,-18.10096 l -4.46922,-7.73814 A 34.92124,34.92124 0 0 0 73.568128,88.16575 Z" />
|
||||
<path
|
||||
id="path897-6-8"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#1976d2;stroke-width:0.165241;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 69.039278,95.120659 a 5.6025989,4.5307973 76.836993 0 0 -2.13225,6.090461 5.6025989,4.5307973 76.836993 0 0 5.68756,4.42371 5.6025989,4.5307973 76.836993 0 0 0.43811,-0.13196 c 0.74268,6.24695 3.78537,13.00002 5.51895,19.04423 3.38604,12.82723 -8.78931,14.73905 -4.53067,19.66107 1.76551,2.04052 6.4553,2.39549 11.0381,2.16453 l 3.43497,-1.98389 c -3.77377,-0.36303 -7.4885,-1.26121 -7.11831,-3.66758 0.68073,-4.42505 12.09089,-7.36618 20.358052,-17.21816 3.5626,-4.29154 6.10303,-9.54716 6.86491,-14.80546 a 4.5272563,5.9903123 18.955588 0 0 4.83918,-2.78197 4.5272563,5.9903123 18.955588 0 0 -0.44664,-8.041018 4.5272563,5.9903123 18.955588 0 0 -5.48235,0.545791 c -0.71126,-1.751685 -1.72325,-3.419125 -3.07226,-4.956384 -7.031112,-8.012324 -18.463402,-8.796579 -26.447502,-1.814405 -0.51824,0.453207 -1.01578,0.934617 -1.49084,1.442271 -0.94352,1.008269 -1.67389,2.076631 -2.2244,3.194051 a 5.6025989,4.5307973 76.836993 0 0 -4.23087,-1.562013 5.6025989,4.5307973 76.836993 0 0 -1.00374,0.396734 z m 13.67184,2.467287 c 0.33015,-0.189526 0.66651,-0.267379 0.99269,-0.229688 1.52358,0.176032 2.47307,2.788932 2.12069,5.836062 -0.35237,3.04705 -1.87308,5.37436 -3.3966,5.19813 -1.52351,-0.17613 -2.47289,-2.78913 -2.12054,-5.83615 0.24002,-2.07551 1.04204,-3.90793 2.07575,-4.742444 l 1.7e-4,-9.5e-5 c 0.10831,-0.08739 0.21769,-0.162799 0.32783,-0.225833 z m 13.69685,-0.803574 c 0.33018,-0.189545 0.6664,-0.267533 0.9926,-0.229843 1.52349,0.176235 2.4729,2.789172 2.12054,5.836151 -0.35234,3.04702 -1.87298,5.37441 -3.39651,5.1983 -1.52348,-0.17624 -2.4729,-2.78917 -2.12054,-5.83617 0.24,-2.075497 1.04211,-3.908161 2.07582,-4.742688 0.10834,-0.0874 0.21795,-0.162716 0.32809,-0.22575 z m -7.40627,13.845038 c 0.21745,-0.12213 0.43816,-0.20399 0.65981,-0.24312 1.97501,-0.34891 3.55829,2.67343 3.53636,6.75042 -0.0219,4.077 -1.64072,7.66488 -3.61571,8.01383 -1.975,0.34891 -3.55819,-2.67326 -3.53627,-6.75027 0.0164,-3.04468 0.93655,-5.93703 2.31716,-7.28321 0.20748,-0.20228 0.42129,-0.36535 0.63865,-0.48765 z m 17.352832,21.22847 c -0.10315,0.0588 -0.20272,0.11824 -0.29832,0.17807 -1.05105,0.65671 -1.69275,1.90208 -1.75,3.22056 l 1.74663,-1.00878 c 0.10074,-0.33823 0.21509,-0.61452 0.34506,-0.77008 0.0713,-0.0853 0.2166,-0.18885 0.42393,-0.30733 0.72661,-0.41504 2.22115,-1.01463 3.95458,-1.65068 l 5.84666,-3.37678 c -3.78613,1.29335 -7.9478,2.39117 -10.26846,3.71516 z" />
|
||||
<rect
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.33699;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect942"
|
||||
width="76.028526"
|
||||
height="73.388649"
|
||||
x="97.497429"
|
||||
y="66.694847"
|
||||
ry="3.5350549" />
|
||||
<rect
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.391977;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect944"
|
||||
width="69.4505"
|
||||
height="56.151112"
|
||||
x="100.86639"
|
||||
y="70.51017"
|
||||
ry="0" />
|
||||
<path
|
||||
id="path10-3"
|
||||
style="fill:#1976d2;fill-opacity:1;stroke-width:0.0683297"
|
||||
d="M 132.97782,70.510038 A 34.92124,34.92124 0 0 0 100.8663,102.61974 v 5.41128 a 34.92124,34.92124 0 0 0 7.17007,18.63021 h 55.29238 a 34.92124,34.92124 0 0 0 6.98797,-16.86711 v -8.93604 A 34.92124,34.92124 0 0 0 138.38694,70.510038 Z" />
|
||||
<path
|
||||
id="path897-6"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#1976d2;stroke-width:0.165241;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 130.9868,74.267588 a 4.5307973,5.6025989 16.845913 0 0 -4.89247,4.20761 4.5307973,5.6025989 16.845913 0 0 2.71268,6.675244 4.5307973,5.6025989 16.845913 0 0 0.44538,0.10485 c -2.4812,5.78097 -3.22383,13.15053 -4.74557,19.251518 -3.4832,12.80118 -14.98258,8.3674 -13.7565,14.75951 0.5083,2.64997 4.39189,5.30288 8.47586,7.39491 h 3.96671 c -3.08632,-2.20176 -5.85386,-4.8374 -4.32979,-6.73605 2.80259,-3.4914 14.15415,-0.33165 26.2404,-4.72825 5.23137,-1.93447 10.05977,-5.215 13.34938,-9.38737 a 4.5272563,5.9903123 48.964508 0 0 5.58183,0.0112 4.5272563,5.9903123 48.964508 0 0 3.63483,-7.186478 4.5272563,5.9903123 48.964508 0 0 -5.0204,-2.26929 c 0.26017,-1.87259 0.21778,-3.82264 -0.18155,-5.82851 -2.08133,-10.454754 -11.58886,-16.851564 -21.9947,-14.798494 -0.67543,0.13326 -1.34705,0.3013 -2.01232,0.50331 -1.32131,0.40122 -2.4881,0.96108 -3.52367,1.65338 a 4.5307973,5.6025989 16.845913 0 0 -2.8825,-3.46863 4.5307973,5.6025989 16.845913 0 0 -1.0676,-0.15845 z m 10.60512,8.974304 c 0.38068,10e-4 0.71089,0.10181 0.97449,0.29758 1.2313,0.91443 0.74671,3.65194 -1.08241,6.11436 -1.82906,2.46235 -4.30989,3.71712 -5.54104,2.80255 -1.23119,-0.91448 -0.74644,-3.65202 1.08259,-6.11436 1.24587,-1.67724 2.85684,-2.8629 4.16936,-3.06855 h 1.8e-4 c 0.1375,-0.0215 0.26994,-0.0321 0.39683,-0.0316 z m 12.26265,6.15442 c 0.38072,0.001 0.71088,0.10162 0.97449,0.2974 1.23112,0.91456 0.74644,3.65206 -1.08258,6.11436 -1.82902,2.46234 -4.30984,3.71721 -5.54104,2.80274 -1.23112,-0.91456 -0.74645,-3.65206 1.08258,-6.11437 1.24586,-1.67724 2.85702,-2.86307 4.16954,-3.06873 0.13752,-0.0215 0.27011,-0.0319 0.39701,-0.0314 z m -13.33783,8.28494 c 0.24939,0.003 0.48145,0.0425 0.69297,0.11947 1.88474,0.68563 1.74421,4.094668 -0.31382,7.614168 -2.05805,3.51949 -5.25426,5.8168 -7.13902,5.13121 -1.88474,-0.68563 -1.74422,-4.09448 0.31382,-7.61399 1.53693,-2.62835 3.78032,-4.672758 5.64914,-5.147988 0.28082,-0.0714 0.54752,-0.10567 0.79691,-0.10287 z m 4.40955,27.061498 c -0.11872,-7e-4 -0.23468,0.001 -0.34739,0.005 -1.2386,0.043 -2.41713,0.80049 -3.12612,1.9136 h 2.01701 c 0.2564,-0.24251 0.4936,-0.42457 0.68395,-0.49428 0.10436,-0.0382 0.28201,-0.0552 0.52081,-0.0541 0.83678,0.004 2.43085,0.23225 4.25002,0.54842 h 6.75175 c -3.92545,-0.7736 -8.07829,-1.90434 -10.75003,-1.91848 z" />
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.33699;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect942-6"
|
||||
width="76.028526"
|
||||
height="73.388649"
|
||||
x="183.4511"
|
||||
y="-21.159952"
|
||||
ry="3.5350549"
|
||||
transform="rotate(27.418518)" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.391977;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect944-6"
|
||||
width="69.4505"
|
||||
height="56.151112"
|
||||
x="186.82002"
|
||||
y="-17.344629"
|
||||
ry="0"
|
||||
transform="rotate(27.418518)" />
|
||||
<path
|
||||
id="path10-3-9"
|
||||
style="fill:#1976d2;fill-opacity:1;stroke-width:0.0683297"
|
||||
d="m 202.32517,85.418659 a 34.92124,34.92124 0 0 0 -43.2904,13.715795 l -2.49182,4.803416 a 34.92124,34.92124 0 0 0 -2.21435,19.83912 l 49.0812,25.46141 a 34.92124,34.92124 0 0 0 13.97007,-11.7545 l 4.11493,-7.93223 A 34.92124,34.92124 0 0 0 207.12667,87.90949 Z" />
|
||||
<path
|
||||
id="path897-6-3"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#1976d2;stroke-width:0.165241;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 198.82751,87.837273 a 4.5307973,5.6025989 44.264431 0 0 -6.28043,1.482039 4.5307973,5.6025989 44.264431 0 0 -0.6659,7.174546 4.5307973,5.6025989 44.264431 0 0 0.34706,0.298162 c -4.86454,3.98902 -8.91733,10.18876 -13.07755,14.90366 -8.9867,9.75921 -17.15262,0.52818 -19.00775,6.76684 -0.76908,2.58635 1.45663,6.72959 4.11849,10.46723 l 3.52111,1.82662 c -1.72575,-3.37564 -2.96872,-6.98962 -0.74155,-7.97318 4.09551,-1.80864 12.71689,6.22341 25.47003,7.88625 5.5345,0.69181 11.33116,0.003 16.17256,-2.18564 a 4.5272563,5.9903123 76.383026 0 0 4.94964,2.5803 4.5272563,5.9903123 76.383026 0 0 6.5358,-4.70541 4.5272563,5.9903123 76.383026 0 0 -3.41147,-4.3262 c 1.09325,-1.54243 1.95359,-3.29295 2.5228,-5.25738 2.96674,-10.23876 -2.52713,-20.295083 -12.70946,-23.264393 -0.66092,-0.192737 -1.33447,-0.352844 -2.01803,-0.479873 -1.35764,-0.252295 -2.65117,-0.292618 -3.88921,-0.154954 a 4.5307973,5.6025989 44.264431 0 0 -0.96143,-4.406339 4.5307973,5.6025989 44.264431 0 0 -0.87472,-0.632269 z m 5.28126,12.849707 c 0.33746,0.17619 0.58416,0.41773 0.72799,0.71289 0.67191,1.37871 -1.01884,3.58556 -3.7764,4.92908 -2.75748,1.34349 -5.53743,1.31492 -6.20913,-0.0638 -0.67178,-1.3787 1.01911,-3.58551 3.77656,-4.929 1.87826,-0.91512 3.85425,-1.22576 5.11402,-0.80391 l 1.7e-4,8e-5 c 0.13196,0.0442 0.25439,0.0958 0.3668,0.15469 z m 8.05112,11.10986 c 0.33749,0.17621 0.58422,0.41755 0.72807,0.71273 0.67168,1.37874 -1.01913,3.58554 -3.77655,4.929 -2.75744,1.3435 -5.53743,1.31502 -6.20922,-0.0637 -0.67169,-1.37874 1.01912,-3.58554 3.77655,-4.92901 1.87826,-0.91512 3.8545,-1.22583 5.11428,-0.80399 0.13197,0.0442 0.25445,0.0961 0.36687,0.15495 z m -15.65465,1.21237 c 0.21999,0.1175 0.4078,0.25943 0.56011,0.42515 1.3573,1.47651 -0.33727,4.43789 -3.7848,6.61434 -3.44753,2.17643 -7.34258,2.74386 -8.69992,1.26738 -1.3573,-1.47651 0.33717,-4.43772 3.78471,-6.61418 2.57461,-1.62536 5.50741,-2.40706 7.38513,-1.96834 0.28216,0.0659 0.53468,0.15833 0.75477,0.27565 z m -8.54725,26.05213 c -0.10506,-0.0553 -0.20878,-0.10718 -0.31067,-0.15553 -1.11927,-0.53219 -2.51422,-0.40249 -3.65614,0.2591 l 1.79043,0.92881 c 0.33927,-0.0972 0.63366,-0.14958 0.83473,-0.12381 0.11023,0.0142 0.27575,0.0809 0.48722,0.19181 0.74094,0.38887 2.05083,1.32553 3.52006,2.44389 l 5.9933,3.10909 c -3.12826,-2.49432 -6.2939,-5.41036 -8.65901,-6.65322 z" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
id="text1098"
|
||||
style="font-style:normal;font-weight:normal;font-size:22.57779999999999987px;line-height:1.35;font-family:sans-serif;white-space:pre;shape-inside:url(#rect1100);fill:#000000;fill-opacity:1;stroke:none;"
|
||||
x="52.690414"
|
||||
y="0"
|
||||
transform="translate(380.03788,147.12437)"><tspan
|
||||
x="-284.65002"
|
||||
y="47.162986"><tspan
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:22.5778px;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Bold Condensed';text-align:center;text-anchor:middle">Kein Bild </tspan></tspan><tspan
|
||||
x="-292.36704"
|
||||
y="78.223231"><tspan
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:22.5778px;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Bold Condensed';text-align:center;text-anchor:middle">vorhanden</tspan></tspan></text>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 3.4 KiB |
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* This file runs in a Node context (it's NOT transpiled by Babel), so use only
|
||||
* the ES6 features that are supported by your Node version. https://node.green/
|
||||
*/
|
||||
|
||||
// Configuration for your app
|
||||
// https://quasar.dev/quasar-cli/quasar-conf-js
|
||||
|
||||
/* eslint-env node */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||
const { ModifySourcePlugin, ReplaceOperation } = require('modify-source-webpack-plugin');
|
||||
const { configure } = require('quasar/wrappers');
|
||||
|
||||
const operation = () => {
|
||||
const custom_plgns = require('./plugin.config.js');
|
||||
const required_plgns = require('./src/vendor-plugin.config.js');
|
||||
const plugins = [...custom_plgns, ...required_plgns].map(
|
||||
(v) => `import("${v}").catch(() => "${v}")`
|
||||
);
|
||||
const replace = new ReplaceOperation(
|
||||
'all',
|
||||
`\\/\\* *INSERT_PLUGIN_LIST *\\*\\/`,
|
||||
`${plugins.join(', ')}`
|
||||
);
|
||||
return replace;
|
||||
};
|
||||
|
||||
module.exports = configure(function (/* ctx */) {
|
||||
return {
|
||||
// https://quasar.dev/quasar-cli/supporting-ts
|
||||
supportTS: {
|
||||
tsCheckerConfig: {
|
||||
eslint: {
|
||||
enabled: true,
|
||||
files: './src/**/*.{ts,tsx,js,jsx,vue}',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// https://quasar.dev/quasar-cli/prefetch-feature
|
||||
// preFetch: true,
|
||||
|
||||
// app boot file (/src/boot)
|
||||
// --> boot files are part of "main.js"
|
||||
// https://quasar.dev/quasar-cli/boot-files
|
||||
boot: ['axios', 'store', 'plugins', 'login', 'init'],
|
||||
|
||||
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css
|
||||
css: ['app.scss'],
|
||||
|
||||
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||
extras: [
|
||||
// 'eva-icons',
|
||||
// 'fontawesome-v5',
|
||||
// 'ionicons-v5',
|
||||
// 'line-awesome',
|
||||
// 'material-icons',
|
||||
'mdi-v7',
|
||||
// 'themify',
|
||||
|
||||
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
||||
'roboto-font', // optional, you are not bound to it
|
||||
],
|
||||
|
||||
// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build
|
||||
build: {
|
||||
vueRouterMode: 'history', // available values: 'hash', 'history'
|
||||
//publicPath: 'flaschengeist2',
|
||||
// transpile: false,
|
||||
|
||||
// Add dependencies for transpiling with Babel (Array of string/regex)
|
||||
// (from node_modules, which are by default not transpiled).
|
||||
// Applies only if "transpile" is set to true.
|
||||
// transpileDependencies: [],
|
||||
|
||||
// rtl: false,
|
||||
|
||||
// analyze: true,
|
||||
|
||||
// Options below are automatically set depending on the env, set them if you want to override
|
||||
// extractCSS: false,
|
||||
|
||||
// https://quasar.dev/quasar-cli/handling-webpack
|
||||
// "chain" is a webpack-chain object https://github.com/neutrinojs/webpack-chain
|
||||
chainWebpack(chain) {
|
||||
chain.plugin('eslint-webpack-plugin').use(ESLintPlugin, [
|
||||
{
|
||||
extensions: ['ts', 'js', 'vue'],
|
||||
exclude: ['node_modules', 'src-capacitor'],
|
||||
},
|
||||
]);
|
||||
chain.plugin('modify-source-webpack-plugin').use(ModifySourcePlugin, [
|
||||
{
|
||||
rules: [
|
||||
{
|
||||
test: /plugins\.ts$/,
|
||||
operations: [operation()],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
chain.merge({
|
||||
snapshot: {
|
||||
managedPaths: [],
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer
|
||||
devServer: {
|
||||
https: false,
|
||||
port: 8080,
|
||||
open: false, // opens browser window automatically
|
||||
watchFiles: { paths: ['/node_modules/@flaschengeist/**/*'] },
|
||||
},
|
||||
|
||||
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
|
||||
framework: {
|
||||
iconSet: 'mdi-v6', // Quasar icon set
|
||||
lang: 'de', // Quasar language pack
|
||||
config: {
|
||||
dark: 'auto',
|
||||
loadingBar: {
|
||||
position: 'top',
|
||||
color: 'warning',
|
||||
size: '5px',
|
||||
},
|
||||
},
|
||||
|
||||
// For special cases outside of where the auto-import stategy can have an impact
|
||||
// (like functional components as one of the examples),
|
||||
// you can manually specify Quasar components/directives to be available everywhere:
|
||||
//
|
||||
// components: [],
|
||||
// directives: [],
|
||||
|
||||
// Quasar plugins
|
||||
plugins: ['LocalStorage', 'SessionStorage', 'Dialog', 'Loading', 'Notify', 'LoadingBar'],
|
||||
},
|
||||
|
||||
// animations: 'all', // --- includes all animations
|
||||
// https://quasar.dev/options/animations
|
||||
animations: [],
|
||||
|
||||
// https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr
|
||||
ssr: {
|
||||
pwa: false,
|
||||
},
|
||||
|
||||
// https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa
|
||||
pwa: {
|
||||
workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
|
||||
workboxOptions: {}, // only for GenerateSW
|
||||
manifest: {
|
||||
name: 'Flaschengeist',
|
||||
short_name: 'Flaschengeist',
|
||||
description: 'Modular student club administration system',
|
||||
display: 'standalone',
|
||||
orientation: 'portrait',
|
||||
background_color: '#ffffff',
|
||||
theme_color: '#027be3',
|
||||
icons: [
|
||||
{
|
||||
src: 'flaschengeist-logo.svg',
|
||||
sizes: 'any',
|
||||
type: 'image/svg+xml',
|
||||
},
|
||||
{
|
||||
src: 'favicon-128x128.png',
|
||||
sizes: '128x128',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'favicon-256x256.png',
|
||||
sizes: '256x256',
|
||||
type: 'image/png',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
|
||||
cordova: {
|
||||
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
|
||||
},
|
||||
|
||||
// Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
|
||||
capacitor: {
|
||||
hideSplashscreen: true,
|
||||
},
|
||||
|
||||
// Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
|
||||
electron: {
|
||||
bundler: 'packager', // 'packager' or 'builder'
|
||||
|
||||
packager: {
|
||||
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
|
||||
// OS X / Mac App Store
|
||||
// appBundleId: '',
|
||||
// appCategoryType: '',
|
||||
// osxSign: '',
|
||||
// protocol: 'myapp://path',
|
||||
// Windows only
|
||||
// win32metadata: { ... }
|
||||
},
|
||||
|
||||
builder: {
|
||||
// https://www.electron.build/configuration/configuration
|
||||
|
||||
appId: 'flaschengeist-frontend',
|
||||
},
|
||||
|
||||
// More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration
|
||||
nodeIntegration: true,
|
||||
|
||||
extendWebpack(/* cfg */) {
|
||||
// do something with Electron main process Webpack cfg
|
||||
// chainWebpack also available besides this extendWebpack
|
||||
},
|
||||
},
|
||||
bin: {
|
||||
linuxAndroidStudio: '/home/crimsen/.local/share/JetBrains/Toolbox/scripts/studio',
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable */
|
||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
||||
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
||||
import 'quasar/dist/types/feature-flag';
|
||||
|
||||
declare module 'quasar/dist/types/feature-flag' {
|
||||
interface QuasarFeatureFlags {
|
||||
capacitor: true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"appId": "dev.flaschengeist",
|
||||
"appName": "flaschengeist-frontend",
|
||||
"bundledWebRuntime": false,
|
||||
"npmClient": "yarn",
|
||||
"webDir": "www",
|
||||
"android": {
|
||||
"minWebViewVersion": 71
|
||||
},
|
||||
"ios": {
|
||||
"allowsLinkPreview": false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Quasar</title>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="description" content="Quasar Capacitor App">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="msapplication-tap-highlight" content="no">
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, viewport-fit=cover">
|
||||
|
||||
<style>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page">
|
||||
<div>
|
||||
This file will be auto-generated. Do not edit.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Run "quasar dev" or "quasar build" with Capacitor mode.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "flaschengeist",
|
||||
"version": "2.0.0",
|
||||
"description": "Modular student club administration system",
|
||||
"author": "Tim Gröger <flaschengeist@wu5.de>",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^5.0.0-beta.0",
|
||||
"@capacitor/app": "^5.0.0",
|
||||
"@capacitor/cli": "^5.0.0",
|
||||
"@capacitor/core": "^5.0.0",
|
||||
"@capacitor/ios": "^5.0.0",
|
||||
"@capacitor/preferences": "^5.0.0",
|
||||
"@capacitor/splash-screen": "^5.0.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable */
|
||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
||||
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
||||
import 'quasar/dist/types/feature-flag';
|
||||
|
||||
declare module 'quasar/dist/types/feature-flag' {
|
||||
interface QuasarFeatureFlags {
|
||||
electron: true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { app, BrowserWindow, nativeTheme } from 'electron'
|
||||
import path from 'path'
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32' && nativeTheme.shouldUseDarkColors === true) {
|
||||
require('fs').unlinkSync(require('path').join(app.getPath('userData'), 'DevTools Extensions'))
|
||||
}
|
||||
} catch (_) { }
|
||||
|
||||
let mainWindow
|
||||
|
||||
function createWindow () {
|
||||
/**
|
||||
* Initial window options
|
||||
*/
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 600,
|
||||
useContentSize: true,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
// More info: /quasar-cli/developing-electron-apps/electron-preload-script
|
||||
preload: path.resolve(__dirname, process.env.QUASAR_ELECTRON_PRELOAD)
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.loadURL(process.env.APP_URL)
|
||||
|
||||
if (process.env.DEBUGGING) {
|
||||
// if on DEV or Production with debug enabled
|
||||
mainWindow.webContents.openDevTools()
|
||||
} else {
|
||||
// we're on production; no access to devtools pls
|
||||
mainWindow.webContents.on('devtools-opened', () => {
|
||||
mainWindow.webContents.closeDevTools()
|
||||
})
|
||||
}
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null
|
||||
})
|
||||
}
|
||||
|
||||
app.on('ready', createWindow)
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
if (mainWindow === null) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* This file is used specifically for security reasons.
|
||||
* Here you can access Nodejs stuff and inject functionality into
|
||||
* the renderer thread (accessible there through the "window" object)
|
||||
*
|
||||
* WARNING!
|
||||
* If you import anything from node_modules, then make sure that the package is specified
|
||||
* in package.json > dependencies and NOT in devDependencies
|
||||
*
|
||||
* Example (injects window.myAPI.doAThing() into renderer thread):
|
||||
*
|
||||
* const { contextBridge } = require('electron')
|
||||
*
|
||||
* contextBridge.exposeInMainWorld('myAPI', {
|
||||
* doAThing: () => {}
|
||||
* })
|
||||
*/
|
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 8.7 KiB |
230
src/App.vue
|
@ -1,232 +1,10 @@
|
|||
<template>
|
||||
<v-app>
|
||||
<TitleBar />
|
||||
<router-view />
|
||||
<v-footer app>
|
||||
<span class="px-4 d-none d-sm-flex"
|
||||
>© {{ new Date().getFullYear() }}
|
||||
<v-btn x-small text class="text-none subtitle-1" href="https://wu5.de">
|
||||
Studentenclub Wu5 e.V.
|
||||
</v-btn>
|
||||
</span>
|
||||
<span>
|
||||
<v-btn text x-small href="https://wu5.de/impressum">
|
||||
Impressum
|
||||
</v-btn>
|
||||
<v-btn text x-small href="https://wu5.de/datenschutz">
|
||||
Datenschutzerklärung
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
x-small
|
||||
v-if="isLoggedIn"
|
||||
href="https://groeger-clan.duckdns.org/redmine/projects/geruecht/issues/new"
|
||||
>
|
||||
Bugs?
|
||||
</v-btn>
|
||||
</span>
|
||||
<v-spacer />
|
||||
<div v-if="isLoggedIn && !change" :key="render">
|
||||
<v-hover
|
||||
v-slot:default="{ hover }"
|
||||
open-delay="200"
|
||||
close-delay="200"
|
||||
class="d-none d-sm-flex"
|
||||
>
|
||||
<v-sheet
|
||||
:elevation="hover ? 16 : 0"
|
||||
color="#f5f5f5"
|
||||
@click="change = !change"
|
||||
>{{ calcTime }}</v-sheet
|
||||
>
|
||||
</v-hover>
|
||||
<v-hover
|
||||
v-slot:default="{ hover }"
|
||||
open-delay="200"
|
||||
close-delay="200"
|
||||
class="d-flex d-sm-none"
|
||||
>
|
||||
<v-sheet
|
||||
:elevation="hover ? 16 : 0"
|
||||
color="#f5f5f5"
|
||||
@click="change = !change"
|
||||
>{{ calcTimeLittle }}</v-sheet
|
||||
>
|
||||
</v-hover>
|
||||
</div>
|
||||
<v-dialog v-model="change" max-width="300">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
Zeit bis zum Logout ändern
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-combobox
|
||||
solo
|
||||
:items="lifeTimes"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
v-model="selectLifeTime"
|
||||
return-object
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn text @click="change = false">Abbrechen</v-btn>
|
||||
<v-btn color="primary" text @click="save()">Speichern</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-footer>
|
||||
</v-app>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
<script>
|
||||
import TitleBar from './components/TitleBar'
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
components: { TitleBar },
|
||||
data: () => ({
|
||||
render: 0,
|
||||
timer: null,
|
||||
change: false,
|
||||
selectLifeTime: { text: '30 Minuten', value: 1800 },
|
||||
lifeTimes: [
|
||||
{
|
||||
text: '5 Minuten',
|
||||
value: 300
|
||||
},
|
||||
{
|
||||
text: '10 Minuten',
|
||||
value: 600
|
||||
},
|
||||
{
|
||||
text: '15 Minuten',
|
||||
value: 900
|
||||
},
|
||||
{
|
||||
text: '30 Minuten',
|
||||
value: 1800
|
||||
},
|
||||
{
|
||||
text: '1 Stunde',
|
||||
value: 3600
|
||||
},
|
||||
{
|
||||
text: '2 Stunden',
|
||||
value: 7200
|
||||
},
|
||||
{
|
||||
text: '3 Stunden',
|
||||
value: 10800
|
||||
},
|
||||
{
|
||||
text: '1 Tag',
|
||||
value: 86400
|
||||
},
|
||||
{
|
||||
text: '2 Tage',
|
||||
value: 172800
|
||||
},
|
||||
{
|
||||
text: '1 Woche',
|
||||
value: 604800
|
||||
},
|
||||
{
|
||||
text: '1 Monat',
|
||||
value: 2678400
|
||||
}
|
||||
]
|
||||
}),
|
||||
created() {
|
||||
if (this.isLoggedIn) {
|
||||
this.getLifeTime()
|
||||
}
|
||||
this.timer = setInterval(this.test, 1000)
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setLifeTime', 'saveLifeTime', 'logout', 'getLifeTime']),
|
||||
test() {
|
||||
if (this.isLoggedIn) {
|
||||
if (this.lifeTime == 0) this.logout()
|
||||
this.setLifeTime(this.lifeTime - 1)
|
||||
}
|
||||
},
|
||||
save() {
|
||||
this.saveLifeTime(this.selectLifeTime.value)
|
||||
this.change = false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isLoggedIn', 'lifeTime', 'getMinute', 'getSeconds']),
|
||||
calcTime() {
|
||||
var minutes = this.lifeTime / 60
|
||||
var seconds = this.lifeTime % 60
|
||||
var minutesString =
|
||||
minutes < 10 ? '0' + Math.floor(minutes % 60) : Math.floor(minutes % 60)
|
||||
var secondsString =
|
||||
seconds < 10 ? '0' + Math.floor(seconds) : Math.floor(seconds)
|
||||
if (minutes > 60) {
|
||||
var hours = minutes / 60
|
||||
if (hours > 24) {
|
||||
var days = hours / 24
|
||||
var now = new Date()
|
||||
var dayMonth = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth() + 1,
|
||||
0
|
||||
).getDate()
|
||||
if (days >= dayMonth) {
|
||||
return Math.floor(days / dayMonth) + ' Monate bis zum Logout'
|
||||
} else {
|
||||
return Math.floor(days) + ' Tage bis zum Logout'
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
Math.floor(hours) +
|
||||
':' +
|
||||
minutesString +
|
||||
':' +
|
||||
secondsString +
|
||||
' Stunden bis zum Logout'
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return minutesString + ':' + secondsString + ' Minuten bis zum Logout'
|
||||
}
|
||||
},
|
||||
calcTimeLittle() {
|
||||
var minutes = this.lifeTime / 60
|
||||
var seconds = this.lifeTime % 60
|
||||
var minutesString =
|
||||
minutes < 10 ? '0' + Math.floor(minutes % 60) : Math.floor(minutes % 60)
|
||||
var secondsString =
|
||||
seconds < 10 ? '0' + Math.floor(seconds) : Math.floor(seconds)
|
||||
if (minutes > 60) {
|
||||
var hours = minutes / 60
|
||||
if (hours > 24) {
|
||||
var days = hours / 24
|
||||
var now = new Date()
|
||||
var dayMonth = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth() + 1,
|
||||
0
|
||||
).getDate()
|
||||
if (days >= dayMonth) {
|
||||
return Math.floor(days / dayMonth) + 'M'
|
||||
} else {
|
||||
return Math.floor(days) + 'D'
|
||||
}
|
||||
} else {
|
||||
return Math.floor(hours) + ':' + minutesString + ':' + secondsString
|
||||
}
|
||||
} else {
|
||||
return minutesString + ':' + secondsString
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer)
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 322 KiB |
Before Width: | Height: | Size: 6.7 KiB |
|
@ -1 +0,0 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>
|
Before Width: | Height: | Size: 539 B |
Before Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* This boot file registers interceptors for axios
|
||||
*/
|
||||
import { useMainStore, api } from '@flaschengeist/api';
|
||||
import { AxiosError } from 'axios';
|
||||
import { boot } from 'quasar/wrappers';
|
||||
import config from 'src/config';
|
||||
import { clone } from '@flaschengeist/api';
|
||||
|
||||
/**
|
||||
* Minify data sent to backend server
|
||||
*
|
||||
* Drop unneeded entities which can be identified by ID.
|
||||
*
|
||||
* @param obj Object to minify
|
||||
* @param cloned If this entity is already cloned (JSON En+Decoded)
|
||||
* @returns Minified object (some types are converted, like a Date object is now a ISO string)
|
||||
*/
|
||||
function minify(entity: unknown, cloned = false) {
|
||||
if (!cloned) entity = clone(entity);
|
||||
|
||||
if (typeof entity === 'object') {
|
||||
const obj = entity as { [index: string]: unknown };
|
||||
|
||||
for (const prop in obj) {
|
||||
if (obj.hasOwnProperty(prop) && !!obj[prop]) {
|
||||
if (Array.isArray(obj[prop])) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
obj[prop] = (<Array<unknown>>obj[prop]).map((v) => minify(v, true));
|
||||
} else if (
|
||||
typeof obj[prop] === 'object' &&
|
||||
Object.keys(<object>obj[prop]).includes('id') &&
|
||||
typeof (<{ id: unknown }>obj[prop])['id'] === 'number' &&
|
||||
!isNaN((<{ id: number }>obj[prop])['id'])
|
||||
) {
|
||||
obj[prop] = (<{ id: unknown }>obj[prop])['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
export default boot(({ router }) => {
|
||||
// Persisted value is read in plugins.ts boot file!
|
||||
if (api.defaults.baseURL === undefined) api.defaults.baseURL = config.baseURL;
|
||||
|
||||
/***
|
||||
* Intercept requests
|
||||
* - insert Token if available
|
||||
* - minify JSON requests
|
||||
*/
|
||||
api.interceptors.request.use((config) => {
|
||||
const store = useMainStore();
|
||||
if (store.session?.token) {
|
||||
config.headers = Object.assign(config.headers || {}, {
|
||||
Authorization: `Bearer ${store.session.token}`,
|
||||
});
|
||||
}
|
||||
// Minify JSON requests
|
||||
if (
|
||||
!!config.data &&
|
||||
(config.headers === undefined ||
|
||||
config.headers['Content-Type'] === undefined ||
|
||||
config.headers['Content-Type'] === 'application/json')
|
||||
)
|
||||
config.data = minify(config.data);
|
||||
return config;
|
||||
});
|
||||
|
||||
/***
|
||||
* Intercept responses
|
||||
* - filter 401 --> handleLoggedOut
|
||||
* - filter timeout or 502-504 --> backendOffline
|
||||
*/
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const store = useMainStore();
|
||||
|
||||
if (error) {
|
||||
const e = <AxiosError>error;
|
||||
const current = router.currentRoute.value;
|
||||
if (
|
||||
e.code === 'ECONNABORTED' ||
|
||||
(e.response && e.response.status >= 502 && e.response.status <= 504)
|
||||
) {
|
||||
let next = current.path;
|
||||
if ((current.name == 'login' || current.name == 'offline') && current.query.redirect)
|
||||
next = <string>current.query.redirect;
|
||||
await router.push({
|
||||
name: 'offline',
|
||||
query: { redirect: next },
|
||||
});
|
||||
} else if (e.response && e.response.status == 401) {
|
||||
store.handleLoggedOut();
|
||||
if (current.name != 'login') {
|
||||
await router.push({
|
||||
name: 'login',
|
||||
query: { redirect: current.fullPath },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* This boot file initalizes the store from persistent storage and load all plugins
|
||||
*/
|
||||
import {
|
||||
PersistentStorage,
|
||||
api,
|
||||
isAxiosError,
|
||||
saveSession,
|
||||
useMainStore,
|
||||
} from '@flaschengeist/api';
|
||||
import { Notify, Platform } from 'quasar';
|
||||
import { loadPlugins } from './plugins';
|
||||
import { boot } from 'quasar/wrappers';
|
||||
import routes from 'src/router/routes';
|
||||
|
||||
async function loadBaseUrl() {
|
||||
try {
|
||||
const url = await PersistentStorage.get<string>('baseURL');
|
||||
if (url !== null) api.defaults.baseURL = url;
|
||||
} catch (e) {
|
||||
console.warn('Could not load BaseURL', e);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
class BackendError extends Error { }
|
||||
|
||||
/**
|
||||
* Loading backend information
|
||||
* @returns Backend object or null
|
||||
*/
|
||||
async function getBackend() {
|
||||
const { data } = await api.get<FG.Backend>('/');
|
||||
if (!data || typeof data !== 'object' || !('plugins' in data))
|
||||
throw new BackendError('Invalid backend response received');
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot file for loading baseURL + Session from PersistentStorage + loading and initializing all plugins
|
||||
*/
|
||||
export default boot(async ({ app, router }) => {
|
||||
const store = useMainStore();
|
||||
|
||||
// FIRST(!) get the base URL
|
||||
await loadBaseUrl();
|
||||
|
||||
// Init the store, load current session and user, if available
|
||||
try {
|
||||
await store.init();
|
||||
} finally {
|
||||
// Any changes on the session is written back to the persistent store
|
||||
store.$subscribe((mutation, state) => {
|
||||
saveSession(state.session);
|
||||
});
|
||||
}
|
||||
|
||||
// Load all plugins
|
||||
try {
|
||||
// Fetch backend data
|
||||
const backend = await getBackend();
|
||||
// Load enabled plugins
|
||||
const flaschengeist = await loadPlugins(backend, routes);
|
||||
// Add loaded routes to router
|
||||
flaschengeist.routes.forEach((route) => router.addRoute(route));
|
||||
// save plugins in VM-variable
|
||||
app.provide('flaschengeist', flaschengeist);
|
||||
} catch (error) {
|
||||
// Handle errors from loading the backend information
|
||||
if (error instanceof BackendError || isAxiosError(error)) {
|
||||
router.isReady().finally(() => {
|
||||
// if (Platform.is.capacitor) void router.push({ name: 'setup_backend' });
|
||||
if (Platform.is.capacitor) {
|
||||
//void router.push({ name: 'setup_backend' })
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message:
|
||||
'Backend nicht erreichbar! Prüfe deine Internetverbindung oder probiere es später nochmal.',
|
||||
timeout: 0,
|
||||
icon: 'mdi-alert-circle-outline',
|
||||
closeBtn: true,
|
||||
});
|
||||
} else void router.push({ name: 'offline', params: { refresh: 1 } });
|
||||
});
|
||||
} else if (typeof error === 'string') {
|
||||
// Handle plugin not found errors
|
||||
void router.push({ name: 'error' });
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: `Fehler beim Laden: Bitte wende dich an den Admin (${error})!`,
|
||||
timeout: 10000,
|
||||
progress: true,
|
||||
});
|
||||
} else {
|
||||
console.error('Unknown error in init.ts:', error);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* This boot file registers login / authentification related axios interceptors
|
||||
*/
|
||||
import { useMainStore, hasPermissions } from '@flaschengeist/api';
|
||||
import { boot } from 'quasar/wrappers';
|
||||
|
||||
export default boot(({ router }) => {
|
||||
/**
|
||||
* Login guard
|
||||
* Check if user tries to access the secured area and validates token
|
||||
*/
|
||||
router.beforeEach((to, from) => {
|
||||
const store = useMainStore();
|
||||
|
||||
// Skip loops
|
||||
if (to.name == 'login' && from.name == 'login') return false;
|
||||
|
||||
// Secured area '/in/...' requires to be authenticated
|
||||
if (to.path.startsWith('/in') && (!store.session || store.session.expires <= new Date())) {
|
||||
store.handleLoggedOut();
|
||||
return { name: 'login' };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Permission guard
|
||||
* Check permissions for route, cancel navigation on errors
|
||||
*/
|
||||
router.beforeResolve((to) => {
|
||||
if (!!to.meta.permissions && !hasPermissions(<FG.Permission[]>to.meta.permissions))
|
||||
return false;
|
||||
});
|
||||
});
|
|
@ -0,0 +1,295 @@
|
|||
import { FG_Plugin } from '@flaschengeist/types';
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
/****************************************************
|
||||
******** Internal area for some magic **************
|
||||
****************************************************/
|
||||
|
||||
declare type ImportPlgn = { default: FG_Plugin.Plugin };
|
||||
|
||||
function validatePlugin(plugin: FG_Plugin.Plugin) {
|
||||
return (
|
||||
typeof plugin.name === 'string' &&
|
||||
typeof plugin.id === 'string' &&
|
||||
plugin.id.length > 0 &&
|
||||
typeof plugin.version === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
// Here does some magic happens, WebPack will automatically replace the following comment with the import statements
|
||||
const PLUGINS = <Array<Promise<ImportPlgn>>>[
|
||||
/*INSERT_PLUGIN_LIST*/
|
||||
];
|
||||
|
||||
// Handle Notifications
|
||||
export const translateNotification = (note: FG.Notification): FG_Plugin.Notification => note;
|
||||
|
||||
// Combine routes, shortcuts and widgets from plugins
|
||||
|
||||
/**
|
||||
* Helper function, set permissions from MenuRoute to meta from RouteRecordRaw
|
||||
* @param object MenuRoute to set route meta
|
||||
*/
|
||||
function setPermissions(object: FG_Plugin.MenuRoute) {
|
||||
if (object.permissions !== undefined) {
|
||||
if (object.route.meta === undefined) object.route.meta = {};
|
||||
object.route.meta['permissions'] = object.permissions;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert MenuRoute to the parents RouteRecordRaw
|
||||
* @param parent Parent RouteRecordRaw
|
||||
* @param children MenuRoute to convert
|
||||
*/
|
||||
function convertRoutes(parent: RouteRecordRaw, children?: FG_Plugin.MenuRoute[]) {
|
||||
if (children !== undefined) {
|
||||
children.forEach((child) => {
|
||||
setPermissions(child);
|
||||
convertRoutes(child.route, child.children);
|
||||
if (parent.children === undefined) parent.children = [];
|
||||
parent.children.push(child.route);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines routes from plugin MenuRoute to Vue-Router RouteRecordRaw to get a clean route-tree
|
||||
* @param target
|
||||
* @param source
|
||||
* @param mainPath
|
||||
*/
|
||||
function combineMenuRoutes(
|
||||
target: RouteRecordRaw[],
|
||||
source: FG_Plugin.MenuRoute[],
|
||||
mainPath: '/' | '/in' = '/'
|
||||
): RouteRecordRaw[] {
|
||||
// Search parent
|
||||
target.forEach((target) => {
|
||||
if (target.path === mainPath) {
|
||||
// Parent found = target
|
||||
source.forEach((sourceMainConfig: FG_Plugin.MenuRoute) => {
|
||||
// Check if source is already in target
|
||||
const targetMainConfig = target.children?.find((targetMainConfig: RouteRecordRaw) => {
|
||||
return sourceMainConfig.route.path === targetMainConfig.path;
|
||||
});
|
||||
// Already in target routes, add only children
|
||||
if (targetMainConfig) {
|
||||
convertRoutes(targetMainConfig, sourceMainConfig.children);
|
||||
} else {
|
||||
// Append to target
|
||||
if (target.children === undefined) {
|
||||
target.children = [];
|
||||
}
|
||||
convertRoutes(sourceMainConfig.route, sourceMainConfig.children);
|
||||
if (
|
||||
sourceMainConfig.children &&
|
||||
sourceMainConfig.children.length > 0 &&
|
||||
!sourceMainConfig.route.component
|
||||
)
|
||||
Object.assign(sourceMainConfig.route, {
|
||||
component: () => import('src/components/navigation/EmptyParent.vue'),
|
||||
});
|
||||
target.children.push(sourceMainConfig.route);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return target;
|
||||
}
|
||||
|
||||
function combineRoutes(
|
||||
target: RouteRecordRaw[],
|
||||
source: FG_Plugin.NamedRouteRecordRaw[],
|
||||
mainPath: '/' | '/in'
|
||||
) {
|
||||
// Search parent
|
||||
target.forEach((target) => {
|
||||
if (target.path === mainPath) {
|
||||
// Parent found = target
|
||||
source.forEach((sourceRoute) => {
|
||||
// Check if source is already in target
|
||||
const targetRoot = target.children?.find(
|
||||
(targetRoot) => sourceRoute.path === targetRoot.path
|
||||
);
|
||||
// Already in target routes, add only children
|
||||
if (targetRoot) {
|
||||
if (targetRoot.children === undefined) targetRoot.children = [];
|
||||
targetRoot.children.push(...(sourceRoute.children || []));
|
||||
} else {
|
||||
// Append to target
|
||||
if (target.children === undefined) target.children = [];
|
||||
if (
|
||||
sourceRoute.children &&
|
||||
sourceRoute.children.length > 0 &&
|
||||
sourceRoute.component === undefined
|
||||
)
|
||||
Object.assign(sourceRoute, {
|
||||
component: () => import('src/components/navigation/EmptyParent.vue'),
|
||||
});
|
||||
target.children.push(sourceRoute);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine MenuRoutes into Flaschengeist MenuLinks for the main menu
|
||||
* @param target Flaschengeist list of menu links
|
||||
* @param source MenuRoutes to combine
|
||||
*/
|
||||
function combineMenuLinks(target: FG_Plugin.MenuLink[], source: FG_Plugin.MenuRoute) {
|
||||
let idx = target.findIndex((link) => link.title == source.title);
|
||||
// Link not found, add new one
|
||||
if (idx === -1) {
|
||||
idx += target.push({
|
||||
title: source.title,
|
||||
icon: source.icon,
|
||||
link: source.route.name,
|
||||
permissions: source.permissions,
|
||||
});
|
||||
}
|
||||
if (target[idx].children === undefined) {
|
||||
target[idx].children = [];
|
||||
}
|
||||
source.children?.forEach((sourceChild) => {
|
||||
target[idx].children?.push({
|
||||
title: sourceChild.title,
|
||||
icon: sourceChild.icon,
|
||||
link: sourceChild.route.name,
|
||||
permissions: sourceChild.permissions,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine shortcuts from Plugin MenuRouts into the Flaschenbeist Shortcut list
|
||||
* @param target Flaschengeist list of shortcuts
|
||||
* @param source MenuRoutes to extract shortcuts from
|
||||
*/
|
||||
function combineShortcuts(target: FG_Plugin.Shortcut[], source: FG_Plugin.MenuRoute[]) {
|
||||
source.forEach((route) => {
|
||||
if (route.shortcut) {
|
||||
target.push(<FG_Plugin.Shortcut>{
|
||||
link: route.route.name,
|
||||
icon: route.icon,
|
||||
permissions: route.permissions,
|
||||
});
|
||||
}
|
||||
if (route.children) {
|
||||
combineShortcuts(target, route.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Flaschengeist plugin
|
||||
* @param loadedPlugins Flaschgeist object
|
||||
* @param plugin Plugin to load
|
||||
* @param router VueRouter instance
|
||||
*/
|
||||
function loadPlugin(
|
||||
loadedPlugins: FG_Plugin.Flaschengeist,
|
||||
plugin: FG_Plugin.Plugin,
|
||||
backend: FG.Backend
|
||||
) {
|
||||
// Check if already loaded
|
||||
if (loadedPlugins.plugins.findIndex((p) => p.id === plugin.id) !== -1) return true;
|
||||
|
||||
// Check backend dependencies
|
||||
if (
|
||||
!plugin.requiredModules.every(
|
||||
(required) =>
|
||||
backend.plugins[required[0]] !== undefined &&
|
||||
(required.length == 1 ||
|
||||
true) /* validate the version, semver440 from python is... tricky on node*/
|
||||
)
|
||||
) {
|
||||
console.error(`Plugin ${plugin.id}: Backend modules not satisfied`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start combining and loading routes, shortcuts etc
|
||||
if (plugin.internalRoutes) {
|
||||
combineRoutes(loadedPlugins.routes, plugin.internalRoutes, '/in');
|
||||
}
|
||||
|
||||
if (plugin.innerRoutes) {
|
||||
// Routes for Vue Router
|
||||
combineMenuRoutes(loadedPlugins.routes, plugin.innerRoutes, '/in');
|
||||
// Combine links for menu
|
||||
plugin.innerRoutes.forEach((route) => combineMenuLinks(loadedPlugins.menuLinks, route));
|
||||
// Combine shortcuts
|
||||
combineShortcuts(loadedPlugins.shortcuts, plugin.innerRoutes);
|
||||
}
|
||||
|
||||
if (plugin.outerRoutes) {
|
||||
combineMenuRoutes(loadedPlugins.routes, plugin.outerRoutes);
|
||||
combineShortcuts(loadedPlugins.outerShortcuts, plugin.outerRoutes);
|
||||
}
|
||||
|
||||
if (plugin.widgets.length > 0) {
|
||||
plugin.widgets.forEach((widget) => (widget.name = plugin.id + '.' + widget.name));
|
||||
Array.prototype.push.apply(loadedPlugins.widgets, plugin.widgets);
|
||||
}
|
||||
|
||||
if (!plugin.settingWidgets) plugin.settingWidgets = [];
|
||||
if (plugin.settingWidgets.length > 0) {
|
||||
plugin.settingWidgets.forEach((widget) => (widget.name = plugin.id + '.' + widget.name));
|
||||
Array.prototype.push.apply(loadedPlugins.settingWidgets, plugin.settingWidgets);
|
||||
}
|
||||
|
||||
loadedPlugins.plugins.push({
|
||||
id: plugin.id,
|
||||
name: plugin.name,
|
||||
version: plugin.version,
|
||||
notification: plugin.notification?.bind({}) || translateNotification,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function loadPlugins(backend: FG.Backend, baseRoutes: RouteRecordRaw[]) {
|
||||
const loadedPlugins: FG_Plugin.Flaschengeist = {
|
||||
routes: baseRoutes,
|
||||
plugins: [],
|
||||
menuLinks: [],
|
||||
shortcuts: [],
|
||||
outerShortcuts: [],
|
||||
widgets: [],
|
||||
settingWidgets: [],
|
||||
};
|
||||
|
||||
// Wait for all plugins to be loaded
|
||||
const results = await Promise.allSettled(PLUGINS);
|
||||
|
||||
// Check if loaded successfully
|
||||
results.forEach((result) => {
|
||||
if (result.status === 'rejected') {
|
||||
throw <string>result.reason;
|
||||
} else {
|
||||
if (
|
||||
!(
|
||||
validatePlugin(result.value.default) &&
|
||||
loadPlugin(loadedPlugins, result.value.default, backend)
|
||||
)
|
||||
)
|
||||
throw result.value.default.id;
|
||||
}
|
||||
});
|
||||
|
||||
// Sort widgets by priority
|
||||
/** @todo Remove priority with first beta */
|
||||
loadedPlugins.widgets.sort(
|
||||
(a, b) => <number>(b.order || b.priority) - <number>(a.order || a.priority)
|
||||
);
|
||||
|
||||
/** @todo Can be cleaned up with first beta */
|
||||
loadedPlugins.menuLinks.sort((a, b) => {
|
||||
const diff = a.order && b.order ? b.order - a.order : 0;
|
||||
return diff ? diff : a.title.toString().localeCompare(b.title.toString());
|
||||
});
|
||||
|
||||
return loadedPlugins;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* This boot file installs the global pinia instance
|
||||
*/
|
||||
import { pinia } from '@flaschengeist/api';
|
||||
import { boot } from 'quasar/wrappers';
|
||||
|
||||
export default boot(({ app }) => {
|
||||
app.use(pinia);
|
||||
});
|
|
@ -1,40 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-snackbar :timeout="0" color="error" :value="visible" top>
|
||||
<v-list color="error" dense>
|
||||
<v-list-item v-for="(error, index) in errors" :key="index" dense>
|
||||
<v-list-item-title class="caption" style="color: white;">
|
||||
{{error.message}}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-btn icon color="white" @click="deleteErrors">
|
||||
<v-icon>
|
||||
mdi-close
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
export default {
|
||||
name: "ConnectionError",
|
||||
methods: {
|
||||
...mapActions({
|
||||
deleteErrors: 'connectionError/deleteErrors'
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
errors: 'connectionError/errors',
|
||||
visible: 'connectionError/visible'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,48 +0,0 @@
|
|||
<template>
|
||||
<v-bottom-sheet persistent v-model="show" hide-overlay>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
Cookie und Local Storage Hinweis
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
Diese Webseite benutzt den Local Storage. Dabei werden Daten in ihm
|
||||
gespeichert, welche notwendig sind um sich einzuloggen und eingeloggt zu
|
||||
bleiben. Außerdem sind diese Daten notwendig um mit dem Server zu
|
||||
kommunizieren. Dabei wird ein Key 'user' angelegt, in welchem ein
|
||||
Accesstoken, Benutzername, sowie der Name des Benutzers und deren Rechte
|
||||
gespeichert. Dazu kommt ein Key 'cookie:accepted', falls sie diesem
|
||||
zustimmen. Diese Daten bleiben solange erhalten bis Sie sich ausloggen
|
||||
oder der Accesstoken abgelaufen ist und Sie ausgeloggt werden.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn text @click="disableNotification()">Ablehnen</v-btn>
|
||||
<v-btn text color="primary" @click="acceptNotification()">Akzeptieren</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-bottom-sheet>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
export default {
|
||||
name: 'CookieNotification',
|
||||
methods: {
|
||||
...mapActions(['acceptNotification', 'disableNotification', 'getCookieAccepted'])
|
||||
},
|
||||
created() {
|
||||
this.getCookieAccepted()
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
model: 'cookieNotification',
|
||||
cookie: 'cookieAccepted'
|
||||
}),
|
||||
show() {
|
||||
return !this.cookie ? this.model : false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,101 @@
|
|||
<template>
|
||||
<q-card
|
||||
bordered
|
||||
style="position: relative; min-height: 3em"
|
||||
:class="{ 'cursor-pointer': modelValue.link }"
|
||||
@click="click"
|
||||
>
|
||||
<q-btn
|
||||
round
|
||||
dense
|
||||
icon="mdi-trash-can"
|
||||
size="sm"
|
||||
color="negative"
|
||||
class="q-ma-xs"
|
||||
title="Löschen"
|
||||
style="position: absolute; top: 0; right: 0; z-index: 999"
|
||||
@click.stop.prevent="dismiss"
|
||||
/>
|
||||
<q-card-section class="q-pa-xs">
|
||||
<div class="text-overline">{{ dateString }}</div>
|
||||
<q-item style="padding: 1px">
|
||||
<q-item-section v-if="modelValue.icon" side
|
||||
><q-icon color="primary" :name="modelValue.icon"
|
||||
/></q-item-section>
|
||||
<q-item-section>{{ modelValue.text }}</q-item-section>
|
||||
</q-item>
|
||||
</q-card-section>
|
||||
<q-card-actions v-if="modelValue.reject || modelValue.accept">
|
||||
<q-btn
|
||||
v-if="modelValue.accept"
|
||||
icon="mdi-check"
|
||||
color="positive"
|
||||
label="Annehmen"
|
||||
flat
|
||||
dense
|
||||
size="sm"
|
||||
@click.stop.prevent="accept"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="modelValue.reject"
|
||||
icon="mdi-close"
|
||||
color="negative"
|
||||
label="Ablehnen"
|
||||
flat
|
||||
dense
|
||||
size="sm"
|
||||
@click.stop.prevent="reject"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from 'vue';
|
||||
import { formatDateTime } from '@flaschengeist/api';
|
||||
import { FG_Plugin } from '@flaschengeist/types';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Notification',
|
||||
props: {
|
||||
modelValue: {
|
||||
required: true,
|
||||
type: Object as PropType<FG_Plugin.Notification>,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
remove: (id: number) => !!id,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const router = useRouter();
|
||||
const dateString = computed(() => formatDateTime(props.modelValue.time, true, true));
|
||||
|
||||
async function click() {
|
||||
if (props.modelValue.link) await router.push(props.modelValue.link);
|
||||
}
|
||||
|
||||
function accept() {
|
||||
if (typeof props.modelValue.accept === 'function')
|
||||
void props.modelValue.accept().finally(() => emit('remove', props.modelValue.id));
|
||||
else emit('remove', props.modelValue.id);
|
||||
}
|
||||
|
||||
function reject() {
|
||||
if (typeof props.modelValue.reject === 'function')
|
||||
void props.modelValue.reject().finally(() => emit('remove', props.modelValue.id));
|
||||
else emit('remove', props.modelValue.id);
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
if (typeof props.modelValue.dismiss === 'function')
|
||||
void props.modelValue.dismiss().finally(() => emit('remove', props.modelValue.id));
|
||||
else emit('remove', props.modelValue.id);
|
||||
}
|
||||
|
||||
return { accept, click, dateString, dismiss, reject };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,146 +0,0 @@
|
|||
<template>
|
||||
<v-content>
|
||||
<v-container class="fill-height" fluid>
|
||||
<v-row align="center" justify="center">
|
||||
<v-col cols="12" sm="8" md="4">
|
||||
<v-card class="elevation-12">
|
||||
<v-toolbar color="blue accent-4" dark flat dense>
|
||||
<v-toolbar-title>Password vergessen</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<v-form lazy-validation ref="reset">
|
||||
<v-text-field
|
||||
label="E-Mail oder Nutzername"
|
||||
v-model="input"
|
||||
hint="Hier bitte deinen Nutzernamen oder deine E-Mail angeben. Sollte eins der beiden Daten gefunden werden, wird an deine hinterlegte E-Mail ein Password zum Zurücksetzen gesendet."
|
||||
persistent-hint
|
||||
:prepend-icon="prependIcon"
|
||||
required
|
||||
:rules="[notEmpty, isMail ? email : true]"
|
||||
@keyup.enter="resetPassword()"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="resetPassword()"
|
||||
@submit.prevent="resetPassword()"
|
||||
>
|
||||
Zurücksetzen
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<v-snackbar bottom :timeout="0" :value="response.value" :color="response.error? 'error' : 'success'">
|
||||
{{ response.message }}
|
||||
<v-btn icon @click="response.value = false">
|
||||
<v-icon color="white">
|
||||
mdi-close
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</v-content>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import url from '@/plugins/routes'
|
||||
export default {
|
||||
name: 'ResetPassword',
|
||||
data() {
|
||||
return {
|
||||
input: '',
|
||||
response: {
|
||||
error: false,
|
||||
value: false,
|
||||
message: null
|
||||
},
|
||||
defaultResponse: {
|
||||
error: false,
|
||||
value: false,
|
||||
message: null
|
||||
},
|
||||
notEmpty: data => {
|
||||
return data ? true : 'Darf nicht leer sein.'
|
||||
},
|
||||
email: value => {
|
||||
if (value.length > 0) {
|
||||
const pattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
return pattern.test(value) || 'keine gültige E-Mail'
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetPassword() {
|
||||
if (this.$refs.reset.validate()) {
|
||||
console.log(this.input, this.isMail)
|
||||
if (this.isMail) {
|
||||
axios
|
||||
.post(url.resetPassword, { mail: this.input })
|
||||
.then(data => {
|
||||
console.log(data)
|
||||
this.setMessage(data.data.mail, false)
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
this.setMessage(error, true)
|
||||
})
|
||||
.finally(() => {
|
||||
this.$refs.reset.reset()
|
||||
})
|
||||
} else {
|
||||
axios
|
||||
.post(url.resetPassword, { username: this.input })
|
||||
.then(data => {
|
||||
console.log(data)
|
||||
this.setMessage(data.data.mail, false)
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
this.setMessage(error, true)
|
||||
})
|
||||
.finally(() => {
|
||||
this.$refs.reset.reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
setMessage(mail, error) {
|
||||
if (error) {
|
||||
this.response.error = true
|
||||
this.response.value = true
|
||||
this.response.message =
|
||||
'Es ist ein Fehler aufgetreten. Wende dich an einen Administrator oder probiere es erneut.'
|
||||
} else {
|
||||
this.response.error = false
|
||||
this.response.value = true
|
||||
this.response.message = `Es wurde ein neues Password an ${mail} versendet`
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
prependIcon() {
|
||||
if (this.input) {
|
||||
return this.input.includes('@') ? 'mdi-email' : 'mdi-account'
|
||||
}
|
||||
return 'mdi-account'
|
||||
},
|
||||
isMail() {
|
||||
if (this.input) {
|
||||
return this.input.includes('@')
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,94 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-app-bar
|
||||
app
|
||||
clipped-left
|
||||
clipped-right
|
||||
color="blue accent-4"
|
||||
class="elevation-4"
|
||||
dark
|
||||
dense
|
||||
>
|
||||
<v-btn icon @click="reload()">
|
||||
<v-img src="@/assets/logo-64.png" contain height="40"></v-img>
|
||||
</v-btn>
|
||||
<v-toolbar-title>Flaschengeist <span class="caption">v1.0.1</span></v-toolbar-title>
|
||||
<v-spacer/>
|
||||
<v-btn icon v-if="getRouteName == 'resetPassword'" @click="goTo('login')">
|
||||
<v-icon>
|
||||
mdi-home
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon v-if="getRouteName == 'priceListNoLogin'" @click="goBack()">
|
||||
<v-icon>
|
||||
{{ back }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon v-if="isFinanzer" :disabled="locked" @click="goTo('overview')">
|
||||
<v-icon>{{ attach_money }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon v-if="isGastro" :disabled="locked" @click="goTo('gastroPricelist')">
|
||||
<v-icon>{{ gastro }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon v-if="isBar" @click="goTo('geruecht')">
|
||||
<v-icon>{{ local_bar }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon v-if="isUser" :disabled="locked" @click="goTo('add')">
|
||||
<v-icon>{{ person }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon @click="goTo('priceListNoLogin')">
|
||||
<v-icon>{{ list }}</v-icon>
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
<ConnectionError/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import {
|
||||
mdiCurrencyEur,
|
||||
mdiGlassCocktail,
|
||||
mdiAccount,
|
||||
mdiFileMultiple,
|
||||
mdiFoodForkDrink,
|
||||
mdiArrowLeftBoldCircle
|
||||
} from '@mdi/js'
|
||||
import ConnectionError from "@/components/ConnectionError";
|
||||
|
||||
export default {
|
||||
name: 'TitleBar',
|
||||
components: {ConnectionError},
|
||||
data() {
|
||||
return {
|
||||
attach_money: mdiCurrencyEur,
|
||||
local_bar: mdiGlassCocktail,
|
||||
person: mdiAccount,
|
||||
list: mdiFileMultiple,
|
||||
gastro: mdiFoodForkDrink,
|
||||
back: mdiArrowLeftBoldCircle
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isBar', 'isFinanzer', 'isUser', 'isLoggedIn', 'isGastro']),
|
||||
...mapGetters({locked: 'barUsers/locked'}),
|
||||
getRouteName() {
|
||||
return this.$route.name
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['logout']),
|
||||
reload() {
|
||||
location.reload()
|
||||
},
|
||||
goTo(name) {
|
||||
this.$router.push({name: name})
|
||||
},
|
||||
goBack() {
|
||||
window.history.length > 1 ? this.$router.go(-1) : this.$router.push({name: 'main'})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<q-card class="col-4">
|
||||
<q-card-section class="row fit justify-center items-center content-center">
|
||||
<q-avatar size="150px">
|
||||
<q-img :src="pic" placeholder-src="logo-black.svg" />
|
||||
</q-avatar>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="text-h6">{{ firstname }} {{ lastname }}</div>
|
||||
<div class="text-subtitle2">{{ club }}</div>
|
||||
<q-separator />
|
||||
<div class="text-subtitle2">Aufgabe bei Flaschengeist:</div>
|
||||
<div>
|
||||
{{ job }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
{{ description }}
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
export default defineComponent({
|
||||
name: 'Developer',
|
||||
props: {
|
||||
pic: {
|
||||
default: 'logo-dark.svg',
|
||||
type: String,
|
||||
},
|
||||
firstname: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
lastname: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
job: {
|
||||
default: 'student',
|
||||
type: String,
|
||||
},
|
||||
club: {
|
||||
default: 'Studentenclub Wu5 e.V.',
|
||||
type: String,
|
||||
},
|
||||
description: {
|
||||
default:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ',
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -1,26 +0,0 @@
|
|||
<template>
|
||||
<v-list>
|
||||
<v-list-item link :to="{name: 'geruecht'}">
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ glass_mug_variant }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>
|
||||
Geruecht
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mdiGlassMugVariant } from '@mdi/js'
|
||||
export default {
|
||||
name: 'BarNavigation',
|
||||
data() {
|
||||
return {
|
||||
glass_mug_variant: mdiGlassMugVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,595 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-dialog v-model="checkValidate" max-width="290">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
Willst du wirklich??
|
||||
</v-card-title>
|
||||
<v-card-text v-if="stornoMessage">
|
||||
Willst du wirklich den Betrag
|
||||
{{ (stornoMessage.amount / 100).toFixed(2) }}€ von
|
||||
{{ stornoMessage.user.firstname }}
|
||||
{{ stornoMessage.user.lastname }} stornieren?
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn text @click="cancelStorno">Abbrechen</v-btn>
|
||||
<v-btn text @click="acceptStorno">Stornieren</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="dialog" max-width="290">
|
||||
<v-card>
|
||||
<v-card-title class="headline"
|
||||
>Transaktion ist länger als 1 Minute her!</v-card-title
|
||||
>
|
||||
<v-card-text>
|
||||
Da die Transaktion länger als 1 Minuter her ist, kann eine Stornierung
|
||||
nicht durchgeführt werden. Wende dich bitte an den Finanzer.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn text @click="dialog = false">
|
||||
Verstanden
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog
|
||||
v-if="overLimitUser"
|
||||
v-model="overLimitUser"
|
||||
max-width="290"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>Warnung</v-card-title>
|
||||
<v-card-text>
|
||||
{{ overLimitUser.firstname }} {{ overLimitUser.lastname }} übersteigt
|
||||
das Anschreibelimit von
|
||||
{{ (overLimitUser.limit / 100).toFixed(2) }} €. Danach kann dieses
|
||||
Mitglied nichts mehr anschreiben. Will er das wirklich?
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn text @click="cancel()">Abbrechen</v-btn>
|
||||
<v-btn text @click="continueAdd(overLimitUser)">Anschreiben</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog
|
||||
v-if="overOverLimit"
|
||||
v-model="overOverLimit"
|
||||
max-width="290"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>Anschreiben nicht möglich</v-card-title>
|
||||
<v-card-text>
|
||||
{{ overOverLimit.firstname }}
|
||||
{{ overOverLimit.lastname }} überschreitet das Anschreibelimit zuviel.
|
||||
Das Anschreiben wurde daher gestoppt und zurückgesetzt.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn text @click="overOverLimit = null">Verstanden</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-progress-linear v-if="loading && users.length !== 0" indeterminate />
|
||||
<v-container>
|
||||
<AddAmountSkeleton v-if="loading && users.length === 0" />
|
||||
</v-container>
|
||||
<v-navigation-drawer v-model="menu" right app clipped>
|
||||
<v-list-item-group :key="componentRenderer">
|
||||
<v-list-item inactive>
|
||||
<v-list-item-title class="headline">
|
||||
Verlauf
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider />
|
||||
<div
|
||||
v-for="message in messages"
|
||||
three-line
|
||||
:key="messages.indexOf(message)"
|
||||
>
|
||||
<v-list-item three-line inactive @click="storno(message)">
|
||||
<v-list-item-content>
|
||||
<v-progress-linear indeterminate v-if="message.loading" />
|
||||
<v-list-item-title>{{ now(message.date) }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{
|
||||
createMessage(message)
|
||||
}}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle class="red--text" v-if="message.storno">
|
||||
STORNIERT!!!
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle class="red--text" v-else-if="message.error">
|
||||
ERROR!
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-action-text v-if="under5minutes(message.date)"
|
||||
>Klicken um zu Stornieren
|
||||
</v-list-item-action-text>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</v-list-item-group>
|
||||
</v-navigation-drawer>
|
||||
<div v-for="user in users" :key="users.indexOf(user)">
|
||||
<div v-if="isFiltered(user) && calcLastSeen(user)">
|
||||
<v-container>
|
||||
<v-card :loading="user.loading">
|
||||
<v-card-title>
|
||||
<v-list-item-title class="title"
|
||||
>{{ user.firstname }} {{ user.lastname }}</v-list-item-title
|
||||
>
|
||||
</v-card-title>
|
||||
<v-card-subtitle v-if="user.limit + user.amount > 0">
|
||||
Nur noch {{ ((user.limit + user.amount) / 100).toFixed(2) }} €
|
||||
übrig!!
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-row v-if="!user.locked">
|
||||
<v-col cols="10">
|
||||
<v-row>
|
||||
<v-col cols="6" xs="5" sm="4">
|
||||
<v-btn
|
||||
class="creditBtn"
|
||||
block
|
||||
@click="addingAmount(user, 200)"
|
||||
:color="color"
|
||||
:disabled="user.locked"
|
||||
>2 €
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" xs="5" sm="4">
|
||||
<v-btn
|
||||
class="creditBtn"
|
||||
block
|
||||
@click="addingAmount(user, 100)"
|
||||
:color="color"
|
||||
:disabled="user.locked"
|
||||
>1 €
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" xs="5" sm="4">
|
||||
<v-btn
|
||||
class="creditBtn"
|
||||
block
|
||||
@click="addingAmount(user, 50)"
|
||||
:color="color"
|
||||
:disabled="user.locked"
|
||||
>0,50 €
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" xs="5" sm="4">
|
||||
<v-btn
|
||||
class="creditBtn"
|
||||
block
|
||||
@click="addingAmount(user, 40)"
|
||||
:color="color"
|
||||
:disabled="user.locked"
|
||||
>0,40 €
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" xs="5" sm="4">
|
||||
<v-btn
|
||||
class="creditBtn"
|
||||
block
|
||||
@click="addingAmount(user, 20)"
|
||||
:color="color"
|
||||
:disabled="user.locked"
|
||||
>0,20 €
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" xs="5" sm="4">
|
||||
<v-btn
|
||||
class="creditBtn"
|
||||
block
|
||||
@click="addingAmount(user, 10)"
|
||||
:color="color"
|
||||
:disabled="user.locked"
|
||||
>0,10 €
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
outlined
|
||||
type="number"
|
||||
v-model="user.value"
|
||||
label="Benutzerdefinierter Betrag"
|
||||
:disabled="user.locked"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
fab
|
||||
:color="color"
|
||||
@click="addAmountMore(user)"
|
||||
:disabled="user.locked"
|
||||
>
|
||||
<v-icon>{{ plus }}</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col align-self="center">
|
||||
<v-row>
|
||||
<v-list-item>
|
||||
<v-list-item-content class="text-center">
|
||||
<v-list-item-action-text :class="getColor(user.type)"
|
||||
>{{ (user.amount / 100).toFixed(2) }}
|
||||
€
|
||||
</v-list-item-action-text>
|
||||
<v-list-item-action-text v-if="user.toSetAmount">
|
||||
- {{ (user.toSetAmount / 100).toFixed(2) }}
|
||||
</v-list-item-action-text>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col class="hidden-sm-and-down" cols="80">
|
||||
<v-alert v-if="user.locked" type="error"
|
||||
>{{ user.firstname }} darf nicht mehr anschreiben.
|
||||
{{ user.firstname }} sollte sich lieber mal beim Finanzer
|
||||
melden.
|
||||
</v-alert>
|
||||
</v-col>
|
||||
<v-col align-self="center" v-if="user.locked">
|
||||
<v-row>
|
||||
<v-list-item>
|
||||
<v-list-item-content class="text-center">
|
||||
<v-list-item-action-text :class="getColor(user.type)"
|
||||
>{{ (user.amount / 100).toFixed(2) }}
|
||||
€
|
||||
</v-list-item-action-text>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-snackbar
|
||||
:color="
|
||||
messages.length > 0
|
||||
? messages[0].error
|
||||
? 'error'
|
||||
: 'success'
|
||||
: 'success'
|
||||
"
|
||||
bottom
|
||||
:timeout="0"
|
||||
:multi-line="true"
|
||||
:value="messages.length > 0 ? messages[0].visible : test"
|
||||
vertical
|
||||
>
|
||||
<v-list-item
|
||||
v-for="message in messages"
|
||||
:key="messages.indexOf(message)"
|
||||
:style="
|
||||
message.error
|
||||
? 'background-color: #FF5252;'
|
||||
: 'background-color: #4CAF50;'
|
||||
"
|
||||
v-show="message.visible"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title style="color: white">
|
||||
{{ createMessage(message) }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action v-if="message.error">
|
||||
<v-btn icon @click="message.visible = false">
|
||||
<v-icon color="white">
|
||||
mdi-close
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import { mdiPlus } from '@mdi/js'
|
||||
import AddAmountSkeleton from '../user/Skeleton/AddAmountSkeleton'
|
||||
|
||||
export default {
|
||||
name: 'CreditLists',
|
||||
components: { AddAmountSkeleton },
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
plus: mdiPlus,
|
||||
value: null,
|
||||
color: 'green accent-4',
|
||||
menu: true,
|
||||
dialog: false,
|
||||
componentRenderer: 0,
|
||||
timer: '',
|
||||
stornoMessage: null,
|
||||
checkValidate: false,
|
||||
test: null,
|
||||
overLimitUser: null,
|
||||
overOverLimit: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.menu = this.menu_from_store
|
||||
this.getUsers()
|
||||
this.timer = setInterval(this.forceRender, 1000)
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addAmount: 'barUsers/addAmount',
|
||||
getUsers: 'barUsers/getUsers',
|
||||
deactivate: 'barUsers/deactivateMenu',
|
||||
commitStorno: 'barUsers/storno'
|
||||
}),
|
||||
continueAdd(user) {
|
||||
this.overLimitUser = null
|
||||
user.checkedOverLimit = true
|
||||
if (user.value) {
|
||||
this.addAmount({
|
||||
username: user.username,
|
||||
amount: Math.round(Math.abs(user.value * 100)),
|
||||
user: user
|
||||
})
|
||||
setTimeout(() => {
|
||||
user.value = null
|
||||
user.toSetAmount = null
|
||||
}, 300)
|
||||
} else {
|
||||
user.timeout = setTimeout(() => {
|
||||
this.addAmount({
|
||||
username: user.username,
|
||||
amount: user.toSetAmount,
|
||||
user: user
|
||||
})
|
||||
setTimeout(() => {
|
||||
user.toSetAmount = null
|
||||
}, 300)
|
||||
}, 2000)
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
this.overLimitUser.toSetAmount = null
|
||||
this.overLimitUser.value = null
|
||||
this.overLimitUser = null
|
||||
},
|
||||
checkOverLimitIsValid(user) {
|
||||
console.log(user)
|
||||
if (user.toSetAmount && user.autoLock) {
|
||||
if (
|
||||
(user.amount - Number.parseInt(user.toSetAmount)) <
|
||||
-(user.limit + 500)
|
||||
) {
|
||||
this.overOverLimit = user
|
||||
user.toSetAmount = null
|
||||
user.value = null
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
checkOverLimit(user) {
|
||||
console.log(user)
|
||||
if (user.toSetAmount) {
|
||||
if ((user.amount - user.toSetAmount) < -user.limit) {
|
||||
return user.checkedOverLimit ? false : true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
addingAmount(user, amount) {
|
||||
clearTimeout(user.timeout)
|
||||
user.toSetAmount = user.toSetAmount ? user.toSetAmount + amount : amount
|
||||
if (this.checkOverLimitIsValid(user)) {
|
||||
if (this.checkOverLimit(user) && user.autoLock) {
|
||||
this.overLimitUser = user
|
||||
} else {
|
||||
user.timeout = setTimeout(() => {
|
||||
this.addAmount({
|
||||
username: user.username,
|
||||
amount: user.toSetAmount,
|
||||
user: user
|
||||
})
|
||||
setTimeout(() => {
|
||||
user.toSetAmount = null
|
||||
}, 300)
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
},
|
||||
forceRender() {
|
||||
this.componentRenderer += 1
|
||||
},
|
||||
getColor(type) {
|
||||
return type === 'credit' ? 'title green--text' : 'title red--text'
|
||||
},
|
||||
isFiltered(user) {
|
||||
try {
|
||||
var filters = this.filter.split(' ')
|
||||
if (filters.length === 1) {
|
||||
if (
|
||||
user.firstname.toLowerCase().includes(filters[0].toLowerCase()) ||
|
||||
user.lastname.toLowerCase().includes(filters[0].toLowerCase())
|
||||
) {
|
||||
return true
|
||||
}
|
||||
} else if (filters.length > 1) {
|
||||
if (
|
||||
user.firstname.toLowerCase().includes(filters[0].toLowerCase()) &&
|
||||
user.lastname.toLowerCase().includes(filters[1].toLowerCase())
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
} catch (e) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
addAmountMore(user) {
|
||||
user.toSetAmount = user.toSetAmount
|
||||
? user.toSetAmount + Math.round(Math.abs(user.value * 100))
|
||||
: Math.round(Math.abs(user.value * 100))
|
||||
if (this.checkOverLimitIsValid(user)) {
|
||||
if (this.checkOverLimit(user) && user.autoLock) {
|
||||
this.overLimitUser = user
|
||||
} else {
|
||||
this.addAmount({
|
||||
username: user.username,
|
||||
amount: Math.round(Math.abs(user.value * 100)),
|
||||
user: user
|
||||
})
|
||||
setTimeout(() => {
|
||||
user.value = null
|
||||
user.toSetAmount = null
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
},
|
||||
storno(message) {
|
||||
if (!message.error) {
|
||||
if (!this.under5minutes(message.date)) this.dialog = true
|
||||
else {
|
||||
this.checkValidate = true
|
||||
this.stornoMessage = message
|
||||
}
|
||||
}
|
||||
},
|
||||
acceptStorno() {
|
||||
this.commitStorno({
|
||||
username: this.stornoMessage.user.username,
|
||||
amount: this.stornoMessage.amount,
|
||||
date: this.stornoMessage.date
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.cancelStorno()
|
||||
}, 300)
|
||||
},
|
||||
cancelStorno() {
|
||||
this.stornoMessage = null
|
||||
this.checkValidate = null
|
||||
},
|
||||
createMessage(message) {
|
||||
var text = ''
|
||||
if (message.error) {
|
||||
text =
|
||||
'ERROR: Konnte ' +
|
||||
(message.amount / 100).toFixed(2) +
|
||||
'€ nicht zu ' +
|
||||
message.user.firstname +
|
||||
' ' +
|
||||
message.user.lastname +
|
||||
' hinzufügen.'
|
||||
} else {
|
||||
text =
|
||||
'' +
|
||||
(message.amount / 100).toFixed(2) +
|
||||
'€ wurde zu ' +
|
||||
message.user.firstname +
|
||||
' ' +
|
||||
message.user.lastname +
|
||||
' hinzugefügt.'
|
||||
}
|
||||
return text
|
||||
},
|
||||
calcLastSeen(user) {
|
||||
if (user.last_seen) {
|
||||
let date = new Date()
|
||||
if ((date - user.last_seen) / 1000 / 60 / 60 < 72) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
users: 'barUsers/users',
|
||||
filter: 'barUsers/filter',
|
||||
loading: 'barUsers/usersLoading',
|
||||
messages: 'barUsers/messages',
|
||||
menu_from_store: 'barUsers/menu'
|
||||
}),
|
||||
under5minutes() {
|
||||
return now => {
|
||||
var actual = new Date()
|
||||
return actual - now < 60000
|
||||
}
|
||||
},
|
||||
now() {
|
||||
return now => {
|
||||
var actual = new Date()
|
||||
var zero = new Date(0)
|
||||
var date = new Date(actual - now)
|
||||
if (date.getFullYear() === zero.getFullYear()) {
|
||||
if (date.getMonth() === zero.getMonth()) {
|
||||
if (date.getDate() === zero.getDate()) {
|
||||
if (date.getHours() === zero.getDate()) {
|
||||
if (date.getMinutes() < 1) {
|
||||
return 'vor ' + date.getSeconds() + ' Sekunden'
|
||||
} else if (date.getMinutes() < 10) {
|
||||
return 'vor ' + date.getMinutes() + ' Minuten'
|
||||
} else {
|
||||
return (
|
||||
(now.getHours() < 10 ? '0' : '') +
|
||||
now.getHours() +
|
||||
':' +
|
||||
(now.getMinutes() < 10 ? '0' : '') +
|
||||
now.getMinutes()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
(now.getHours() < 10 ? '0' : '') +
|
||||
now.getHours() +
|
||||
':' +
|
||||
(now.getMinutes() < 10 ? '0' : '') +
|
||||
now.getMinutes()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
now.getDate() +
|
||||
'.' +
|
||||
now.getMonth() +
|
||||
'.' +
|
||||
now.getFullYear() +
|
||||
' ' +
|
||||
(now.getHours() < 10 ? '0' : '') +
|
||||
now.getHours() +
|
||||
':' +
|
||||
(now.getMinutes() < 10 ? '0' : '') +
|
||||
now.getMinutes()
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
menu(newValue) {
|
||||
if (!newValue) this.deactivate()
|
||||
},
|
||||
menu_from_store() {
|
||||
this.menu = this.menu_from_store
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.creditBtn {
|
||||
margin: 2px;
|
||||
}
|
||||
</style>
|
|
@ -1,132 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-toolbar>
|
||||
<v-spacer />
|
||||
<v-toolbar-items>
|
||||
<v-autocomplete
|
||||
outlined
|
||||
return-object
|
||||
v-model="user"
|
||||
style="margin-top: 3px"
|
||||
placeholder="Suche Person"
|
||||
:items="allUsers"
|
||||
item-text="fullName"
|
||||
full-width
|
||||
:loading="loading"
|
||||
:search-input.sync="filter"
|
||||
clearable
|
||||
>
|
||||
<template v-slot:prepend-inner>
|
||||
<v-icon>{{ search_person }}</v-icon>
|
||||
</template>
|
||||
<template v-slot:item="data">
|
||||
<v-list-item-icon v-if="getLocked(data.item)">
|
||||
<v-icon>mdi-alert</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
{{data.item.fullName}}
|
||||
<v-spacer/>
|
||||
{{(getCredit(data.item)/100).toFixed(2)}} €
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
<v-btn text @click="addUser">Hinzufügen</v-btn>
|
||||
<v-btn v-if="!locked" text @click="lock">Sperren</v-btn>
|
||||
<v-btn v-else text @click="overlay = true">Entsperren</v-btn>
|
||||
<v-btn @click="clickMenu" icon>
|
||||
<v-icon>{{ menuIcon }}</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<v-dialog v-model="overlay">
|
||||
<v-card>
|
||||
<v-card-title>Entsperre Baransicht</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field outlined type="password" label="Passwort" v-model="password"></v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn text @click="overlay = false">Abbrechen</v-btn>
|
||||
<v-btn text @click="doUnlock">Entsperren</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import { mdiAccountSearch, mdiMenu, mdiAlert } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
name: 'SearchBar',
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
user: null,
|
||||
filter: '',
|
||||
search_person: mdiAccountSearch,
|
||||
menuIcon: mdiMenu,
|
||||
alert: mdiAlert,
|
||||
overlay: false,
|
||||
password: ''
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getAllUsers()
|
||||
this.getLocked()
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
getAllUsers: 'barUsers/getAllUsers',
|
||||
addCreditList: 'barUsers/addCreditList',
|
||||
setFilter: 'barUsers/setFilter',
|
||||
activateMenu: 'barUsers/activateMenu',
|
||||
deactivateMenu: 'barUsers/deactivateMenu',
|
||||
lock: 'barUsers/setLocked',
|
||||
unlock: 'barUsers/unlock',
|
||||
getLocked: 'barUsers/getLocked'
|
||||
}),
|
||||
addUser() {
|
||||
this.addCreditList(this.user)
|
||||
},
|
||||
clickMenu() {
|
||||
if (this.menu) this.deactivateMenu()
|
||||
else this.activateMenu()
|
||||
},
|
||||
doUnlock() {
|
||||
this.unlock(this.password)
|
||||
this.password = ''
|
||||
this.overlay = false
|
||||
},
|
||||
getCredit(user) {
|
||||
let retUser = this.users.find(item => {
|
||||
return item.username === user.username
|
||||
})
|
||||
return retUser ? retUser.amount : 0
|
||||
},
|
||||
getLocked(user) {
|
||||
let retUser = this.users.find(item => {
|
||||
return item.username === user.username
|
||||
})
|
||||
return retUser ? retUser.locked : false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
allUsers: 'barUsers/allUsers',
|
||||
users: 'barUsers/users',
|
||||
loading: 'barUsers/allUsersLoading',
|
||||
menu: 'barUsers/menu',
|
||||
locked: 'barUsers/locked'
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
filter(val) {
|
||||
this.setFilter(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,49 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-list>
|
||||
<v-list-item class="title" link :to="{name: 'overview'}">
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{home}}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>Gesamtübersicht</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-divider />
|
||||
<v-list>
|
||||
<div v-for="user in users" v-bind:key="users.indexOf(user)">
|
||||
<v-list-item
|
||||
:to="{ name: 'activeUser', params: { id: user.username } }"
|
||||
link
|
||||
>
|
||||
<v-list-item-title
|
||||
>{{ user.lastname }}, {{ user.firstname }}</v-list-item-title
|
||||
>
|
||||
</v-list-item>
|
||||
</div>
|
||||
<v-list-item>
|
||||
<v-progress-circular indeterminate color="grey" v-if="loading" />
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import {mdiHome} from '@mdi/js'
|
||||
export default {
|
||||
name: 'FinanzerNavigation',
|
||||
data () {
|
||||
return {
|
||||
home: mdiHome
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
users: 'finanzerUsers/users',
|
||||
loading: 'finanzerUsers/usersLoading'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,400 +0,0 @@
|
|||
<template>
|
||||
<v-content>
|
||||
<v-toolbar tile>
|
||||
<v-toolbar-title>Gesamtübersicht</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-toolbar-items>
|
||||
<v-btn text icon @click="countYear(false)">
|
||||
<v-icon>{{keyboard_arrow_left}}</v-icon>
|
||||
</v-btn>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="title">{{ year }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-btn text icon @click="countYear(true)" :disabled="isActualYear">
|
||||
<v-icon>{{keyboard_arrow_right}}</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
<v-spacer />
|
||||
<v-toolbar-items>
|
||||
<v-btn text @click="sendMails">Emails senden</v-btn>
|
||||
<v-autocomplete
|
||||
outlined
|
||||
return-object
|
||||
v-model="user"
|
||||
style="margin-top: 3px"
|
||||
placeholder="Suche Person"
|
||||
:items="allUsers"
|
||||
item-text="fullName"
|
||||
full-width
|
||||
:loading="allUsersLoading"
|
||||
:search-input.sync="filter"
|
||||
@change="addToUser(user)"
|
||||
>
|
||||
<template v-slot:prepend-inner>
|
||||
<v-icon>{{search_person}}</v-icon>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<v-expand-transition>
|
||||
<v-card style="margin-top: 3px" v-show="errorMails">
|
||||
<v-row>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
icon
|
||||
style="margin-right: 5px"
|
||||
@click="errorExpand ? (errorExpand = false) : (errorExpand = true)"
|
||||
>
|
||||
<v-icon :class="isExpand(errorExpand)" dense>$expand</v-icon>
|
||||
</v-btn>
|
||||
</v-row>
|
||||
<v-expand-transition>
|
||||
<div v-show="errorExpand">
|
||||
<v-alert
|
||||
v-for="error in errorMails"
|
||||
:key="errorMails.indexOf(error)"
|
||||
dense
|
||||
:type="computeError(error.error)"
|
||||
>{{ errorMessage(error) }}</v-alert
|
||||
>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</v-card>
|
||||
</v-expand-transition>
|
||||
<v-progress-linear v-if="loading && users.length !== 0" indeterminate />
|
||||
<TableSkeleton v-if="loading && users.length === 0" />
|
||||
<div v-for="user in users" :key="users.indexOf(user)">
|
||||
<v-card
|
||||
v-if="user.creditList[year] && isFiltered(user)"
|
||||
style="margin-top: 3px"
|
||||
:loading="user.loading"
|
||||
>
|
||||
<v-card-title>
|
||||
{{ user.lastname }}, {{ user.firstname }}
|
||||
</v-card-title>
|
||||
<Table v-bind:user="user" v-bind:year="year" />
|
||||
<v-container fluid>
|
||||
<v-row align="start" align-content="start">
|
||||
<v-col>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-label>Vorjahr:</v-label>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
outlined
|
||||
:text-color="getLastColor(user.creditList[year][1].last)"
|
||||
>{{
|
||||
(user.creditList[year][1].last / 100).toFixed(2)
|
||||
}}</v-chip
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-label>Gesamt:</v-label>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
outlined
|
||||
x-large
|
||||
:text-color="
|
||||
getLastColor(
|
||||
getAllSum(
|
||||
user.creditList[year][2].sum,
|
||||
user.creditList[year][1].last
|
||||
)
|
||||
)
|
||||
"
|
||||
>
|
||||
{{
|
||||
(
|
||||
getAllSum(
|
||||
user.creditList[year][2].sum,
|
||||
user.creditList[year][1].last
|
||||
) / 100
|
||||
).toFixed(2)
|
||||
}}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col align-self="center">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-label>Status:</v-label>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip outlined :text-color="getLockedColor(user.locked)">{{
|
||||
user.locked ? 'Gesperrt' : 'nicht Gesperrt'
|
||||
}}</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card outlined>
|
||||
<v-row>
|
||||
<v-card-title class="subtitle-2"
|
||||
>Geld transferieren</v-card-title
|
||||
>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
icon
|
||||
style="margin-right: 5px"
|
||||
@click="setExpand(user)"
|
||||
>
|
||||
<v-icon :class="isExpand(user.expand)" dense
|
||||
>$expand</v-icon
|
||||
>
|
||||
</v-btn>
|
||||
</v-row>
|
||||
<v-expand-transition>
|
||||
<v-card-text v-show="user.expand">
|
||||
<v-form style="margin-left: 15px; margin-right: 15px">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
:rules="[isNumber]"
|
||||
label="Betrag"
|
||||
v-model="amount"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-select
|
||||
return-object
|
||||
v-model="type"
|
||||
label="Typ"
|
||||
:items="[
|
||||
{ value: 'amount', text: 'Schulden' },
|
||||
{ value: 'credit', text: 'Guthaben' }
|
||||
]"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-select
|
||||
return-object
|
||||
v-model="selectedYear"
|
||||
label="Jahr"
|
||||
:items="years"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-select
|
||||
return-object
|
||||
v-model="selectedMonth"
|
||||
label="Monat"
|
||||
:items="months"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<v-btn block @click="add(user)">Hinzufügen</v-btn>
|
||||
</v-card-text>
|
||||
</v-expand-transition>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-content>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Table from './Table'
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import TableSkeleton from './Skeleton/TableSkeleton'
|
||||
import {mdiChevronLeft, mdiChevronRight, mdiAccountSearch} from '@mdi/js'
|
||||
export default {
|
||||
name: 'Overview',
|
||||
components: { TableSkeleton, Table },
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
keyboard_arrow_left: mdiChevronLeft,
|
||||
keyboard_arrow_right: mdiChevronRight,
|
||||
search_person: mdiAccountSearch,
|
||||
errorExpand: false,
|
||||
|
||||
filter: '',
|
||||
user: null,
|
||||
|
||||
amount: null,
|
||||
isNumber: value => !isNaN(value) || 'Betrag muss eine Zahl sein.',
|
||||
type: { value: 'credit', text: 'Guthaben' },
|
||||
selectedYear: {
|
||||
value: new Date().getFullYear(),
|
||||
text: new Date().getFullYear()
|
||||
},
|
||||
selectedMonth: {
|
||||
value: new Date().getMonth() + 1,
|
||||
text: [
|
||||
'Januar',
|
||||
'Februar',
|
||||
'März',
|
||||
'April',
|
||||
'Mai',
|
||||
'Juni',
|
||||
'Juli',
|
||||
'August',
|
||||
'September',
|
||||
'Oktober',
|
||||
'November',
|
||||
'Dezember'
|
||||
][new Date().getMonth()]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {},
|
||||
methods: {
|
||||
...mapActions({
|
||||
createYears: 'finanzerUsers/createYears',
|
||||
addAmount: 'finanzerUsers/addAmount',
|
||||
addCredit: 'finanzerUsers/addCredit',
|
||||
countYear: 'finanzerUsers/countYear',
|
||||
sendMails: 'finanzerUsers/sendMails',
|
||||
addUser: 'finanzerUsers/addUser'
|
||||
}),
|
||||
async getData(promise) {
|
||||
return await promise
|
||||
},
|
||||
getLastColor(value) {
|
||||
return value < 0 ? 'red' : 'green'
|
||||
},
|
||||
getAllSum(sum, lastYear) {
|
||||
return lastYear + sum
|
||||
},
|
||||
getLockedColor(value) {
|
||||
return value ? 'red' : 'green'
|
||||
},
|
||||
computeError(error) {
|
||||
if (error) return 'error'
|
||||
else return 'success'
|
||||
},
|
||||
errorMessage(error) {
|
||||
if (error.error)
|
||||
return (
|
||||
'Konnte Email an ' +
|
||||
error.user.firstname +
|
||||
' ' +
|
||||
error.user.lastname +
|
||||
' nicht senden!'
|
||||
)
|
||||
else
|
||||
return (
|
||||
'Email wurde an ' +
|
||||
error.user.firstname +
|
||||
' ' +
|
||||
error.user.lastname +
|
||||
' versandt.'
|
||||
)
|
||||
},
|
||||
setExpand(user) {
|
||||
user.expand ? (user.expand = false) : (user.expand = true)
|
||||
},
|
||||
isExpand(value) {
|
||||
return value ? 'rotate' : ''
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
add(user) {
|
||||
if (this.type.value === 'amount') {
|
||||
this.addAmount({
|
||||
user: user,
|
||||
amount: this.amount,
|
||||
year: this.selectedYear.value,
|
||||
month: this.selectedMonth.value
|
||||
})
|
||||
}
|
||||
if (this.type.value === 'credit') {
|
||||
this.addCredit({
|
||||
user: user,
|
||||
credit: this.amount,
|
||||
year: this.selectedYear.value,
|
||||
month: this.selectedMonth.value
|
||||
})
|
||||
}
|
||||
|
||||
this.createDefault(
|
||||
this.amount,
|
||||
this.type,
|
||||
this.selectedYear,
|
||||
this.selectedMonth
|
||||
)
|
||||
},
|
||||
isFiltered(user) {
|
||||
try {
|
||||
var filters = this.filter.split(' ')
|
||||
for (var filter in filters) {
|
||||
if (
|
||||
user.firstname.toLowerCase().includes(filters[filter].toLowerCase()) ||
|
||||
user.lastname.toLowerCase().includes(filters[filter].toLowerCase())
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
} catch (e) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
addToUser(user) {
|
||||
this.addUser(user)
|
||||
this.$router.push({name: 'activeUser', params: {id: user.username}})
|
||||
},
|
||||
createDefault() {
|
||||
this.amount = null
|
||||
this.type = { value: 'credit', text: 'Guthaben' }
|
||||
this.selectedYear = {
|
||||
value: new Date().getFullYear(),
|
||||
text: new Date().getFullYear()
|
||||
}
|
||||
this.selectedMonth = {
|
||||
value: new Date().getMonth() + 1,
|
||||
text: [
|
||||
'Januar',
|
||||
'Februar',
|
||||
'März',
|
||||
'April',
|
||||
'Mai',
|
||||
'Juni',
|
||||
'Juli',
|
||||
'August',
|
||||
'September',
|
||||
'Oktober',
|
||||
'November',
|
||||
'Dezember'
|
||||
][new Date().getMonth()]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isActualYear() {
|
||||
return this.year === new Date().getFullYear()
|
||||
},
|
||||
...mapGetters({
|
||||
users: 'finanzerUsers/users',
|
||||
allUsers: 'finanzerUsers/allUsers',
|
||||
errorMails: 'finanzerUsers/errorMails',
|
||||
year: 'finanzerUsers/year',
|
||||
years: 'finanzerUsers/years',
|
||||
months: 'finanzerUsers/months',
|
||||
loading: 'finanzerUsers/usersLoading',
|
||||
allUsersLoading: 'finanzerUsers/allUsersLoading'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
|
@ -1,60 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card style="margin-top: 3px">
|
||||
<v-card-title>
|
||||
<v-skeleton-loader type="heading" />
|
||||
</v-card-title>
|
||||
<v-container>
|
||||
<v-skeleton-loader type="table-thead"/>
|
||||
<v-skeleton-loader type="table-row-divider@3"/>
|
||||
</v-container>
|
||||
<v-container fluid>
|
||||
<v-row align="start" align-content="start">
|
||||
<v-col>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip" />
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip"/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col align-self="center">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip" />
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card outlined>
|
||||
<v-row>
|
||||
<v-card-title>
|
||||
<v-skeleton-loader style="margin: 3px; margin-left: 10px" type="chip"/>
|
||||
</v-card-title>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TableSkeleton'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,82 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-toolbar tile>
|
||||
<v-toolbar-title>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip" />
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-toolbar-items>
|
||||
<v-skeleton-loader type="button" />
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<v-card style="margin-top: 3px;">
|
||||
<v-card-title><v-skeleton-loader type="heading"/></v-card-title>
|
||||
<v-card-text>
|
||||
<v-form style="margin-left: 15px; margin-right: 15px">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip" />
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip" />
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="button" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider style="margin-bottom: 15px;" />
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip" />
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-skeleton-loader type="button" />
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card style="margin-top: 3px;">
|
||||
<v-card-title><v-skeleton-loader type="chip"/></v-card-title>
|
||||
<v-card-text>
|
||||
<v-form style="margin-left: 15px; margin-right: 15px">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip" />
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip" />
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-skeleton-loader type="chip" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<v-skeleton-loader type="button" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'UserSkeleton'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,125 +0,0 @@
|
|||
<template>
|
||||
<v-data-table dense :headers="headers" :items="user.creditList[year]" :hide-default-footer="true">
|
||||
<template v-slot:item.jan_amount="{ item }">
|
||||
<v-chip outlined :text-color="getColor(item, item.jan_amount)">
|
||||
{{(item.jan_amount /
|
||||
100).toFixed(2)}}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.feb_amount="{ item }">
|
||||
<v-chip outlined :text-color="getColor(item, item.feb_amount)">
|
||||
{{(item.feb_amount /
|
||||
100).toFixed(2)}}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.maer_amount="{ item }">
|
||||
<v-chip outlined :text-color="getColor(item, item.maer_amount)">
|
||||
{{(item.maer_amount /
|
||||
100).toFixed(2)}}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.apr_amount="{ item }">
|
||||
<v-chip outlined :text-color="getColor(item, item.apr_amount)">
|
||||
{{(item.apr_amount /
|
||||
100).toFixed(2)}}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.mai_amount="{ item }">
|
||||
<v-chip outlined :text-color="getColor(item, item.mai_amount)">
|
||||
{{(item.mai_amount /
|
||||
100).toFixed(2)}}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.jun_amount="{ item }">
|
||||
<v-chip outlined :text-color="getColor(item, item.jun_amount)">
|
||||
{{(item.jun_amount /
|
||||
100).toFixed(2)}}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.jul_amount="{ item }">
|
||||
<v-chip outlined :text-color="getColor(item, item.jul_amount)">
|
||||
{{(item.jul_amount /
|
||||
100).toFixed(2)}}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.aug_amount="{ item }">
|
||||
<v-chip outlined :text-color="getColor(item, item.aug_amount)">
|
||||
{{(item.aug_amount /
|
||||
100).toFixed(2)}}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.sep_amount="{ item }">
|
||||
<v-chip outlined :text-color="getColor(item, item.sep_amount)">
|
||||
{{(item.sep_amount /
|
||||
100).toFixed(2)}}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.okt_amount="{ item }">
|
||||
<v-chip outlined :text-color="getColor(item, item.okt_amount)">
|
||||
{{(item.okt_amount /
|
||||
100).toFixed(2)}}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.nov_amount="{ item }">
|
||||
<v-chip outlined :text-color="getColor(item, item.nov_amount)">
|
||||
{{(item.nov_amount /
|
||||
100).toFixed(2)}}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.dez_amount="{ item }">
|
||||
<v-chip outlined :text-color="getColor(item, item.dez_amount)">
|
||||
{{(item.dez_amount /
|
||||
100).toFixed(2)}}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.sum="{ item }">
|
||||
<v-chip outlined :text-color="getColor(item, item.sum)">{{(item.sum / 100).toFixed(2)}}</v-chip>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Table',
|
||||
props: {
|
||||
user: Object,
|
||||
year: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
text: 'Schulden / Guthaben',
|
||||
align: 'left',
|
||||
sortable: false,
|
||||
value: 'type'
|
||||
},
|
||||
{ text: 'Januar in EUR', value: 'jan_amount' },
|
||||
{ text: 'Februar in EUR', value: 'feb_amount' },
|
||||
{ text: 'März in EUR', value: 'maer_amount' },
|
||||
{ text: 'April in EUR', value: 'apr_amount' },
|
||||
{ text: 'Mai in EUR', value: 'mai_amount' },
|
||||
{ text: 'Juni in EUR', value: 'jun_amount' },
|
||||
{ text: 'Juli in EUR', value: 'jul_amount' },
|
||||
{ text: 'August in EUR', value: 'aug_amount' },
|
||||
{ text: 'September in EUR', value: 'sep_amount' },
|
||||
{ text: 'Oktober in EUR', value: 'okt_amount' },
|
||||
{ text: 'November in EUR', value: 'nov_amount' },
|
||||
{ text: 'Dezember in EUR', value: 'dez_amount' },
|
||||
{ text: 'Summe in EUR', value: 'sum' }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getColor(item, value) {
|
||||
if (item.type === 'Summe') {
|
||||
return value < 0 ? 'red' : 'green'
|
||||
}
|
||||
return item.type === 'Guthaben' ? 'green' : 'red'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
|
@ -1,363 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-content v-if="loading" >
|
||||
<UserSkeleton />
|
||||
</v-content>
|
||||
<v-content v-if="activeUser">
|
||||
<v-toolbar tile>
|
||||
<v-toolbar-title
|
||||
>{{ activeUser.lastname }},
|
||||
{{ activeUser.firstname }}</v-toolbar-title
|
||||
>
|
||||
<v-spacer />
|
||||
<v-toolbar-items>
|
||||
<v-btn @click="sendMail({ username: activeUser.username })" text
|
||||
>Email senden</v-btn
|
||||
>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<v-progress-linear v-if="activeUser.loading" indeterminate />
|
||||
<v-expand-transition>
|
||||
<v-card style="margin-top: 3px" v-show="errorMail">
|
||||
<v-alert dense :type="computeError(errorMail)">
|
||||
{{ errorMessage(errorMail) }}
|
||||
</v-alert>
|
||||
</v-card>
|
||||
</v-expand-transition>
|
||||
<v-card style="margin-top: 3px;">
|
||||
<v-card-title>Konfiguration</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form style="margin-left: 15px; margin-right: 15px">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-label>Status:</v-label>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip outlined :text-color="getLockedColor(activeUser.locked)"
|
||||
>{{ activeUser.locked ? 'Gesperrt' : 'nicht Gesperrt' }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
@click="
|
||||
doLock({ user: activeUser, locked: !activeUser.locked })
|
||||
"
|
||||
>{{ activeUser.locked ? 'Entperren' : 'Sperren' }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider style="margin-bottom: 15px;" />
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
:rules="[isNumber]"
|
||||
label="Betrag des Sperrlimits in € (EURO)"
|
||||
v-model="limit"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-select
|
||||
return-object
|
||||
v-model="autoLock"
|
||||
label="Automatische Sperre"
|
||||
:items="[
|
||||
{ value: true, text: 'Aktiviert' },
|
||||
{ value: false, text: 'Deaktiviert' }
|
||||
]"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-btn
|
||||
block
|
||||
@click="
|
||||
saveConfig({
|
||||
user: activeUser,
|
||||
limit: limit,
|
||||
autoLock: autoLock.value
|
||||
})
|
||||
"
|
||||
>Speichern
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card style="margin-top: 3px;">
|
||||
<v-card-title>Geld transferieren</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form style="margin-left: 15px; margin-right: 15px">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
:rules="[isNumber]"
|
||||
label="Betrag"
|
||||
v-model="amount"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-select
|
||||
return-object
|
||||
v-model="type"
|
||||
label="Typ"
|
||||
:items="[
|
||||
{ value: 'amount', text: 'Schulden' },
|
||||
{ value: 'credit', text: 'Guthaben' }
|
||||
]"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-select
|
||||
return-object
|
||||
v-model="selectedYear"
|
||||
label="Jahr"
|
||||
:items="selectYears"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-select
|
||||
return-object
|
||||
v-model="selectedMonth"
|
||||
label="Monat"
|
||||
:items="months"
|
||||
item-text="text"
|
||||
item-value="value"
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<v-btn block @click="add">Hinzufügen</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div v-for="year in years" :key="years.indexOf(year)">
|
||||
<v-card style="margin-top: 3px;">
|
||||
<v-card-title>{{ year }}</v-card-title>
|
||||
<Table v-bind:user="activeUser" v-bind:year="year" />
|
||||
<v-container fluid>
|
||||
<v-col>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-label>Vorjahr:</v-label>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
outlined
|
||||
:text-color="
|
||||
getLastColor(activeUser.creditList[year][1].last)
|
||||
"
|
||||
>
|
||||
{{ (activeUser.creditList[year][1].last / 100).toFixed(2) }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-label>Gesamt:</v-label>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
outlined
|
||||
x-large
|
||||
:text-color="
|
||||
getLastColor(
|
||||
getAllSum(
|
||||
activeUser.creditList[year][2].sum,
|
||||
activeUser.creditList[year][1].last
|
||||
)
|
||||
)
|
||||
"
|
||||
>
|
||||
{{
|
||||
(
|
||||
getAllSum(
|
||||
activeUser.creditList[year][2].sum,
|
||||
activeUser.creditList[year][1].last
|
||||
) / 100
|
||||
).toFixed(2)
|
||||
}}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Table from './Table'
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import UserSkeleton from "./Skeleton/UserSkeleton";
|
||||
export default {
|
||||
name: 'User',
|
||||
props: {
|
||||
id: String
|
||||
},
|
||||
components: {UserSkeleton, Table },
|
||||
data() {
|
||||
return {
|
||||
isNumber: value => !isNaN(value) || 'Betrag muss eine Zahl sein.',
|
||||
limit: null,
|
||||
autoLock: null,
|
||||
amount: null,
|
||||
type: { value: 'credit', text: 'Guthaben' },
|
||||
selectedYear: {
|
||||
value: new Date().getFullYear(),
|
||||
text: new Date().getFullYear()
|
||||
},
|
||||
selectedMonth: {
|
||||
value: new Date().getMonth() + 1,
|
||||
text: [
|
||||
'Januar',
|
||||
'Februar',
|
||||
'März',
|
||||
'April',
|
||||
'Mai',
|
||||
'Juni',
|
||||
'Juli',
|
||||
'August',
|
||||
'September',
|
||||
'Oktober',
|
||||
'November',
|
||||
'Dezember'
|
||||
][new Date().getMonth()]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.setActiveUser(this.$route.params.id)
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addAmount: 'finanzerUsers/addAmount',
|
||||
addCredit: 'finanzerUsers/addCredit',
|
||||
sendMail: 'finanzerUsers/sendMail',
|
||||
doLock: 'finanzerUsers/doLock',
|
||||
saveConfig: 'finanzerUsers/saveConfig',
|
||||
setActiveUser: 'finanzerUsers/setActiveUser'
|
||||
}),
|
||||
getLastColor(value) {
|
||||
return value < 0 ? 'red' : 'green'
|
||||
},
|
||||
getAllSum(sum, lastYear) {
|
||||
return lastYear + sum
|
||||
},
|
||||
getLockedColor(value) {
|
||||
return value ? 'red' : 'green'
|
||||
},
|
||||
add() {
|
||||
if (this.type.value === 'amount') {
|
||||
this.addAmount({
|
||||
user: this.activeUser,
|
||||
amount: this.amount,
|
||||
year: this.selectedYear.value,
|
||||
month: this.selectedMonth.value
|
||||
})
|
||||
}
|
||||
if (this.type.value === 'credit') {
|
||||
this.addCredit({
|
||||
user: this.activeUser,
|
||||
credit: this.amount,
|
||||
year: this.selectedYear.value,
|
||||
month: this.selectedMonth.value
|
||||
})
|
||||
}
|
||||
|
||||
this.createDefault()
|
||||
},
|
||||
createDefault() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let year = new Date().getFullYear()
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let month = new Date().getMonth()
|
||||
this.amount = null
|
||||
this.type = { value: 'credit', text: 'Guthaben' }
|
||||
this.selectedYear = {
|
||||
value: new Date().getFullYear(),
|
||||
text: new Date().getFullYear()
|
||||
}
|
||||
this.selectedMonth = {
|
||||
value: new Date().getMonth() + 1,
|
||||
text: [
|
||||
'Januar',
|
||||
'Februar',
|
||||
'März',
|
||||
'April',
|
||||
'Mai',
|
||||
'Juni',
|
||||
'Juli',
|
||||
'August',
|
||||
'September',
|
||||
'Oktober',
|
||||
'November',
|
||||
'Dezember'
|
||||
][new Date().getMonth()]
|
||||
}
|
||||
},
|
||||
computeError(error) {
|
||||
if (error) {
|
||||
if (error.error) return 'error'
|
||||
else return 'success'
|
||||
}
|
||||
},
|
||||
errorMessage(error) {
|
||||
if (error) {
|
||||
if (error.error)
|
||||
return (
|
||||
'Konnte Email an ' +
|
||||
error.user.firstname +
|
||||
' ' +
|
||||
error.user.lastname +
|
||||
' nicht senden!'
|
||||
)
|
||||
else
|
||||
return (
|
||||
'Email wurde an ' +
|
||||
error.user.firstname +
|
||||
' ' +
|
||||
error.user.lastname +
|
||||
' versandt.'
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
years() {
|
||||
let years = []
|
||||
for (let year in this.activeUser.creditList) {
|
||||
years.unshift(parseInt(year))
|
||||
}
|
||||
return years
|
||||
},
|
||||
...mapGetters({
|
||||
activeUser: 'finanzerUsers/activeUser',
|
||||
errorMail: 'finanzerUsers/errorMail',
|
||||
months: 'finanzerUsers/months',
|
||||
selectYears: 'finanzerUsers/selectYears',
|
||||
loading: 'finanzerUsers/addUserLoading'
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
activeUser(newVal) {
|
||||
this.limit = (newVal.limit / 100).toFixed(2)
|
||||
this.autoLock = {
|
||||
value: newVal.autoLock,
|
||||
text: newVal.autoLock ? 'Aktiviert' : 'Deaktiviert'
|
||||
}
|
||||
},
|
||||
id(newVal) {
|
||||
this.setActiveUser(newVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,28 +0,0 @@
|
|||
<template>
|
||||
<v-list>
|
||||
<v-list-item class="title" link :to="{name: 'gastroPricelist'}">
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{list}}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>
|
||||
Preisliste
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mdiFileMultiple } from '@mdi/js'
|
||||
export default {
|
||||
name: "GastroNavigation",
|
||||
data() {
|
||||
return {
|
||||
list: mdiFileMultiple
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<q-circular-progress
|
||||
indeterminate
|
||||
show-value
|
||||
font-size="10px"
|
||||
class="q-ma-md"
|
||||
size="80px"
|
||||
:thickness="0.15"
|
||||
color="primary"
|
||||
track-color="grey-3"
|
||||
>
|
||||
<q-avatar size="60px">
|
||||
<img src="flaschengeist-logo.svg" />
|
||||
</q-avatar>
|
||||
</q-circular-progress>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
export default defineComponent({
|
||||
name: 'CircularProgress',
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<q-circular-progress
|
||||
indeterminate
|
||||
show-value
|
||||
font-size="10px"
|
||||
class="q-ma-md"
|
||||
size="80px"
|
||||
:thickness="0.15"
|
||||
color="primary"
|
||||
track-color="grey-3"
|
||||
>
|
||||
<q-avatar size="60px">
|
||||
<img src="flaschengeist-logo.svg" />
|
||||
</q-avatar>
|
||||
</q-circular-progress>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
export default defineComponent({
|
||||
name: 'DarkCircularProgress',
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EmptyParent',
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<q-expansion-item
|
||||
v-if="isGranted(entry)"
|
||||
clickable
|
||||
:label="getTitle(entry)"
|
||||
:icon="entry.icon"
|
||||
expand-separator
|
||||
>
|
||||
<q-list class="q-ml-lg">
|
||||
<div v-for="child in entry.children" :key="child.link">
|
||||
<q-item v-if="isGranted(child)" clickable :to="{ name: child.link }">
|
||||
<q-menu context-menu>
|
||||
<q-btn v-close-popup label="Verknüpfung erstellen" dense @click="addShortCut(child)" />
|
||||
</q-menu>
|
||||
<q-item-section avatar>
|
||||
<q-icon :name="child.icon" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
{{ getTitle(child) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
</q-list>
|
||||
</q-expansion-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { hasPermissions } from '@flaschengeist/api';
|
||||
import { FG_Plugin } from '@flaschengeist/types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EssentialExpansionLink',
|
||||
components: {},
|
||||
props: {
|
||||
entry: {
|
||||
type: Object as PropType<FG_Plugin.MenuLink>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
addShortCut: (val: FG_Plugin.MenuLink) => val.link,
|
||||
},
|
||||
setup(_, { emit }) {
|
||||
function isGranted(val: FG_Plugin.MenuLink) {
|
||||
return hasPermissions(val.permissions || []);
|
||||
}
|
||||
function getTitle(entry: FG_Plugin.MenuLink) {
|
||||
return typeof entry.title === 'function' ? entry.title() : entry.title;
|
||||
}
|
||||
|
||||
function addShortCut(val: FG_Plugin.MenuLink) {
|
||||
emit('addShortCut', val);
|
||||
}
|
||||
|
||||
return { isGranted, getTitle, addShortCut };
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<q-item v-if="isGranted" clickable tag="a" target="self" :to="{ name: entry.link }">
|
||||
<q-item-section v-if="entry.icon" avatar>
|
||||
<q-icon :name="entry.icon" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>{{ title }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from 'vue';
|
||||
import { hasPermissions } from '@flaschengeist/api';
|
||||
import { FG_Plugin } from '@flaschengeist/types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EssentialLink',
|
||||
props: {
|
||||
entry: {
|
||||
type: Object as PropType<FG_Plugin.MenuLink>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const isGranted = computed(() => hasPermissions(props.entry.permissions || []));
|
||||
const title = computed(() =>
|
||||
typeof props.entry.title === 'function' ? props.entry.title() : props.entry.title
|
||||
);
|
||||
return { isGranted, title };
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<q-btn v-if="isGranted" flat dense :icon="shortcut.icon" :to="{ name: shortcut.link }" round>
|
||||
<q-menu v-if="context" context-menu>
|
||||
<q-btn v-close-popup label="Verknüpfung entfernen" @click="deleteShortcut" />
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from 'vue';
|
||||
import { hasPermissions } from '@flaschengeist/api';
|
||||
import { FG_Plugin } from '@flaschengeist/types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ShortcutLink',
|
||||
props: {
|
||||
shortcut: {
|
||||
required: true,
|
||||
type: Object as PropType<FG_Plugin.Shortcut | FG_Plugin.MenuLink>,
|
||||
},
|
||||
context: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
deleteShortcut: (val: FG_Plugin.MenuLink | FG_Plugin.Shortcut) => val.link,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const isGranted = computed(() => hasPermissions(props.shortcut.permissions || []));
|
||||
function deleteShortcut() {
|
||||
emit('deleteShortcut', props.shortcut);
|
||||
}
|
||||
return { isGranted, deleteShortcut };
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -1,497 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="priceList"
|
||||
:search="search"
|
||||
:loading="priceListLoading || typesLoading"
|
||||
>
|
||||
<template v-slot:top>
|
||||
<v-toolbar flat color="white">
|
||||
<v-toolbar-title>Preisliste</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
label="Suche Getränk"
|
||||
single-line
|
||||
hide-details
|
||||
>
|
||||
<template v-slot:append>
|
||||
<v-icon>{{ searchIcon }}</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<v-dialog v-model="dialog" v-if="isGastro && isGastroPage">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
fab
|
||||
x-small
|
||||
color="primary"
|
||||
class="mb-2"
|
||||
v-on="on"
|
||||
style="margin: 5px"
|
||||
>
|
||||
<v-icon>{{ plus }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="headline">{{ formTitle }}</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
v-model="editedItem.name"
|
||||
label="Name"
|
||||
outlined
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-autocomplete
|
||||
return-object
|
||||
v-model="editedItem.type"
|
||||
label="Kategorie"
|
||||
item-text="name"
|
||||
item-value="id"
|
||||
:items="types"
|
||||
outlined
|
||||
:search-input.sync="searchType"
|
||||
no-data-text="Kategorie nicht vorhanden."
|
||||
>
|
||||
<template v-slot:append-item>
|
||||
<v-list-item v-if="!inType(searchType)">
|
||||
<v-list-item-title>
|
||||
{{ searchType }}
|
||||
</v-list-item-title>
|
||||
<v-btn
|
||||
dark
|
||||
x-small
|
||||
color="blue darken-1"
|
||||
@click="addType()"
|
||||
:disabled="inType(searchType)"
|
||||
fab
|
||||
>
|
||||
<v-icon>
|
||||
{{ plus }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
v-model="editedItem.price"
|
||||
label="Preis in €"
|
||||
outlined
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
v-model="editedItem.price_big"
|
||||
label="Preis groß in €"
|
||||
outlined
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
v-model="editedItem.price_club"
|
||||
label="Preis Club in €"
|
||||
outlined
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
v-model="editedItem.price_club_big"
|
||||
label="Preis Club groß in €"
|
||||
outlined
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col col="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
v-model="editedItem.premium"
|
||||
label="Aufpreis in €"
|
||||
outlined
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
v-model="editedItem.premium_club"
|
||||
label="Aufpreis Club in €"
|
||||
outlined
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
v-model="editedItem.price_extern_club"
|
||||
label="Preis extern Club in €"
|
||||
outlined
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="blue darken-1" text @click="close"
|
||||
>Abbrechen</v-btn
|
||||
>
|
||||
<v-btn color="blue darken-1" text @click="save"
|
||||
>Speichern</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
<template v-slot:item.type="{ item }">
|
||||
{{ computeType(item.type) }}
|
||||
</template>
|
||||
<template v-slot:item.price="{ item }">
|
||||
{{ item.price ? (item.price / 100).toFixed(2) : '' }}
|
||||
</template>
|
||||
<template v-slot:item.price_big="{ item }">
|
||||
{{ item.price_big ? (item.price_big / 100).toFixed(2) : '' }}
|
||||
</template>
|
||||
<template v-slot:item.price_club="{ item }">
|
||||
{{
|
||||
item.name.toLowerCase() == 'long island ice tea'.toLowerCase()
|
||||
? 'Ein Klubmitglied bestellt keinen Long Island Icetea'
|
||||
: item.price_club
|
||||
? (item.price_club / 100).toFixed(2)
|
||||
: ''
|
||||
}}
|
||||
</template>
|
||||
<template v-slot:item.price_club_big="{ item }">
|
||||
{{ item.price_club_big ? (item.price_club_big / 100).toFixed(2) : '' }}
|
||||
</template>
|
||||
<template v-slot:item.premium="{ item }">
|
||||
{{ item.premium ? (item.premium / 100).toFixed(2) : '' }}
|
||||
</template>
|
||||
<template v-slot:item.premium_club="{ item }">
|
||||
{{ item.premium_club ? (item.premium_club / 100).toFixed(2) : '' }}
|
||||
</template>
|
||||
<template v-slot:item.price_extern_club="{ item }">
|
||||
{{
|
||||
item.price_extern_club
|
||||
? (item.price_extern_club / 100).toFixed(2)
|
||||
: ''
|
||||
}}
|
||||
</template>
|
||||
<template v-slot:item.action="{ item }">
|
||||
<v-icon small class="mr-2" @click="editItem(item)">
|
||||
{{ editIcon }}
|
||||
</v-icon>
|
||||
<v-icon small @click="deleteItem(item)">
|
||||
{{ deleteIcon }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<v-card tile v-if="isGastro && isGastroPage" :loading="typesLoading">
|
||||
<v-card-title>
|
||||
Kategorien
|
||||
<v-spacer />
|
||||
<v-btn fab x-small @click="dialogType = true" color="primary">
|
||||
<v-icon>{{ plus }}</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div tile v-for="type in types" :key="type.id">
|
||||
<v-card tile>
|
||||
<v-card-text class="black--text">
|
||||
<v-row class="ml-3 mr-3">
|
||||
{{ type.name }}
|
||||
<v-spacer />
|
||||
<v-btn icon @click="editType(type)">
|
||||
<v-icon>
|
||||
{{ editIcon }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon @click="deleteType(type)">
|
||||
<v-icon>
|
||||
{{ deleteIcon }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-dialog v-model="dialogType">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ dialogTypeTitle }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-container>
|
||||
<v-text-field
|
||||
v-model="editedType.name"
|
||||
outlined
|
||||
label="Name der Kategorie"
|
||||
/>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn text color="blue darken-1" @click="closeType()">
|
||||
Abbrechen
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
color="blue darken-1"
|
||||
@click="saveType()"
|
||||
:disabled="inType(editedType.name)"
|
||||
>
|
||||
Speichern
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import { mdiMagnify, mdiPlus, mdiPencil, mdiDelete } from '@mdi/js'
|
||||
export default {
|
||||
name: 'PriceList',
|
||||
data() {
|
||||
return {
|
||||
editIcon: mdiPencil,
|
||||
deleteIcon: mdiDelete,
|
||||
searchIcon: mdiMagnify,
|
||||
plus: mdiPlus,
|
||||
searchType: null,
|
||||
search: null,
|
||||
dialog: null,
|
||||
dialogType: null,
|
||||
editedIndex: -1,
|
||||
headers: [
|
||||
{
|
||||
text: 'Name',
|
||||
value: 'name'
|
||||
},
|
||||
{
|
||||
text: 'Kategorie',
|
||||
value: 'type'
|
||||
},
|
||||
{
|
||||
text: 'Preis in €',
|
||||
value: 'price'
|
||||
},
|
||||
{
|
||||
text: 'Preis groß in €',
|
||||
value: 'price_big'
|
||||
},
|
||||
{
|
||||
text: 'Preis Club in €',
|
||||
value: 'price_club'
|
||||
},
|
||||
{
|
||||
text: 'Preis groß Club in €',
|
||||
value: 'price_club_big'
|
||||
},
|
||||
{
|
||||
text: 'Aufpreis in €',
|
||||
value: 'premium'
|
||||
},
|
||||
{
|
||||
text: 'Aufpreis Club in €',
|
||||
value: 'premium_club'
|
||||
},
|
||||
{
|
||||
text: 'Preis Club extern in €',
|
||||
value: 'price_extern_club'
|
||||
}
|
||||
],
|
||||
editedItem: {
|
||||
name: null,
|
||||
type: { id: -1, name: null },
|
||||
price: null,
|
||||
price_big: null,
|
||||
price_club: null,
|
||||
price_club_big: null,
|
||||
premium: null,
|
||||
premium_club: null,
|
||||
price_extern_club: null
|
||||
},
|
||||
defaultItem: {
|
||||
name: null,
|
||||
type: { id: -1, name: null },
|
||||
price: null,
|
||||
price_big: null,
|
||||
price_club: null,
|
||||
price_club_big: null,
|
||||
premium: null,
|
||||
premium_club: null,
|
||||
price_extern_club: null
|
||||
},
|
||||
editedType: {
|
||||
id: -1,
|
||||
name: null
|
||||
},
|
||||
defaultType: {
|
||||
id: -1,
|
||||
name: null
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
getPriceList: 'priceList/getPriceList',
|
||||
getTypes: 'priceList/getTypes',
|
||||
setDrink: 'priceList/setDrink',
|
||||
updateDrink: 'priceList/updateDrink',
|
||||
deleteDrink: 'priceList/deleteDrink',
|
||||
setDrinkType: 'priceList/setDrinkType',
|
||||
updateDrinkType: 'priceList/updateDrinkType',
|
||||
deleteDrinkType: 'priceList/deleteDrinkType'
|
||||
}),
|
||||
editType(item) {
|
||||
this.editedType = Object.assign({}, item)
|
||||
this.dialogType = true
|
||||
},
|
||||
closeType() {
|
||||
this.dialogType = false
|
||||
setTimeout(() => {
|
||||
this.editedType = Object.assign({}, this.defaultType)
|
||||
}, 300)
|
||||
},
|
||||
saveType() {
|
||||
this.editedType.id === -1
|
||||
? this.setDrinkType(this.editedType)
|
||||
: this.updateDrinkType(this.editedType)
|
||||
this.closeType()
|
||||
},
|
||||
deleteType(item) {
|
||||
confirm('Bist du sicher, dass du diese Kategorie entfernen willst?') &&
|
||||
this.deleteDrinkType({ id: item.id })
|
||||
},
|
||||
addType() {
|
||||
this.setDrinkType({ name: this.searchType })
|
||||
},
|
||||
close() {
|
||||
this.dialog = false
|
||||
setTimeout(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem)
|
||||
this.editedIndex = -1
|
||||
}, 300)
|
||||
},
|
||||
editItem(item) {
|
||||
this.editedIndex = item.id
|
||||
this.editedItem = Object.assign({}, item)
|
||||
for (let i in this.editedItem) {
|
||||
this.editedItem[i] = isNaN(this.editedItem[i])
|
||||
? this.editedItem[i]
|
||||
: this.editedItem[i] == null || this.editedItem[i] == 0
|
||||
? null
|
||||
: (this.editedItem[i] / 100).toFixed(2)
|
||||
}
|
||||
this.editedItem.type = Object.assign(
|
||||
{},
|
||||
this.types.find(a => a.id == item.type)
|
||||
)
|
||||
this.dialog = true
|
||||
},
|
||||
|
||||
deleteItem(item) {
|
||||
confirm('Bist du sicher, dass die dieses Getränk entfernen wills?') &&
|
||||
this.deleteDrink({ id: item.id })
|
||||
},
|
||||
save() {
|
||||
var drink = {
|
||||
id: this.editedIndex,
|
||||
name: this.editedItem.name,
|
||||
type: this.editedItem.type.id,
|
||||
price: !isNaN(this.editedItem.price)
|
||||
? this.editedItem.price * 100
|
||||
: null,
|
||||
price_big: !isNaN(this.editedItem.price_big)
|
||||
? this.editedItem.price_big * 100
|
||||
: null,
|
||||
price_club: !isNaN(this.editedItem.price_club)
|
||||
? this.editedItem.price_club * 100
|
||||
: null,
|
||||
price_club_big: !isNaN(this.editedItem.price_club_big)
|
||||
? this.editedItem.price_club_big * 100
|
||||
: null,
|
||||
premium: !isNaN(this.editedItem.premium)
|
||||
? this.editedItem.premium * 100
|
||||
: null,
|
||||
premium_club: !isNaN(this.editedItem.premium_club)
|
||||
? this.editedItem.premium_club * 100
|
||||
: null,
|
||||
price_extern_club: !isNaN(this.editedItem.price_extern_club)
|
||||
? this.editedItem.price_extern_club * 100
|
||||
: null
|
||||
}
|
||||
drink.id === -1 ? this.setDrink(drink) : this.updateDrink(drink)
|
||||
this.editedItem = Object.assign({}, this.defaultItem)
|
||||
this.close()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
priceList: 'priceList/priceList',
|
||||
types: 'priceList/types',
|
||||
priceListLoading: 'priceList/priceListLoading',
|
||||
typesLoading: 'priceList/typesLoading',
|
||||
isGastro: 'isGastro'
|
||||
}),
|
||||
isGastroPage() {
|
||||
return this.$route.name === 'gastroPricelist'
|
||||
},
|
||||
formTitle() {
|
||||
return this.editedIndex === -1 ? 'Neues Getränk' : 'Bearbeite Getränk'
|
||||
},
|
||||
inType() {
|
||||
return text => {
|
||||
return !!this.types.find(a => {
|
||||
if (a.name === null || text === null) {
|
||||
return true
|
||||
} else {
|
||||
return a.name.toLowerCase() === text.toLowerCase()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
dialogTypeTitle() {
|
||||
return this.editedType.id === -1
|
||||
? 'Neue Kategorie'
|
||||
: 'Bearbeite Kategorie'
|
||||
},
|
||||
computeType() {
|
||||
return id => {
|
||||
const type = this.types.find(a => {
|
||||
return a.id === id
|
||||
})
|
||||
return type ? type.name : null
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getPriceList()
|
||||
this.getTypes()
|
||||
if (this.isGastro && this.isGastroPage) {
|
||||
this.headers.push({
|
||||
text: 'Aktion',
|
||||
value: 'action',
|
||||
sortable: false,
|
||||
filterable: false
|
||||
})
|
||||
}
|
||||
console.log(this.$route)
|
||||
},
|
||||
watch: {
|
||||
dialog(val) {
|
||||
val || this.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,497 +0,0 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<v-dialog v-model="checkValidate" max-width="290">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
Willst du wirklich??
|
||||
</v-card-title>
|
||||
<v-card-text v-if="stornoMessage">
|
||||
Willst du wirklich den Betrag
|
||||
{{ (stornoMessage.amount / 100).toFixed(2) }}€ von
|
||||
{{ stornoMessage.user.firstname }}
|
||||
{{ stornoMessage.user.lastname }} stornieren?
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn text @click="cancelStorno">Abbrechen</v-btn>
|
||||
<v-btn text @click="acceptStorno">Stornieren</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="dialog" max-width="290">
|
||||
<v-card>
|
||||
<v-card-title class="headline"
|
||||
>Transaktion ist länger als 15 Sekunden her!</v-card-title
|
||||
>
|
||||
<v-card-text>
|
||||
Da die Transaktion länger als 15 Sekunden her ist, kann eine
|
||||
Stornierung nicht durchgeführt werden. Wende dich bitte an den
|
||||
Finanzer.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn text @click="dialog = false">
|
||||
Verstanden
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog
|
||||
v-if="overLimitUser"
|
||||
v-model="overLimitUser"
|
||||
max-width="290"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>Warnung</v-card-title>
|
||||
<v-card-text>
|
||||
{{ overLimitUser.firstname }} {{ overLimitUser.lastname }} übersteigt
|
||||
das Anschreibelimit von
|
||||
{{ (overLimitUser.limit / 100).toFixed(2) }} €. Danach kann dieses
|
||||
Mitglied nichts mehr anschreiben. Will er das wirklich?
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn text @click="cancel()">Abbrechen</v-btn>
|
||||
<v-btn text @click="continueAdd(overLimitUser)">Anschreiben</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog v-if="overOverLimit" v-model="overOverLimit" max-width="290" persistent>
|
||||
<v-card>
|
||||
<v-card-title>Anschreiben nicht möglich</v-card-title>
|
||||
<v-card-text>
|
||||
{{ overOverLimit.firstname }}
|
||||
{{ overOverLimit.lastname }} überschreitet das Anschreibelimit zuviel.
|
||||
Das Anschreiben wurde daher gestoppt und zurückgesetzt.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn text @click="overOverLimit = null">Verstanden</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<AddAmountSkeleton v-if="loading" />
|
||||
<v-navigation-drawer v-model="menu" right app clipped>
|
||||
<v-list-item-group :key="componentRenderer">
|
||||
<v-list-item inactive>
|
||||
<v-list-item-title class="headline">
|
||||
Verlauf
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider />
|
||||
<div
|
||||
v-for="message in messages"
|
||||
three-line
|
||||
:key="messages.indexOf(message)"
|
||||
>
|
||||
<div v-if="message">
|
||||
<v-list-item three-line inactive @click="storno(message)">
|
||||
<v-list-item-content>
|
||||
<v-progress-linear indeterminate v-if="message.loading" />
|
||||
<v-list-item-title>{{ now(message.date) }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ createSum(message) }} {{ createMessage(message) }}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle class="red--text" v-if="message.storno"
|
||||
>STORNIERT!!!
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle class="red--text" v-else-if="message.error">
|
||||
ERROR!
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-action-text v-if="under5minutes(message.date) && !message.error"
|
||||
>Klicken um zu Stornieren
|
||||
</v-list-item-action-text>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</div>
|
||||
</v-list-item-group>
|
||||
</v-navigation-drawer>
|
||||
<v-card v-if="!loading" :loading="addLoading">
|
||||
<v-card-title>
|
||||
{{ user.firstname }} {{ user.lastname }}
|
||||
<v-spacer />
|
||||
<v-btn @click="menu = !menu" icon>
|
||||
<v-icon>{{ menuIcon }}</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-subtitle v-if="user.limit + getAllSum() > 0">
|
||||
Nur noch {{ ((user.limit + getAllSum()) / 100).toFixed(2) }} €
|
||||
übrig!!
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="10">
|
||||
<v-row>
|
||||
<v-col cols="6" sm="4">
|
||||
<v-btn
|
||||
class="creditBtn"
|
||||
block
|
||||
@click="addingAmount(200)"
|
||||
:color="color"
|
||||
:disabled="user.locked"
|
||||
>2 €</v-btn
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<v-btn
|
||||
class="creditBtn"
|
||||
block
|
||||
@click="addingAmount(100)"
|
||||
:color="color"
|
||||
:disabled="user.locked"
|
||||
>1 €</v-btn
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<v-btn
|
||||
class="creditBtn"
|
||||
block
|
||||
@click="addingAmount(50)"
|
||||
:color="color"
|
||||
:disabled="user.locked"
|
||||
>0,50 €</v-btn
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<v-btn
|
||||
class="creditBtn"
|
||||
block
|
||||
@click="addingAmount(40)"
|
||||
:color="color"
|
||||
:disabled="user.locked"
|
||||
>0,40 €</v-btn
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<v-btn
|
||||
class="creditBtn"
|
||||
block
|
||||
@click="addingAmount(20)"
|
||||
:color="color"
|
||||
:disabled="user.locked"
|
||||
>0,20 €</v-btn
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<v-btn
|
||||
class="creditBtn"
|
||||
block
|
||||
@click="addingAmount(10)"
|
||||
:color="color"
|
||||
:disabled="user.locked"
|
||||
>0,10 €</v-btn
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-text-field
|
||||
outlined
|
||||
type="number"
|
||||
v-model="value"
|
||||
label="Benutzerdefinierter Betrag"
|
||||
:disabled="user.locked"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
fab
|
||||
:color="color"
|
||||
@click="addAmountMore()"
|
||||
:disabled="user.locked"
|
||||
>
|
||||
<v-icon>{{ plus }}</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col align-self="center">
|
||||
<v-row>
|
||||
<v-list-item>
|
||||
<v-list-item-content class="text-center">
|
||||
<v-list-item-action-text :class="getColor(getAllSum())"
|
||||
>{{ (getAllSum() / 100).toFixed(2) }}
|
||||
€
|
||||
</v-list-item-action-text>
|
||||
<v-list-item-action-text v-if="toSetAmount">
|
||||
- {{ (toSetAmount / 100).toFixed(2) }}
|
||||
</v-list-item-action-text>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-alert v-if="user.locked" type="error"
|
||||
>{{ user.firstname }} darf nicht mehr anschreiben.
|
||||
{{ user.firstname }} sollte sich lieber mal beim Finanzer
|
||||
melden.</v-alert
|
||||
>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-snackbar
|
||||
v-for="message in messages"
|
||||
:key="messages.indexOf(message)"
|
||||
:color="message.error ? 'error' : 'success'"
|
||||
bottom
|
||||
:timeout="0"
|
||||
:multi-line="true"
|
||||
v-model="message.visible"
|
||||
vertical
|
||||
>
|
||||
<div class="title">
|
||||
<p style="font-size: 5em; margin: 20px">{{ createSum(message) }}</p>
|
||||
{{ createMessage(message) }}
|
||||
</div>
|
||||
<div>
|
||||
{{ now(message.date) }}
|
||||
</div>
|
||||
<v-btn color="white" icon @click="message.visible = false">
|
||||
<v-icon>
|
||||
{{ close }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
<ConnectionError/>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { mdiMenu, mdiPlus, mdiClose } from '@mdi/js'
|
||||
import AddAmountSkeleton from './Skeleton/AddAmountSkeleton'
|
||||
import ConnectionError from "@/components/ConnectionError";
|
||||
export default {
|
||||
name: 'AddAmount',
|
||||
components: {ConnectionError, AddAmountSkeleton },
|
||||
data() {
|
||||
return {
|
||||
color: 'green accent-4',
|
||||
value: null,
|
||||
plus: mdiPlus,
|
||||
menu: false,
|
||||
dialog: false,
|
||||
componentRenderer: 0,
|
||||
timer: '',
|
||||
menuIcon: mdiMenu,
|
||||
close: mdiClose,
|
||||
checkValidate: false,
|
||||
stornoMessage: null,
|
||||
timeout: null,
|
||||
toSetAmount: null,
|
||||
overLimitUser: null,
|
||||
overOverLimit: null,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.timer = setInterval(this.forceRender, 1000)
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addAmount: 'user/addAmount',
|
||||
commitStorno: 'user/storno'
|
||||
}),
|
||||
continueAdd(user) {
|
||||
this.overLimitUser = null
|
||||
user.checkedOverLimit = true
|
||||
if (this.value) {
|
||||
this.addAmount(Math.round(Math.abs(this.value * 100)))
|
||||
setTimeout(() => {
|
||||
this.value = null
|
||||
this.toSetAmount = null
|
||||
}, 300)
|
||||
} else {
|
||||
user.timeout = setTimeout(() => {
|
||||
this.addAmount(this.toSetAmount)
|
||||
setTimeout(() => {
|
||||
this.toSetAmount = null
|
||||
}, 300)
|
||||
}, 2000)
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
this.toSetAmount = null
|
||||
this.value = null
|
||||
this.overLimitUser = null
|
||||
},
|
||||
checkOverLimitIsValid(user) {
|
||||
if (this.toSetAmount && user.autoLock) {
|
||||
if ((this.getAllSum() - Number.parseInt(this.toSetAmount)) < -(user.limit + 500)) {
|
||||
this.overOverLimit = user
|
||||
this.toSetAmount = null
|
||||
this.value = null
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
checkOverLimit(user) {
|
||||
if (this.toSetAmount) {
|
||||
if (( this.getAllSum() - this.toSetAmount) < -user.limit) {
|
||||
return user.checkedOverLimit ? false : true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
addingAmount(amount) {
|
||||
clearTimeout(this.timeout)
|
||||
this.toSetAmount = this.toSetAmount ? this.toSetAmount + amount : amount
|
||||
if (this.checkOverLimitIsValid(this.user)) {
|
||||
if (this.checkOverLimit(this.user) && this.user.autoLock) {
|
||||
this.overLimitUser = this.user
|
||||
} else {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.addAmount(this.toSetAmount)
|
||||
setTimeout(() => {
|
||||
this.toSetAmount = null
|
||||
}, 300)
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
},
|
||||
forceRender() {
|
||||
this.componentRenderer += 1
|
||||
},
|
||||
getColor(value) {
|
||||
return value >= 0 ? 'title green--text' : 'title red--text'
|
||||
},
|
||||
getAllSum() {
|
||||
if (this.user)
|
||||
return (
|
||||
this.user.creditList[this.year][2].sum +
|
||||
this.user.creditList[this.year][1].last
|
||||
)
|
||||
return 0
|
||||
},
|
||||
storno(message) {
|
||||
if (!message.error) {
|
||||
if (!this.under5minutes(message.date)) this.dialog = true
|
||||
else {
|
||||
this.checkValidate = true
|
||||
this.stornoMessage = message
|
||||
}
|
||||
}
|
||||
},
|
||||
acceptStorno() {
|
||||
this.commitStorno({
|
||||
amount: this.stornoMessage.amount,
|
||||
date: this.stornoMessage.date
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.cancelStorno()
|
||||
}, 300)
|
||||
},
|
||||
cancelStorno() {
|
||||
this.stornoMessage = null
|
||||
this.checkValidate = null
|
||||
},
|
||||
addAmountMore() {
|
||||
this.toSetAmount = this.toSetAmount
|
||||
? this.toSetAmount + Math.round(Math.abs(this.value * 100))
|
||||
: Math.round(Math.abs(this.value * 100))
|
||||
if (this.checkOverLimitIsValid(this.user)) {
|
||||
if (this.checkOverLimit(this.user) && this.user.autoLock) {
|
||||
this.overLimitUser = this.user
|
||||
}
|
||||
else {
|
||||
this.addAmount(Math.abs(this.value * 100))
|
||||
setTimeout(() => {
|
||||
this.value = null
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
},
|
||||
createSum(message) {
|
||||
var text = '' + (message.amount / 100).toFixed(2) + '€'
|
||||
return text
|
||||
},
|
||||
createMessage(message) {
|
||||
var text = ''
|
||||
if (message.error) {
|
||||
text =
|
||||
' konnten nicht zu ' +
|
||||
message.user.firstname +
|
||||
' ' +
|
||||
message.user.lastname +
|
||||
' hinzufügen.'
|
||||
} else {
|
||||
text =
|
||||
' wurde zu ' +
|
||||
message.user.firstname +
|
||||
' ' +
|
||||
message.user.lastname +
|
||||
' hinzugefügt.'
|
||||
}
|
||||
return text
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'user/user',
|
||||
year: 'user/year',
|
||||
loading: 'user/loading',
|
||||
addLoading: 'user/addLoading',
|
||||
messages: 'user/messages'
|
||||
}),
|
||||
under5minutes() {
|
||||
return now => {
|
||||
var actual = new Date()
|
||||
return actual - now < 15000
|
||||
}
|
||||
},
|
||||
now() {
|
||||
return now => {
|
||||
var actual = new Date()
|
||||
var zero = new Date(0)
|
||||
var date = new Date(actual - now)
|
||||
if (date.getFullYear() === zero.getFullYear()) {
|
||||
if (date.getMonth() === zero.getMonth()) {
|
||||
if (date.getDate() === zero.getDate()) {
|
||||
if (date.getHours() === zero.getDate()) {
|
||||
if (date.getMinutes() < 1) {
|
||||
return 'vor ' + date.getSeconds() + ' Sekunden'
|
||||
} else if (date.getMinutes() < 10) {
|
||||
return 'vor ' + date.getMinutes() + ' Minuten'
|
||||
} else {
|
||||
return (
|
||||
(now.getHours() < 10 ? '0' : '') +
|
||||
now.getHours() +
|
||||
':' +
|
||||
(now.getMinutes() < 10 ? '0' : '') +
|
||||
now.getMinutes()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
(now.getHours() < 10 ? '0' : '') +
|
||||
now.getHours() +
|
||||
':' +
|
||||
(now.getMinutes() < 10 ? '0' : '') +
|
||||
now.getMinutes()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
now.getDate() +
|
||||
'.' +
|
||||
now.getMonth() +
|
||||
'.' +
|
||||
now.getFullYear() +
|
||||
' ' +
|
||||
(now.getHours() < 10 ? '0' : '') +
|
||||
now.getHours() +
|
||||
':' +
|
||||
(now.getMinutes() < 10 ? '0' : '') +
|
||||
now.getMinutes()
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,458 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card v-if="user" :loading="loading" style="margin-top: 3px">
|
||||
<v-card-title>{{ user.firstname }} {{ user.lastname }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
outlined
|
||||
label="Vornamen"
|
||||
:placeholder="user.firstname"
|
||||
v-model="firstname"
|
||||
readonly
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
outlined
|
||||
label="Nachname"
|
||||
:placeholder="user.lastname"
|
||||
v-model="lastname"
|
||||
readonly
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
outlined
|
||||
label="Benutzername"
|
||||
:placeholder="user.username"
|
||||
v-model="username"
|
||||
readonly
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
ref="mail"
|
||||
outlined
|
||||
label="E-Mail"
|
||||
:placeholder="user.mail"
|
||||
v-model="mail"
|
||||
readonly
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
outlined
|
||||
label="neues Password"
|
||||
type="password"
|
||||
v-model="password"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-form ref="newPassword">
|
||||
<v-text-field
|
||||
ref="password"
|
||||
v-model="controlPassword"
|
||||
outlined
|
||||
label="neues Password bestätigen"
|
||||
type="password"
|
||||
:disabled="!password"
|
||||
:rules="[equal_password]"
|
||||
/>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider />
|
||||
<v-row>
|
||||
<v-col cols="12" sm="4">
|
||||
<v-text-field
|
||||
outlined
|
||||
label="Sperrlimit"
|
||||
readonly
|
||||
:value="(user.limit / 100).toFixed(2).toString() + '€'"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4">
|
||||
<v-combobox
|
||||
outlined
|
||||
label="Sperrstatus"
|
||||
v-model="lock"
|
||||
append-icon
|
||||
readonly
|
||||
>
|
||||
<template v-slot:selection="data">
|
||||
<v-chip :color="lockColor">
|
||||
{{ data.item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4">
|
||||
<v-combobox
|
||||
outlined
|
||||
label="Autosperre"
|
||||
v-model="autoLock"
|
||||
readonly
|
||||
append-icon
|
||||
>
|
||||
<template v-slot:selection="data">
|
||||
<v-chip :color="autoLockColor">
|
||||
{{ data.item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col v-bind:class="{ fulllineText: isFulllineText }">
|
||||
<v-combobox
|
||||
outlined
|
||||
multiple
|
||||
label="Gruppen"
|
||||
readonly
|
||||
v-model="user.group"
|
||||
append-icon
|
||||
>
|
||||
<template v-slot:selection="data">
|
||||
<v-icon class="ma-2">{{
|
||||
data.item === 'user'
|
||||
? person
|
||||
: data.item === 'bar'
|
||||
? bar
|
||||
: data.item === 'moneymaster'
|
||||
? finanzer
|
||||
: data.item === 'gastro'
|
||||
? gastro
|
||||
: ''
|
||||
}}</v-icon>
|
||||
</template>
|
||||
</v-combobox>
|
||||
</v-col>
|
||||
<v-col v-bind:class="{ fulllineText: isFulllineText }">
|
||||
<v-text-field
|
||||
outlined
|
||||
:value="computeStatus"
|
||||
readonly
|
||||
label="Mitgliedsstatus"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col v-bind:class="{ fulllineText: isFulllineText }">
|
||||
<v-text-field
|
||||
outlined
|
||||
:value="user.voting ? 'ja' : 'nein'"
|
||||
readonly
|
||||
label="Stimmrecht"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col v-bind:class="{ fulllineText: isFulllineText }">
|
||||
<v-combobox
|
||||
chips
|
||||
outlined
|
||||
multiple
|
||||
label="Arbeitsgruppen"
|
||||
readonly
|
||||
v-model="user.workgroups"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
append-icon
|
||||
>
|
||||
<template v-slot:selection="data">
|
||||
<v-chip>{{ data.item.name }}</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="subtitle-1">
|
||||
Gespeicherte Sessions
|
||||
</div>
|
||||
<v-card v-for="token in tokens" :key="token.id" outlined>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-col>
|
||||
Betriebssystem
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-icon>
|
||||
{{
|
||||
token.platform === 'macos' || token.platform === 'iphone'
|
||||
? apple
|
||||
: token.platform === 'windows'
|
||||
? windows
|
||||
: token.platform === 'android'
|
||||
? android
|
||||
: token.platform === 'linux'
|
||||
? linux
|
||||
: token.platfrom
|
||||
}}
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-if="
|
||||
token.platform === 'macos' || token.platform === 'iphone'
|
||||
"
|
||||
>
|
||||
{{ token.platform === 'macos' ? mac : iphone }}
|
||||
</v-icon>
|
||||
</v-col>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-col>
|
||||
Browser
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-icon>
|
||||
{{
|
||||
token.browser === 'chrome'
|
||||
? chrome
|
||||
: token.browser === 'firefox'
|
||||
? firefox
|
||||
: token.browser === 'opera'
|
||||
? opera
|
||||
: token.browser === 'safari'
|
||||
? safari
|
||||
: token.browser === 'msie'
|
||||
? msie
|
||||
: token.browser
|
||||
}}
|
||||
</v-icon>
|
||||
</v-col>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-col>
|
||||
Letzte Aktualisierung
|
||||
</v-col>
|
||||
<v-col>
|
||||
{{ token.timestamp.day }}.{{ token.timestamp.month }}.{{
|
||||
token.timestamp.year
|
||||
}}
|
||||
um
|
||||
{{
|
||||
10 > token.timestamp.hour
|
||||
? '0' + String(token.timestamp.hour)
|
||||
: token.timestamp.hour
|
||||
}}:{{
|
||||
10 > token.timestamp.minute
|
||||
? '0' + String(token.timestamp.minute)
|
||||
: token.timestamp.minute
|
||||
}}:{{
|
||||
10 > token.timestamp.second
|
||||
? '0' + String(token.timestamp.second)
|
||||
: token.timestamp.second
|
||||
}}</v-col
|
||||
>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-col>
|
||||
Lebenszeit
|
||||
</v-col>
|
||||
<v-col>
|
||||
{{ calcLifefime(token.lifetime) }}
|
||||
</v-col>
|
||||
</v-col>
|
||||
<v-col class="text-right">
|
||||
<v-btn icon @click="deleteToken(token)">
|
||||
<v-icon>
|
||||
{{ trashCan }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-form ref="acceptedPasswordTest">
|
||||
<v-text-field
|
||||
outlined
|
||||
label="Passwort"
|
||||
v-model="acceptedPassword"
|
||||
type="password"
|
||||
ref="acceptedPassword"
|
||||
:rules="[empty_password]"
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
<v-btn text color="primary" @click="save">Speichern</v-btn>
|
||||
</v-card-actions>
|
||||
<v-snackbar
|
||||
v-if="error ? error.value : false"
|
||||
:color="error ? (error.error ? 'error' : 'success') : ''"
|
||||
:value="error"
|
||||
v-model="error"
|
||||
:timeout="0"
|
||||
>
|
||||
{{ error ? error.value : null }}
|
||||
</v-snackbar>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiGlassCocktail,
|
||||
mdiCurrencyEur,
|
||||
mdiFoodForkDrink,
|
||||
mdiApple,
|
||||
mdiGoogleChrome,
|
||||
mdiFirefox,
|
||||
mdiOpera,
|
||||
mdiInternetExplorer,
|
||||
mdiAppleSafari,
|
||||
mdiLaptopMac,
|
||||
mdiCellphoneIphone,
|
||||
mdiTrashCan,
|
||||
mdiAndroid,
|
||||
mdiWindows,
|
||||
mdiLinux
|
||||
} from '@mdi/js'
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
export default {
|
||||
name: 'Config',
|
||||
data() {
|
||||
return {
|
||||
apple: mdiApple,
|
||||
mac: mdiLaptopMac,
|
||||
iphone: mdiCellphoneIphone,
|
||||
android: mdiAndroid,
|
||||
windows: mdiWindows,
|
||||
linux: mdiLinux,
|
||||
chrome: mdiGoogleChrome,
|
||||
firefox: mdiFirefox,
|
||||
opera: mdiOpera,
|
||||
msie: mdiInternetExplorer,
|
||||
safari: mdiAppleSafari,
|
||||
person: mdiAccount,
|
||||
bar: mdiGlassCocktail,
|
||||
finanzer: mdiCurrencyEur,
|
||||
gastro: mdiFoodForkDrink,
|
||||
username: null,
|
||||
mail: null,
|
||||
firstname: null,
|
||||
lastname: null,
|
||||
password: null,
|
||||
controlPassword: null,
|
||||
trashCan: mdiTrashCan,
|
||||
isFulllineText: false,
|
||||
acceptedPassword: null,
|
||||
passError: null,
|
||||
equal_password: value =>
|
||||
this.password === value || 'Passwörter sind nicht identisch.',
|
||||
email: value => {
|
||||
if (value.length > 0) {
|
||||
const pattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
return pattern.test(value) || 'keine gültige E-Mail'
|
||||
}
|
||||
return true
|
||||
},
|
||||
empty_password: data => {
|
||||
return !!data || 'Password wird bentögigt'
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(function() {
|
||||
window.addEventListener('resize', this.getWindowWidth)
|
||||
this.getWindowWidth()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
saveConfig: 'user/saveConfig',
|
||||
getStatus: 'user/getStatus',
|
||||
getTokens: 'user/getTokens',
|
||||
deleteToken: 'user/deleteToken'
|
||||
}),
|
||||
getWindowWidth() {
|
||||
this.isFulllineText = document.documentElement.clientWidth <= 600
|
||||
},
|
||||
save() {
|
||||
let user = {}
|
||||
if (this.firstname) user.firstname = this.firstname
|
||||
if (this.lastname) user.lastname = this.lastname
|
||||
if (this.username) user.username = this.username
|
||||
if (this.$refs.mail.validate()) {
|
||||
if (this.mail) user.mail = this.mail
|
||||
}
|
||||
if (this.$refs.newPassword.validate()) {
|
||||
if (this.password) user.password = this.password
|
||||
} else {
|
||||
return
|
||||
}
|
||||
console.log(this.$refs.acceptedPasswordTest.validate())
|
||||
if (this.$refs.acceptedPasswordTest.validate()) {
|
||||
this.saveConfig({
|
||||
oldUsername: user.username,
|
||||
...user,
|
||||
acceptedPassword: this.acceptedPassword
|
||||
})
|
||||
this.$refs.acceptedPassword.reset()
|
||||
} else {
|
||||
this.passError = 'Du musst dein Password eingeben'
|
||||
}
|
||||
this.password = null
|
||||
this.controlPassword = null
|
||||
},
|
||||
calcLifefime(time) {
|
||||
if (time < 60) return String(time) + 'Sekunden'
|
||||
time = Math.round(time / 60)
|
||||
if (time < 60) return String(time) + 'Minuten'
|
||||
time = Math.round(time / 60)
|
||||
if (time < 24) return String(time) + 'Stunden'
|
||||
time = Math.round(time / 24)
|
||||
if (time < 7) return String(time) + 'Tage'
|
||||
time = Math.round(time / 7)
|
||||
if (time < 30) return String(time) + 'Wochen'
|
||||
time = Math.round(time / 30)
|
||||
if (time < 12) return String(time) + 'Monate'
|
||||
time = Math.round(time / 12)
|
||||
return String(time) + 'Jahre'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'user/user',
|
||||
error: 'user/error',
|
||||
loading: 'user/loading',
|
||||
status: 'user/status',
|
||||
tokens: 'user/tokens'
|
||||
}),
|
||||
lock() {
|
||||
return this.user.locked ? 'gesperrt' : 'nicht gesperrt'
|
||||
},
|
||||
lockColor() {
|
||||
return this.user.locked ? 'red' : 'green'
|
||||
},
|
||||
autoLock() {
|
||||
return this.user.autoLock ? 'aktiviert' : 'deaktiviert'
|
||||
},
|
||||
autoLockColor() {
|
||||
return this.user.autoLock ? 'green' : 'red'
|
||||
},
|
||||
computeStatus() {
|
||||
try {
|
||||
return this.status.find(a => a.id == this.user.statusgroup).name
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getStatus()
|
||||
this.getTokens()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fulllineText {
|
||||
flex-basis: unset;
|
||||
}
|
||||
</style>
|
|
@ -1,116 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-toolbar>
|
||||
<v-toolbar-title>Gesamtübersicht</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-toolbar-items>
|
||||
<v-text-field
|
||||
v-model="filter"
|
||||
style="margin-top: 3px"
|
||||
outlined
|
||||
type="number"
|
||||
:rules="[isNumber]"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<v-icon>{{magnify}}</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<CreditOverviewSkeleton v-if="loading" />
|
||||
<div v-for="year in years" :key="years.indexOf(year)">
|
||||
<v-card style="margin-top: 3px" v-if="isFiltered(year)">
|
||||
<v-card-title>{{ year }}</v-card-title>
|
||||
<Table v-bind:user="user" v-bind:year="year" />
|
||||
<v-container fluid>
|
||||
<v-col>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-label>Vorjahr:</v-label>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
outlined
|
||||
:text-color="getLastColor(user.creditList[year][1].last)"
|
||||
>{{ (user.creditList[year][1].last / 100).toFixed(2) }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-label>Gesamt:</v-label>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
outlined
|
||||
x-large
|
||||
:text-color="
|
||||
getLastColor(
|
||||
getAllSum(
|
||||
user.creditList[year][2].sum,
|
||||
user.creditList[year][1].last
|
||||
)
|
||||
)
|
||||
"
|
||||
>
|
||||
{{
|
||||
(
|
||||
getAllSum(
|
||||
user.creditList[year][2].sum,
|
||||
user.creditList[year][1].last
|
||||
) / 100
|
||||
).toFixed(2)
|
||||
}}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import Table from '../finanzer/Table'
|
||||
import CreditOverviewSkeleton from './Skeleton/CreditOverviewSkeleton'
|
||||
import { mdiMagnify } from '@mdi/js'
|
||||
export default {
|
||||
name: 'CreditOverview',
|
||||
components: { CreditOverviewSkeleton, Table },
|
||||
data() {
|
||||
return {
|
||||
isNumber: value => Number.isInteger(parseInt(value === '' ? 0 : value)) || "Muss eine Zahl sein.",
|
||||
filter: '',
|
||||
magnify: mdiMagnify
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getLastColor(value) {
|
||||
return value < 0 ? 'red' : 'green'
|
||||
},
|
||||
getAllSum(sum, lastYear) {
|
||||
return lastYear + sum
|
||||
},
|
||||
isFiltered(value) {
|
||||
return value.toString().includes(this.filter)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'user/user',
|
||||
loading: 'user/loading'
|
||||
}),
|
||||
years() {
|
||||
let years = []
|
||||
if (this.user) {
|
||||
for (let year in this.user.creditList) {
|
||||
years.unshift(parseInt(year))
|
||||
}
|
||||
}
|
||||
return years
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,208 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card tile :loading="jobInvitesLoading">
|
||||
<v-card-title>
|
||||
Eingehende Einladungen
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel
|
||||
v-for="jobInvite in jobInvitesToMe"
|
||||
:key="jobInvite.id"
|
||||
@click.once="seenJobIvnite(jobInvite)"
|
||||
>
|
||||
<v-expansion-panel-header>
|
||||
<div>
|
||||
{{ jobInvite.on_date.getDate() }}.{{
|
||||
jobInvite.on_date.getMonth() + 1
|
||||
}}.{{ jobInvite.on_date.getFullYear() }} von
|
||||
<v-badge dot :value="!jobInvite.watched" color="red">
|
||||
{{ jobInvite.from_user.firstname }}
|
||||
{{ jobInvite.from_user.lastname }}
|
||||
</v-badge>
|
||||
</div>
|
||||
<v-row class="text-right" style="margin-right: 5px">
|
||||
<v-col>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
v-if="jobInvitesLoading"
|
||||
></v-progress-circular>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-icon color="green" v-show="userInWorker(jobInvite)">
|
||||
{{ check }}
|
||||
</v-icon>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content :eager="true">
|
||||
<v-row class="text-right">
|
||||
<v-col>
|
||||
<v-btn icon @click="updatingJobInvite(jobInvite)">
|
||||
<v-icon>
|
||||
{{ jobInvite.watched ? seen : notSeen }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Day
|
||||
:day="jobInvite.day"
|
||||
:long="true"
|
||||
:loading="jobInvite.day.loading"
|
||||
@addingJob="addingJob(jobInvite, $event)"
|
||||
@deletingJob="deletingJob(jobInvite, $event)"
|
||||
@sendInvites="setJobInvites"
|
||||
@sendRequests="setJobRequests"
|
||||
@deleteJobInvite="deleteInvite"
|
||||
@deleteJobRequest="deleteRequest"
|
||||
/>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card tile :loading="jobInvitesLoading">
|
||||
<v-card-title>
|
||||
Versendete Einladungen
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel
|
||||
v-for="jobInvite in jobInvitesFromMe"
|
||||
:key="jobInvite.id"
|
||||
@click.once="seenJobIvnite(jobInvite)"
|
||||
>
|
||||
<v-expansion-panel-header>
|
||||
|
||||
<div>
|
||||
{{ jobInvite.on_date.getDate() }}.{{
|
||||
jobInvite.on_date.getMonth() + 1
|
||||
}}.{{ jobInvite.on_date.getFullYear() }} an
|
||||
<v-badge :value="jobInvite.watched" icon="mdi-eye" color="grey" inline>
|
||||
{{ jobInvite.to_user.firstname }}
|
||||
{{ jobInvite.to_user.lastname }}
|
||||
</v-badge>
|
||||
</div>
|
||||
<v-row class="text-right" style="margin-right: 5px">
|
||||
<v-col>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
v-if="jobInvitesLoading"
|
||||
></v-progress-circular>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-icon color="green" v-show="userInWorker(jobInvite)">
|
||||
{{ check }}
|
||||
</v-icon>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content :eager="true">
|
||||
<v-row class="text-right">
|
||||
<v-col>
|
||||
<v-btn icon @click="deleteInvite(jobInvite)">
|
||||
<v-icon>
|
||||
{{trashCan}}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Day
|
||||
:day="jobInvite.day"
|
||||
:long="true"
|
||||
:loading="jobInvite.day.loading"
|
||||
@addingJob="addingJob(jobInvite, $event)"
|
||||
@deletingJob="deletingJob(jobInvite, $event)"
|
||||
@sendInvites="setJobInvites"
|
||||
@sendRequests="setJobRequests"
|
||||
@deleteJobInvite="deleteInvite"
|
||||
@deleteJobRequest="deleteRequest"
|
||||
/>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { mdiEyeOff, mdiEyeCheck, mdiCheck, mdiTrashCan } from '@mdi/js'
|
||||
import Day from '@/components/user/Jobs/Day'
|
||||
export default {
|
||||
name: 'JobInvites',
|
||||
components: { Day },
|
||||
data() {
|
||||
return {
|
||||
notSeen: mdiEyeOff,
|
||||
seen: mdiEyeCheck,
|
||||
check: mdiCheck,
|
||||
trashCan: mdiTrashCan,
|
||||
showNotSeen: false,
|
||||
showSeen: false,
|
||||
update: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
getJobInvites: 'jobInvites/getJobInvites',
|
||||
addJob: 'jobInvites/addJob',
|
||||
setJobInvites: 'jobInvites/setJobInvites',
|
||||
updateJobInviteToMe: 'jobInvites/updateJobInviteToMe',
|
||||
deleteJob: 'jobInvites/deleteJob',
|
||||
setJobRequests: 'jobRequests/setJobRequests',
|
||||
deleteInvite: 'jobInvites/deleteJobInviteFromMe',
|
||||
deleteRequest: 'jobRequests/deleteJobRequestFromMe'
|
||||
}),
|
||||
forceRender() {
|
||||
setTimeout(() => {
|
||||
this.update += 0
|
||||
}, 500)
|
||||
},
|
||||
updatingJobInvite(jobInvite) {
|
||||
jobInvite.watched = !jobInvite.watched
|
||||
this.updateJobInviteToMe(jobInvite)
|
||||
},
|
||||
seenJobIvnite(jobInvite) {
|
||||
if (!jobInvite.watched) {
|
||||
jobInvite.watched = true
|
||||
this.updateJobInviteToMe(jobInvite)
|
||||
}
|
||||
},
|
||||
addingJob(jobInvite, event) {
|
||||
this.seenJobIvnite(jobInvite)
|
||||
this.addJob(event)
|
||||
this.forceRender()
|
||||
},
|
||||
deletingJob(jobInvite, event) {
|
||||
this.seenJobIvnite(jobInvite)
|
||||
this.deleteJob(event)
|
||||
this.forceRender()
|
||||
},
|
||||
userInWorker(jobinvite) {
|
||||
var jobkinddate = jobinvite.day.jobkinddate.find(item => {
|
||||
return item.worker.find(workeritem => {
|
||||
return workeritem.id === jobinvite.to_user.id
|
||||
})
|
||||
})
|
||||
return !!jobkinddate
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
jobInvitesFromMe: 'jobInvites/jobInvitesFromMe',
|
||||
jobInvitesToMe: 'jobInvites/jobInvitesToMe',
|
||||
jobInvitesLoading: 'jobInvites/jobInvitesLoading',
|
||||
activeUser: 'user/user'
|
||||
})
|
||||
},
|
||||
created() {
|
||||
setTimeout(() => {
|
||||
this.getJobInvites(new Date())
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,224 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card tile :loading="jobRequestsLoading">
|
||||
<v-card-title>
|
||||
Eingehende Anfragen
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel
|
||||
v-for="(jobrequest, index) in jobRequestsToMe"
|
||||
:key="index"
|
||||
>
|
||||
<v-expansion-panel-header @click.once="seenJobRequest(jobrequest)">
|
||||
<div>
|
||||
{{ jobrequest.on_date.getDate() }}.{{
|
||||
jobrequest.on_date.getMonth() + 1
|
||||
}}.{{ jobrequest.on_date.getFullYear() }} von
|
||||
<v-badge dot :value="!jobrequest.watched" color="red">
|
||||
{{ jobrequest.from_user.firstname }}
|
||||
{{ jobrequest.from_user.lastname }}
|
||||
</v-badge>
|
||||
</div>
|
||||
<v-row class="text-right" style="margin-right: 5px">
|
||||
<v-col>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
v-if="jobRequestsLoading"
|
||||
></v-progress-circular>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-icon color="green" v-show="userInWorker(jobrequest)">
|
||||
{{ check }}
|
||||
</v-icon>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<v-row class="text-right">
|
||||
<v-col>
|
||||
<v-btn icon @click="updatingSeenJobRequest(jobrequest)">
|
||||
<v-icon>
|
||||
{{ jobrequest.watched ? seen : notSeen }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Day
|
||||
:day="jobrequest.day"
|
||||
:long="true"
|
||||
@sendRequests="sendingJobRequests(jobrequest, $event)"
|
||||
@addingJob="addJob"
|
||||
@deletingJob="deleteJob"
|
||||
@sendInvites="setJobInvites"
|
||||
@deleteJobInvite="deleteInvite"
|
||||
@deleteJobRequest="deleteJobRequestFromMe"
|
||||
/>
|
||||
<v-row class="text-right">
|
||||
<v-col>
|
||||
<v-btn
|
||||
v-show="!jobrequest.answered"
|
||||
text
|
||||
@click="updatingAcceptedJobRequest(jobrequest)"
|
||||
>Annehmen</v-btn
|
||||
>
|
||||
<div v-show="jobrequest.answered && !jobrequest.accepted">
|
||||
Dieser Dienst wurde schon übertragen.
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card tile :loading="jobRequestsLoading">
|
||||
<v-card-title>
|
||||
Ausgehende Anfragen
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel
|
||||
v-for="(jobrequest, index) in jobRequestsFromMe"
|
||||
:key="index"
|
||||
>
|
||||
<v-expansion-panel-header>
|
||||
<div>
|
||||
{{ jobrequest.on_date.getDate() }}.{{
|
||||
jobrequest.on_date.getMonth() + 1
|
||||
}}.{{ jobrequest.on_date.getFullYear() }} an
|
||||
<v-badge :value="jobrequest.watched" icon="mdi-eye" color="grey" inline>
|
||||
{{ jobrequest.to_user.firstname }}
|
||||
{{ jobrequest.to_user.lastname }}
|
||||
</v-badge>
|
||||
</div>
|
||||
<v-row class="text-right" style="margin-right: 5px">
|
||||
<v-col>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
v-if="jobRequestsLoading"
|
||||
></v-progress-circular>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-icon color="green" v-show="jobrequest.accepted">
|
||||
{{ check }}
|
||||
</v-icon>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<v-row class="text-right">
|
||||
<v-col>
|
||||
<v-btn icon @click="deleteJobRequestFromMe(jobrequest)">
|
||||
<v-icon>
|
||||
{{trashCan}}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Day
|
||||
:day="jobrequest.day"
|
||||
:long="true"
|
||||
@sendRequests="sendingJobRequests(jobrequest, $event)"
|
||||
@addingJob="addJob"
|
||||
@deletingJob="deleteJob"
|
||||
@sendInvites="setJobInvites"
|
||||
@deleteJobInvite="deleteInvite"
|
||||
@deleteJobRequest="deleteJobRequestFromMe"
|
||||
/>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import { mdiEyeOff, mdiEyeCheck, mdiCheck, mdiTrashCan } from '@mdi/js'
|
||||
import Day from '@/components/user/Jobs/Day'
|
||||
export default {
|
||||
name: 'JobTransfer',
|
||||
components: { Day },
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
notSeen: mdiEyeOff,
|
||||
seen: mdiEyeCheck,
|
||||
check: mdiCheck,
|
||||
trashCan: mdiTrashCan,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
getJobRequests: 'jobRequests/getJobRequests',
|
||||
updateJobRequestToMe: 'jobRequests/updateJobRequestToMe',
|
||||
setJobRequests: 'jobRequests/setJobRequests',
|
||||
deleteJobRequestFromMe: 'jobRequests/deleteJobRequestFromMe',
|
||||
deleteInvite: 'jobInvites/deleteJobInviteFromMe',
|
||||
getJobInvites: 'jobInvites/getJobInvites',
|
||||
addJob: 'jobInvites/addJob',
|
||||
setJobInvites: 'jobInvites/setJobInvites',
|
||||
updateJobInviteToMe: 'jobInvites/updateJobInviteToMe',
|
||||
deleteJob: 'jobInvites/deleteJob',
|
||||
}),
|
||||
updatingAcceptedJobRequest(jobRequest) {
|
||||
jobRequest.accepted = true
|
||||
jobRequest.answered = true
|
||||
this.updateJobRequestToMe({ ...jobRequest })
|
||||
setTimeout(() => {
|
||||
this.getJobRequests(), 200
|
||||
})
|
||||
},
|
||||
updatingSeenJobRequest(jobRequest) {
|
||||
jobRequest.watched = !jobRequest.watched
|
||||
this.updateJobRequestToMe({ ...jobRequest })
|
||||
},
|
||||
seenJobRequest(jobRequest) {
|
||||
if (!jobRequest.watched) {
|
||||
jobRequest.watched = true
|
||||
this.updateJobRequestToMe(jobRequest)
|
||||
}
|
||||
},
|
||||
userInWorker(jobrequest) {
|
||||
var jobkinddate = jobrequest.day.jobkinddate.find(item => {
|
||||
return item.worker.find(workeritem => {
|
||||
return workeritem.id === this.activeUser.id
|
||||
})
|
||||
})
|
||||
return !!jobkinddate
|
||||
},
|
||||
sendingJobRequests(jobrequest, event) {
|
||||
this.seenJobRequest(jobrequest)
|
||||
this.setJobRequests(event)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
jobRequestsToMe: 'jobRequests/jobRequestsToMe',
|
||||
jobRequestsFromMe: 'jobRequests/jobRequestsFromMe',
|
||||
jobRequestsLoading: 'jobRequests/jobRequestsLoading',
|
||||
loading: 'user/loading',
|
||||
activeUser: 'user/user'
|
||||
})
|
||||
},
|
||||
created() {
|
||||
if (!this.loading) {
|
||||
this.getJobRequests()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
loading(newValue) {
|
||||
if (!newValue) {
|
||||
this.getJobRequests()
|
||||
}
|
||||
},
|
||||
jobRequestsLoading(newValue, oldValue) {
|
||||
console.log(newValue, oldValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,192 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-toolbar>
|
||||
<v-toolbar-title>Dienstübersicht</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-toolbar-items>
|
||||
<v-btn
|
||||
text
|
||||
icon
|
||||
:to="{
|
||||
name: 'userJobs',
|
||||
params: { year: date.getFullYear(), month: date.getMonth() }
|
||||
}"
|
||||
>
|
||||
<v-icon>{{ keyboard_arrow_left }}</v-icon>
|
||||
</v-btn>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="title">
|
||||
{{ monthArray[date.getMonth()] }}
|
||||
{{ date.getFullYear() }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-btn
|
||||
text
|
||||
icon
|
||||
:to="{
|
||||
name: 'userJobs',
|
||||
params: { year: date.getFullYear(), month: date.getMonth() + 2 }
|
||||
}"
|
||||
>
|
||||
<v-icon>{{ keyboard_arrow_right }}</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
<v-card v-for="week in month" :key="month.indexOf(week)" flat tile>
|
||||
<v-card-title class="subtitle-1 font-weight-bold">
|
||||
Woche vom {{ week.startDate.getDate() }}.{{
|
||||
week.startDate.getMonth() + 1
|
||||
}}.{{ week.startDate.getFullYear() }} bis
|
||||
{{ week.endDate.getDate() }}.{{ week.endDate.getMonth() + 1 }}.{{
|
||||
week.endDate.getFullYear()
|
||||
}}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row justify="start" align="start">
|
||||
<div v-for="day in week.days" :key="day.id">
|
||||
<v-col cols="12">
|
||||
<Day
|
||||
:day="day"
|
||||
:long="false"
|
||||
@addingJob="addJob"
|
||||
@sendInvites="setJobInvites"
|
||||
@deletingJob="deleteJob"
|
||||
@sendRequests="setJobRequests"
|
||||
@deleteJobInvite="deleteInvite"
|
||||
@deleteJobRequest="deleteRequest"
|
||||
/>
|
||||
</v-col>
|
||||
</div>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js'
|
||||
import { mapGetters, mapActions } from 'vuex'
|
||||
import Day from '@/components/user/Jobs/Day'
|
||||
export default {
|
||||
name: 'Jobs',
|
||||
components: { Day },
|
||||
data() {
|
||||
return {
|
||||
keyboard_arrow_left: mdiChevronLeft,
|
||||
keyboard_arrow_right: mdiChevronRight,
|
||||
date: new Date(this.$route.params.year, this.$route.params.month - 1, 1),
|
||||
monthArray: [
|
||||
'Januar',
|
||||
'Februar',
|
||||
'März',
|
||||
'April',
|
||||
'Mai',
|
||||
'Juni',
|
||||
'Juli',
|
||||
'August',
|
||||
'September',
|
||||
'Oktober',
|
||||
'November',
|
||||
'Dezember'
|
||||
]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getActiveUser()
|
||||
this.getAllJobKinds()
|
||||
this.createMonth(this.date)
|
||||
this.getDBUsers()
|
||||
this.getUsers({
|
||||
from_date: {
|
||||
year: this.startDate.getFullYear(),
|
||||
month: this.startDate.getMonth() + 1,
|
||||
day: this.startDate.getDate()
|
||||
},
|
||||
to_date: {
|
||||
year: this.endDate.getFullYear(),
|
||||
month: this.endDate.getMonth() + 1,
|
||||
day: this.endDate.getDate()
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
getActiveUser: 'user/getUser',
|
||||
createMonth: 'jobs/createMonth',
|
||||
getUsers: 'jobs/getUsers',
|
||||
getDBUsers: 'usermanager/getUsers',
|
||||
getAllJobKinds: 'jkm/getAllJobKinds',
|
||||
addJob: 'jobs/addJob',
|
||||
deleteJob: 'jobs/deleteJob',
|
||||
setJobInvites: 'jobInvites/setJobInvites',
|
||||
getJobInvites: 'jobInvites/getJobInvites',
|
||||
getJobRequests: 'jobRequests/getJobRequests',
|
||||
setJobRequests: 'jobRequests/setJobRequests',
|
||||
deleteInvite: 'jobInvites/deleteJobInviteFromMe',
|
||||
deleteRequest: 'jobRequests/deleteJobRequestFromMe'
|
||||
}),
|
||||
changeMonth(value) {
|
||||
if (value === -1) {
|
||||
this.date = new Date(this.date.getFullYear(), this.date.getMonth() - 1)
|
||||
} else {
|
||||
this.date = new Date(this.date.getFullYear(), this.date.getMonth() + 1)
|
||||
}
|
||||
this.createMonth(this.date)
|
||||
this.getUsers({
|
||||
from_date: {
|
||||
year: this.startDate.getFullYear(),
|
||||
month: this.startDate.getMonth() + 1,
|
||||
day: this.startDate.getDate()
|
||||
},
|
||||
to_date: {
|
||||
year: this.endDate.getFullYear(),
|
||||
month: this.endDate.getMonth() + 1,
|
||||
day: this.endDate.getDate()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
month: 'jobs/month',
|
||||
startDate: 'jobs/getStartDate',
|
||||
endDate: 'jobs/getEndDate',
|
||||
loading: 'user/loading'
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.getActiveUser()
|
||||
this.date = new Date(
|
||||
this.$route.params.year,
|
||||
this.$route.params.month - 1,
|
||||
1
|
||||
)
|
||||
this.getAllJobKinds()
|
||||
this.createMonth(this.date)
|
||||
this.getDBUsers()
|
||||
this.getUsers({
|
||||
from_date: {
|
||||
year: this.startDate.getFullYear(),
|
||||
month: this.startDate.getMonth() + 1,
|
||||
day: this.startDate.getDate()
|
||||
},
|
||||
to_date: {
|
||||
year: this.endDate.getFullYear(),
|
||||
month: this.endDate.getMonth() + 1,
|
||||
day: this.endDate.getDate()
|
||||
}
|
||||
})
|
||||
},
|
||||
loading(newValue) {
|
||||
if (!newValue) {
|
||||
this.getJobInvites()
|
||||
this.getJobRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|