Compare commits

...

482 Commits

Author SHA1 Message Date
Tim Gröger b1e4879881 Merge pull request 'release v2.0.0' (!4) from develop into master
Reviewed-on: #4
2024-01-18 15:15:06 +00:00
Tim Gröger ee7e03ce28 update to version to 2.0.0 2024-01-16 19:52:45 +01:00
Tim Gröger 2928c241ad fix prevention that click card when click on notification buttons 2024-01-16 19:36:49 +01:00
Tim Gröger fe9ec96ce1 update behavior of left drawer 2024-01-16 14:37:38 +01:00
Tim Gröger 417689b725 update dependencies
for modify-source-webpack-plugin new operation function because of api-change
2023-06-14 12:10:33 +02:00
Tim Gröger 847e923265 update to v1.0.0-alpha.2 2023-05-15 23:58:54 +02:00
Tim Gröger 4cb0362bb7 fix capacitor 2023-05-15 23:57:56 +02:00
Tim Gröger a46c41cb5b better widget dashboard 2023-05-14 00:02:08 +02:00
Tim Gröger 3d55f2d2ae fix hyphanation 2023-05-13 09:44:52 +02:00
Tim Gröger 3689da810c add all plugins 2023-05-06 12:35:27 +02:00
Tim Gröger e6d9054256 update dependencies 2023-05-06 11:51:24 +02:00
Tim Gröger ab45bf3667 if capacitor only notification is used when backend is offline 2023-05-05 14:25:31 +02:00
Tim Gröger 857d07040b update capacitor dependencies 2023-05-05 07:53:12 +02:00
groegert e07df08822 eleminate permanent spinner on login
continuous-integration/woodpecker the build was successful Details
2021-12-26 12:01:22 +01:00
Ferdinand Thiessen ec28857af5 chore(deps): Update dependencies
continuous-integration/woodpecker the build was successful Details
2021-12-23 03:21:33 +01:00
Ferdinand Thiessen 1c452e23fe fix(api): Ensure everything is cleared on logout 2021-12-23 03:20:34 +01:00
Ferdinand Thiessen 195593ddc5 feat(ci): Added woodpecker CI
continuous-integration/woodpecker the build was successful Details
2021-12-14 15:48:53 +01:00
Ferdinand Thiessen 2425e6cf2f fix(boot): Fix navigation guards so users get redirected after logging in 2021-12-06 13:14:42 +01:00
Ferdinand Thiessen a9edc12494 fix(api): Allow userid as parameter for avatarURL 2021-12-06 12:51:47 +01:00
Ferdinand Thiessen 1525de1469 chore(deps): Update dependencies 2021-12-06 00:41:13 +01:00
Ferdinand Thiessen 6a75d1bf51 chore(format): Fix formatting, enforce prettier style 2021-12-06 00:40:50 +01:00
Ferdinand Thiessen f9f66e7172 chore(api): Update dependencies, cleanup, tag new version
No need for prettier stuff, it is done by flaschengeists package.json
2021-12-05 23:57:37 +01:00
Ferdinand Thiessen e9c0086859 feat(api): Add component for displaying an users avatar with fallback image 2021-12-05 23:35:32 +01:00
Ferdinand Thiessen b2c70a6657 feat(api): user store decides if data is outdated based on the last update rather then last local changes. 2021-12-05 20:57:57 +01:00
Ferdinand Thiessen f27212f60e feat(api): user store now handels deleted users.
`.users` now is a getter that filters out deleted users.
For all users, including deleted, use `_users` property (should not be needed as `getUser` will return the needed information as well).
2021-12-05 20:57:15 +01:00
Ferdinand Thiessen 9eb5412c14 fix(api): Session is required to load current user 2021-12-05 20:51:44 +01:00
Ferdinand Thiessen 8e552ba508 feat(app): Set lifetime to 14 days on capacitor.
Do not annoy users on capacitory apps with daily logging in again
2021-12-05 13:45:50 +01:00
Ferdinand Thiessen 49c3ec74ba fix(api): Load current user in login function of mainStore 2021-12-05 13:45:50 +01:00
Ferdinand Thiessen 83f32ea82a feat(api): Get current session 2021-12-05 13:30:44 +01:00
Ferdinand Thiessen 656d7a9e3c feat(api): Add delete user function to user store 2021-12-02 21:32:43 +01:00
Ferdinand Thiessen 8fca175d39 chore(api) Tag a new api version 2021-11-29 18:18:00 +01:00
Ferdinand Thiessen 6c219c5226 feat(api) Added function to get the URL of an user avatar 2021-11-29 17:23:55 +01:00
Ferdinand Thiessen cb43b8a39b fix(core) Token should be added to headers instead of replacing them 2021-11-29 17:20:26 +01:00
Tim Gröger acf1816b55 [core] logout on capicitor in drawer 2021-11-28 14:46:00 +01:00
Ferdinand Thiessen 02f60335d4 [api] tag new version 2021-11-27 02:47:14 +01:00
Ferdinand Thiessen d9267bcc0a [api] Save whole session in PersistantStore. 2021-11-27 02:44:40 +01:00
Ferdinand Thiessen 6732921ff7 [core] Improved backend setup on login page 2021-11-27 01:31:43 +01:00
Ferdinand Thiessen 7a705d5f9a [core] Ensure everything is initialized in the correct order.
Make sure api is initialized before making any requests.
2021-11-27 01:18:58 +01:00
Ferdinand Thiessen 368ca23c56 General cleanup of unused code and small fixes 2021-11-27 00:49:12 +01:00
Tim Gröger d82e025700 Merge branch 'capacitor' into develop 2021-11-26 22:14:51 +01:00
Tim Gröger 07e1966471 [capacitor] disable allowLinkPreview for ios 2021-11-26 22:13:35 +01:00
Ferdinand Thiessen 29c085bd2c [core] Save baseURL into PersistentStorage 2021-11-26 20:55:45 +01:00
Ferdinand Thiessen 1b152b52f5 [core] Save session token in PersistentStorage. 2021-11-26 20:40:35 +01:00
Ferdinand Thiessen 6769e18ffa [api] Add PersistentStorage
Wrapper for LocalStorage on browsers and Storage plugin on capacitor
2021-11-26 20:40:35 +01:00
Ferdinand Thiessen 88dd96c937 [app] Use capacitor instead of cordova 2021-11-26 20:40:26 +01:00
Ferdinand Thiessen 4887bc261b [stores] Minor cleanup 2021-11-26 18:11:18 +01:00
Ferdinand Thiessen d516839ad4 [utils] Add placeholder to datetime utils 2021-11-26 11:14:15 +01:00
Ferdinand Thiessen c9df257bbf [core] Fix minor linting error 2021-11-26 01:39:09 +01:00
Tim Gröger 053fdae384 [api] revert user ssp 2021-11-25 12:32:50 +01:00
Ferdinand Thiessen 6fd3f045f8 [core] Fix notifications dismiss function 2021-11-24 15:54:42 +01:00
Ferdinand Thiessen 2d167ebbae [deps] yarn.lock is not needed as it is overwritten when adding plugins 2021-11-24 15:35:26 +01:00
Ferdinand Thiessen a8578e2803 [core] Notifications can now be dismissed without triggering a reject 2021-11-24 15:28:53 +01:00
Ferdinand Thiessen e4889ddac2 [api] Add datetime util for start-end ranges 2021-11-23 16:00:18 +01:00
Ferdinand Thiessen 664def40fc [docs] Add notes on cordova, different browser targets 2021-11-23 15:59:16 +01:00
Ferdinand Thiessen ade6d06eb6 [core] Minify requests 2021-11-22 22:59:50 +01:00
Tim Gröger 53f053a294 [api] add filter for get_users 2021-11-22 15:08:28 +01:00
Tim Gröger cc29893e04 Merge remote-tracking branch 'origin/develop' into develop 2021-11-21 19:43:49 +01:00
Ferdinand Thiessen 42800a9d99 [deps] Update dependencies, node 12 is close to unmaintained 2021-11-21 15:31:02 +01:00
Ferdinand Thiessen f4650ffdeb [api] update dependencies + new release 2021-11-21 15:11:25 +01:00
Ferdinand Thiessen 1158525abb [api] Add clone function to utils 2021-11-21 15:08:58 +01:00
Ferdinand Thiessen 04e3c57397 [clean] Replace unneeded computed with initialized ref 2021-11-21 11:19:08 +01:00
Tim Gröger 2fc411d51d [UI] Show menu drawer if on desktop 2021-11-21 11:13:51 +01:00
Tim Gröger 6061b37887 [users] delete avatar 2021-11-20 22:59:51 +01:00
Tim Gröger efde9a2ee7 [cleanup] remove logs 2021-11-19 23:12:30 +01:00
Tim Gröger e96d15bc66 [chore] show menu drawer auto if on desktop 2021-11-19 23:10:56 +01:00
Tim Gröger dad88ec766 [api][fix] fixed setting roles 2021-11-19 23:10:28 +01:00
Ferdinand Thiessen 3e091fd02b [plugins] Fix bug where not all plugins are loaded 2021-11-19 14:06:29 +01:00
Ferdinand Thiessen fca79c36ef [api] Pass Date objects to IsoDateInput validators instead of string. 2021-11-16 23:57:57 +01:00
Ferdinand Thiessen 34fcdbdb7f [deps] Match dependencies with API 2021-11-16 21:40:56 +01:00
Ferdinand Thiessen d84796b09d [api] Fix dependencies and release new version 2021-11-16 21:36:42 +01:00
Ferdinand Thiessen 62af4f5026 [cleaup] No need for computed if directly derefed 2021-11-16 14:23:18 +01:00
Tim Gröger 36d6fdfb94 [fix] return correct permissions 2021-11-16 11:20:11 +01:00
Ferdinand Thiessen a30da50b1d [deps] FIX require correct version of types package 2021-11-12 10:31:01 +01:00
Ferdinand Thiessen 53951daa25 [notification] Ignore notifications for missing plugins, identify plugins by ID rather then name. 2021-11-11 15:27:57 +01:00
Ferdinand Thiessen 59920e23a5 [cleanup] Minor style cleanup + use MDIv6 2021-11-11 15:26:57 +01:00
Ferdinand Thiessen 5952c9b7f2 [deps] Update dependencies, Quasar is now more stable 2021-11-11 11:53:11 +01:00
Ferdinand Thiessen 73f50e9f4f [cleanup] Fix some warnings of the vue eslint plugin 2021-11-11 11:52:20 +01:00
Ferdinand Thiessen dfb924bb3f [API] Update dependencies, fix error checking
Typescript now defaults to unknown instead of any for Errors (catch).
Implemented helper function to check AxiosErros.
2021-11-11 11:35:11 +01:00
Ferdinand Thiessen bc9dba1c7b [cleanup] Fix some minor formatting 2021-11-11 11:11:06 +01:00
Ferdinand Thiessen 1132bfd129 [deps] Update to rc pinia and fix TypeScript version for ESLint 2021-08-30 14:21:53 +02:00
Ferdinand Thiessen fe1fae10f5 [plugins]: Allow ordering menu links, fixes #10 2021-07-29 18:26:38 +02:00
Ferdinand Thiessen 1c36399ac0 chore(config): Enable HMR for plugins 2021-07-02 15:18:18 +02:00
Tim Gröger a38954cf70 Merge branch 'develop' of flaschengeist.dev:Flaschengeist/flaschengeist-frontend into develop 2021-06-29 09:37:48 +02:00
Ferdinand Thiessen 35d8433e23 [chore] Quasar is out of beta! (And pinia out of alpha) 2021-06-26 17:27:31 +02:00
groegert 731cc20a06 [core] removed some prettier lines
fixes #9
2021-05-27 12:53:18 +02:00
groegert c38032085f [core] added computation in EssentialExpansionLink
fixes #9
2021-05-27 12:35:25 +02:00
Ferdinand Thiessen 22f47bd34e [docs][core] Move devel docs to wiki, use correct api package 2021-05-26 18:49:36 +02:00
Tim Gröger 16fd9201ae Merge pull request 'Seperated plugin and api types into subprojects' (#2) from seperate_plugins into develop
Reviewed-on: #2
2021-05-26 12:12:44 +00:00
Ferdinand Thiessen 873fee3301 [docs] Updated readme on how to install plugins 2021-05-25 22:09:26 +02:00
Ferdinand Thiessen 1802081ad2 Merge branch 'develop' into seperate_plugins 2021-05-25 22:01:13 +02:00
Ferdinand Thiessen 631e78acb3 [core] minor cleanup 2021-05-25 21:55:22 +02:00
Ferdinand Thiessen d422380adc [core] Fixed menu link title generation 2021-05-25 21:54:49 +02:00
Ferdinand Thiessen 0c279289b2 [test] enable all plugins 2021-05-25 21:54:09 +02:00
Ferdinand Thiessen 979eab05af [api] Fixed session loading 2021-05-25 21:52:26 +02:00
Ferdinand Thiessen fd918f5bb7 [core] Added vendor (always) required plugins 2021-05-25 16:27:10 +02:00
Ferdinand Thiessen 068dbdcc7b [docs] README section about plugin development 2021-05-25 16:26:41 +02:00
Ferdinand Thiessen 8c9db67b95 [core] Seperated all plugins from the main source tree 2021-05-25 16:13:15 +02:00
groegert 86bad83e53 [core] added computation when accessing promised store in EssentialExpansionLink
fixes #9
2021-05-25 12:46:54 +02:00
groegert 625ac55b0a [core] prevent recursive logouts on htto-401
fixes #7
2021-05-24 19:30:28 +02:00
groegert 9940589d1a [core] prevent recursive logouts on htto-401
fixes #7
2021-05-24 19:21:49 +02:00
groegert f2b7f3a3b4 [core] call api.delete(`auth/${token}`) within a valid session
fixes: #7
2021-05-21 10:17:56 +02:00
Ferdinand Thiessen f9c9f6efbe [core] Seperated plugin and api types into subproject 2021-05-21 01:42:41 +02:00
Ferdinand Thiessen 0873b2da22 [core][plugin] Fixed compatibility with Pinia
* API BREAK! Menu link title now must be a string or a function return a string
* Pinia unwraps Refs so we had to get rid of the ComputedRef
* Use current typescript
2021-05-20 20:31:37 +02:00
groegert 734a3e51c9 [core] added some hints to ease the initial development setup 2021-05-20 16:27:40 +00:00
Ferdinand Thiessen cf1a5cc922 [core][deps] Updated deps and fix typescript errors 2021-05-19 15:37:50 +02:00
Ferdinand Thiessen 6e503ed38f [deps] Upgrade dependencies for webpack 5 2021-04-28 15:28:05 +02:00
Ferdinand Thiessen 4cbff6b077 [deps]: Update quasar, now uses webpack5 (Node14+ support!) 2021-04-27 15:16:58 +02:00
Ferdinand Thiessen 2b42dad617 [deps] Updated dependencies, dropped calendar submodule in favor of alpha release 2021-04-22 23:16:00 +02:00
Ferdinand Thiessen 8b6c400689 pricelist: Fixed wrong default value for modelValue 2021-04-22 23:14:11 +02:00
Tim Gröger 5174e7fea3 [core] some style loginpage 2021-04-19 10:12:18 +02:00
Tim Gröger 95867428a8 [core] draggable shortcutlinks 2021-04-18 23:43:16 +02:00
Tim Gröger 3c7d711f59 [core] dynamic shortcutlinks 2021-04-18 23:26:54 +02:00
Tim Gröger 81c5b10101 [core] change style of menulinks in drawer 2021-04-18 22:33:32 +02:00
Tim Gröger a8a6cd8814 Merge branch 'feature/pricelist' into develop 2021-04-17 18:32:06 +02:00
Tim Gröger 18d0098bb3 [pricelist][#6] draggable change of order of pricelist columns 2021-04-17 18:26:21 +02:00
Tim Gröger be347225c4 [pricelist] some more style 2021-04-17 14:44:25 +02:00
Tim Gröger e2b46d96b9 [pricelist] clean style tags 2021-04-17 14:17:42 +02:00
Tim Gröger 6e74105f38 [pricelist] clean styele extraIngredients 2021-04-17 13:58:27 +02:00
Tim Gröger ae5212bbfc [pricelist] clean style drinkType 2021-04-17 13:40:03 +02:00
Tim Gröger 2a11964c4b [pricelist][electron] add electron 2021-04-17 12:36:19 +02:00
Tim Gröger e0bf8f77bf [pricelist][fix] real fix for loading pictures in cordova 2021-04-16 17:19:34 +02:00
Tim Gröger c76da59290 [pricelist][fix] cordova loading pictures 2021-04-16 16:50:58 +02:00
Tim Gröger 52ae8fce29 [pricelist] add fullscreen for pricelist 2021-04-15 23:07:03 +02:00
Tim Gröger f7a6f3fbe1 [pricelist] add useroption for pricelist_view 2021-04-15 22:54:40 +02:00
Tim Gröger 8ee5c891a5 [pricelist] add pricelist as list 2021-04-15 22:04:33 +02:00
Tim Gröger 479dfd658a [pricelist] add some more permissions 2021-04-15 15:40:40 +02:00
Tim Gröger 97dcc8e602 Merge remote-tracking branch 'origin/develop' into feature/pricelist 2021-04-15 15:36:35 +02:00
Tim Gröger a376fd7cc2 [pricelist] fix update volume when ingredients changed 2021-04-15 15:35:08 +02:00
Tim Gröger 8b21ccf978 [pricelist] add permissions 2021-04-14 22:42:09 +02:00
Tim Gröger cee6eda585 [pricelist] delete visibleColumns 2021-04-14 20:11:44 +02:00
Tim Gröger dc948e6f11 [pricelist][prettier] some style up 2021-04-14 19:44:53 +02:00
Tim Gröger 86bb722623 [pricelist] no change of cost_per_volume if volumes has ingredients 2021-04-14 19:43:32 +02:00
Tim Gröger 0df2677b1b [pricelist](issue #4) add warning when price is less then min_price 2021-04-14 19:39:28 +02:00
Tim Gröger 43397fe3a7 [pricelist] dont show 'add ingredients' if cost_per_volume exists 2021-04-14 19:13:47 +02:00
Tim Gröger 851c5a0588 [pricelist] sync min_prices if changing 2021-04-14 18:53:17 +02:00
Tim Gröger 0626cf993f [pricelist] cleanup some code 2021-04-13 16:20:32 +02:00
Tim Gröger 9b0679278c [pricelist][cocktailbuilder] fix new ingredient component 2021-04-12 06:45:18 +02:00
Tim Gröger b40b1064e7 [pricelist][calculation] can add new drink 2021-04-11 23:11:43 +02:00
Tim Gröger 06256f651a [pricelist][calculation] modify picture 2021-04-11 22:08:59 +02:00
Tim Gröger e90dc4c306 [pricelist][calculation] stable version of new view 2021-04-11 16:29:11 +02:00
Tim Gröger 8c321fb8ca [pricelist] modify ingredients without saving 2021-04-10 23:05:03 +02:00
Tim Gröger 459a9fea08 [pricelist] fix copy object of state 2021-04-10 00:14:39 +02:00
Tim Gröger 7a3a151688 [pricelist] cleanup some code 2021-04-09 23:49:49 +02:00
Tim Gröger 1c64dbbaf6 [pricelist][calculation] add edit-button 2021-04-06 22:45:33 +02:00
Ferdinand Thiessen 4e340e5bea [core] Updates dependencies 2021-04-06 16:04:17 +02:00
Tim Gröger 89cd109587 [pricelist][calculation] fixed overloade 2021-04-04 23:20:53 +02:00
Ferdinand Thiessen 010ad6e107 [notifications] Fixed icon names 2021-04-04 22:55:11 +02:00
Ferdinand Thiessen 6f053d849a [utils] IsoDateTime, do not set time if used as date input 2021-04-04 21:38:27 +02:00
Tim Gröger f5b370e743 [pricelist][calculation] update grid view 2021-04-03 22:39:06 +02:00
Tim Gröger 741216ac3e Merge remote-tracking branch 'origin/develop' into feature/pricelist 2021-04-03 20:51:41 +02:00
Ferdinand Thiessen 6a6afcb2d4 [users] Design: Scroll area of roles should look like input elements 2021-04-03 14:42:09 +02:00
Ferdinand Thiessen ee3cb0ba40 [style] QPage should be the first element, otherwise design is broken 2021-04-03 14:41:30 +02:00
Ferdinand Thiessen ecb0649594 [layout] Fixed some more icons 2021-04-03 13:24:19 +02:00
Ferdinand Thiessen 66dd33dc25 [core] Revert usage of material icons and use mdi-v5
* Fixed usage of old material-icons
* Added icons section to readme
2021-04-02 22:31:07 +02:00
Tim Gröger 284652b002 [pricelist][calculation] start add grid view 2021-04-02 22:30:06 +02:00
Tim Gröger 736ea04b4a [pricelist][search] add help 2021-04-02 19:32:08 +02:00
Tim Gröger 5d1df48b9a [pricelist][fix] fixed issues from merge 3eea079871 2021-04-02 14:06:03 +02:00
Ferdinand Thiessen 7fb689b31f [core] Added license and fixed links in package.json 2021-04-02 04:41:49 +02:00
Tim Gröger 7289a1724d [pricelist][tags] add tags 2021-04-01 22:39:54 +02:00
Ferdinand Thiessen 927a5214b1 [deps] Update dependencies 2021-04-01 20:54:32 +02:00
Ferdinand Thiessen 679d98a2af [users] Fixed avatar on widget 2021-04-01 19:40:07 +02:00
Ferdinand Thiessen f787e314ef [notifications] Load when page gets loaded 2021-04-01 19:39:47 +02:00
Tim Gröger 909275727a [pricelist][chore] minor cleanup 2021-04-01 11:15:21 +02:00
Ferdinand Thiessen fc35e2ecec [user] Fixed avatar upload 2021-04-01 03:16:56 +02:00
Ferdinand Thiessen 7d914d065e [user] Fixed user settings and widget 2021-03-31 21:14:17 +02:00
Ferdinand Thiessen 48d6792fa0 [users][vue3] Fixed managing roles 2021-03-31 20:24:01 +02:00
Ferdinand Thiessen c61b5fcc0c [core] Allow users to see passwords if wished 2021-03-31 17:22:55 +02:00
Tim Gröger 8ecbddc6ca Merge branch 'develop' into feature/pricelist 2021-03-31 15:21:35 +02:00
Tim Gröger f25651a71e [pricelist][receipt] change to table with grid, add search 2021-03-31 15:17:43 +02:00
Ferdinand Thiessen 1b1888d4fd [notification] Disable native is not available 2021-03-31 14:25:58 +02:00
Tim Gröger fb14df0c43 [pricelist] add search 2021-03-31 14:18:55 +02:00
Tim Gröger 6cdc143aa9 [pricelist] some styling on pricelist 2021-03-30 22:32:18 +02:00
Tim Gröger 7089ee4d62 [pricelist][receipt] some more styling 2021-03-30 22:16:12 +02:00
Tim Gröger aadfca2d31 Merge branch 'develop' into feature/pricelist 2021-03-30 20:43:54 +02:00
Ferdinand Thiessen 6891a3ffba [notifications] Make text clickable if link is present 2021-03-30 15:48:15 +02:00
Tim Gröger a7d32d6f7c [pricelist][styling] style up receipts 2021-03-30 15:40:02 +02:00
Ferdinand Thiessen b7aeea0a23 [notifications] Fixed some warnings about inject outside setup() 2021-03-30 15:08:12 +02:00
Tim Gröger 1b478d7680 Merge remote-tracking branch 'origin/develop' into feature/pricelist 2021-03-30 09:59:27 +02:00
Ferdinand Thiessen e564901d2e [core] Remove last vuex references
* src/stores can not be used as quasar thinks it contains vuex
2021-03-30 00:28:18 +02:00
Tim Gröger d185b84823 [pricelist] receipt as list, fixed some merge issues 2021-03-29 22:35:54 +02:00
Tim Gröger 0d044b505a [pricelist] fix some merge issues 2021-03-29 21:29:04 +02:00
Tim Gröger 3eea079871 Merge remote-tracking branch 'origin/develop' into feature/pricelist 2021-03-29 20:22:50 +02:00
Tim Gröger 30e101c364 [pricelist] add Receipts 2021-03-29 12:50:50 +02:00
Ferdinand Thiessen 534d5e3034 [users] Require all needed backend plugins 2021-03-29 07:36:02 +02:00
Ferdinand Thiessen 5731fc9d6d [events] Fixed permissions names 2021-03-29 07:35:37 +02:00
Ferdinand Thiessen 852b1dad03 [notifications] Implemented 2021-03-29 07:35:23 +02:00
Ferdinand Thiessen c362843c8e [pricelist] Same cleanup as in backend 2021-03-28 23:45:35 +02:00
Tim Gröger cf0f453b7c [pricelist][reciepts] first try for reciepts 2021-03-28 20:34:06 +02:00
Tim Gröger 24aec1a98c [pricelist] add Cocktailbuilder 2021-03-28 19:53:04 +02:00
Tim Gröger 8eecb70df0 [pricelist] add public (outer) pricelist withoud login 2021-03-28 16:43:30 +02:00
Tim Gröger 59d6023462 [pricelist] add picture and visiblity for user pricelist 2021-03-28 13:40:41 +02:00
Tim Gröger f58e0c382c [pricelist][fix] new price will be shown after adding it 2021-03-28 13:08:41 +02:00
Tim Gröger 3d20292898 [pricelist][picture] update picture if changed, add no-image.svg 2021-03-28 11:34:36 +02:00
Tim Gröger 4bd2d24e9e [logo] add more new logo for android and ios (cordova) 2021-03-28 08:43:16 +02:00
Ferdinand Thiessen e9333c4af4 [events] Fixed tables for Event and JobTypes 2021-03-28 03:11:09 +02:00
Ferdinand Thiessen 718c6eaf9d [deps] Upgrade quasar deps 2021-03-28 03:08:32 +02:00
Tim Gröger aa82b9029a Merge branch 'develop' into feature/pricelist 2021-03-27 22:17:07 +01:00
Tim Gröger 5e2a60ead4 [logo] update logo 2021-03-27 22:16:49 +01:00
Tim Gröger d42c6dcce1 [pricelist] delete drink pictures 2021-03-25 23:14:49 +01:00
Ferdinand Thiessen 8c6036c686 [events] Edit events in calendar (QDialog) 2021-03-25 14:11:54 +01:00
Ferdinand Thiessen f28e597d53 [pricelist] Fixed store issue 2021-03-24 23:14:06 +01:00
Ferdinand Thiessen 61ed9e70e2 [events] Remove invalid ref 2021-03-24 21:50:05 +01:00
Ferdinand Thiessen 27ad9984d4 [core] Minor cleanup / fixed typo 2021-03-24 20:49:54 +01:00
Ferdinand Thiessen fb2febdadd [events] Allow editing events and removing templates 2021-03-24 20:49:35 +01:00
Ferdinand Thiessen b15ff0f5e3 [events] Sync route names 2021-03-24 20:48:45 +01:00
Ferdinand Thiessen 6da4f67d68 [events] Hide edit on old events. Update deps. 2021-03-24 18:15:17 +01:00
Ferdinand Thiessen 6dc70d12f6 [pricelist] Fixed warnings 2021-03-24 17:28:41 +01:00
Ferdinand Thiessen 87139077da [plugin] Error message if loading failed 2021-03-24 17:18:10 +01:00
Tim Gröger 827fb1aadd [pricelist] save and load pictures 2021-03-22 23:18:22 +01:00
Tim Gröger e0046aa7d2 Merge remote-tracking branch 'origin/next' into next 2021-03-22 13:00:22 +01:00
Ferdinand Thiessen f2610b8e84 [events]: Added removal of events + minor optical improvement 2021-03-22 03:31:14 +01:00
Ferdinand Thiessen 851ce3aa8b [plugins] Cleanup, documentation and refactoring of plugin system 2021-03-22 03:26:12 +01:00
Tim Gröger 6097e510c5 [pricelist] simple pricelist 2021-03-21 23:25:22 +01:00
Tim Gröger 14206d9117 [pricelist] improved add drink 2021-03-21 23:02:25 +01:00
Tim Gröger cd937f111c [pricelist][vue3] now it works 2021-03-21 22:07:12 +01:00
Ferdinand Thiessen 36bbc2dbf1 [submodule] Fixed calendar 2021-03-21 19:24:21 +01:00
Ferdinand Thiessen 84fe321ecc [deps] Drop unneeded dependencies 2021-03-21 16:26:40 +01:00
Ferdinand Thiessen ade1c984c6 [submodule]: New calendar version 2021-03-21 16:13:43 +01:00
Ferdinand Thiessen 218e41e94a [events] Fixed minor optical issues 2021-03-21 16:00:19 +01:00
Ferdinand Thiessen 77bb463e5e [schedule][events] Improved plugin
* Allow creating recurring events
* Fixed AgendaView
2021-03-21 00:58:31 +01:00
Ferdinand Thiessen 17460a8543 [pricelist] Fixed warnings 2021-03-20 22:16:37 +01:00
Ferdinand Thiessen c5a34ce63f [pricelist] Fixed build errors 2021-03-20 18:05:00 +01:00
Ferdinand Thiessen 0b255c481a Sync, improved IsoDateInput, composeable
* Synchronized with backend definitions
* Improved IsoDateInput to support clear (undefined model)
* Split validators to reuse them instead of redefine everywhere
2021-03-20 17:11:51 +01:00
Ferdinand Thiessen b8531ad816 [API] Fixed logout loop 2021-03-20 17:07:42 +01:00
Tim Gröger 73a5de021d [pricelist][break] some cleanup code. update not work 2021-03-20 14:59:55 +01:00
Tim Gröger c3e3a272dc Merge remote-tracking branch 'origin/next' into next 2021-03-20 11:34:40 +01:00
Ferdinand Thiessen 7e7f9c943d [schedule] Improved creating new events 2021-03-20 00:59:44 +01:00
Ferdinand Thiessen a4ce273bb1 [plugin] Fixed setting permission if non are set 2021-03-19 21:34:49 +01:00
Ferdinand Thiessen 575090552f [schedule] Improved calendar view
* Load events on next and prev
* Resize if display is not wide enough
* Fixed permissions
2021-03-19 21:34:28 +01:00
Tim Gröger e2d2a5cf9d [pricelist][pinia] can load data und modify some data 2021-03-19 20:41:21 +01:00
Ferdinand Thiessen 1316c47706 [schedule][quasar2] Fixed overview and eventslot 2021-03-19 18:33:57 +01:00
Ferdinand Thiessen 42b43aa56c [Backend] Exported current API 2021-03-19 18:33:04 +01:00
Ferdinand Thiessen 9469cda4b0 [Dashboard] Fixed display of widgets 2021-03-19 17:36:34 +01:00
Ferdinand Thiessen 51fcc6f9be [Vue3][pinia] Make pricelist compile with vue3, needs more work! 2021-03-19 16:33:27 +01:00
Ferdinand Thiessen 26148f8827 Fixed Agenda 2021-03-19 15:53:41 +01:00
Ferdinand Thiessen dbcd1b2c5f Fixed dependencies, calendar and submodule 2021-03-19 15:53:25 +01:00
Tim Gröger 9b273c2501 [QuasarV2][fix] table-data 2021-03-19 15:48:45 +01:00
Ferdinand Thiessen ccf13eae9c Fixed some recursion issues 2021-03-19 15:48:44 +01:00
Ferdinand Thiessen f18897caf4 [pinia] Remove last vuex parts 2021-03-19 15:48:03 +01:00
Ferdinand Thiessen 98375f81be [pinia] use pinia for pricelist 2021-03-19 15:47:45 +01:00
Ferdinand Thiessen 76978e8883 [Pinia] Use pinia for schedule 2021-03-19 15:47:15 +01:00
Ferdinand Thiessen e347129ba9 [pinia] Fixed handling of multiple calls to logout
* Better handling if we are logged out
2021-03-19 15:46:01 +01:00
Ferdinand Thiessen fd45a46c01 [pinia] Implemented and migrated balance
* Fixed revert of transaction if Conflic occures
2021-03-19 15:44:59 +01:00
Ferdinand Thiessen 4a7ed50281 [Vue3] Fixed usage of plugins 2021-03-19 15:43:36 +01:00
Ferdinand Thiessen bc3c15e3bc Fixed login and login-guard, redirect when offline 2021-03-19 15:43:13 +01:00
Ferdinand Thiessen 0922d468d9 [pinia] Some work on balance store 2021-03-19 15:42:10 +01:00
Ferdinand Thiessen 544d58889b Fixed Sessions 2021-03-19 15:42:10 +01:00
Ferdinand Thiessen 4b198b6472 [pinia] Added Pinia, replaced vuex on user and session 2021-03-19 15:42:10 +01:00
Ferdinand Thiessen 5153f074b5 [Vue3] Fixed pricelist 2021-03-19 15:41:03 +01:00
Ferdinand Thiessen 62aa627f0c Update Quasar deps 2021-03-19 15:34:46 +01:00
Ferdinand Thiessen c60f171285 [Quasar2] Submodule for QCalendar 2021-03-19 15:33:43 +01:00
Ferdinand Thiessen e4394db93b Testing with custom quasar calendar 2021-03-19 15:33:31 +01:00
Ferdinand Thiessen 78427aa5d2 [quasar2] Updated depencies to fix TS Error.
* Fixed some merge issues
  * Fixed minor issues
  * Fixed some warnings
2021-03-19 15:32:22 +01:00
Ferdinand Thiessen d54b398c14 Fixed redirect when offline 2021-03-19 15:31:04 +01:00
Ferdinand Thiessen 50f1f028eb Allow empty value in UserSelector 2021-03-19 15:31:04 +01:00
Ferdinand Thiessen b2d54a046f [Vue3] Fixed usage of plugins 2021-03-19 15:31:03 +01:00
Ferdinand Thiessen 9967296698 Fixed IsoDateInput 2021-03-19 15:30:20 +01:00
Ferdinand Thiessen 17ffd19c5b [Vue3] Fixed loading of current plugins 2021-03-19 15:29:10 +01:00
Ferdinand Thiessen d3d8c1e5f2 [Vue3] Fixed usage of value vs modelValue 2021-03-19 15:28:55 +01:00
Ferdinand Thiessen cb68f9ff7e [Vue3][Quasar2] Update dependencies
* Update quasar to beta version
2021-03-19 15:27:49 +01:00
Ferdinand Thiessen 34312cca96 [Vue3] Increase warning level for better code quality and fixed issues found. 2021-03-19 15:26:58 +01:00
Ferdinand Thiessen 68fa8fa1a8 [Vue3] Fixed some more error and merge issues 2021-03-19 15:25:22 +01:00
Tim Gröger ee2a6a71eb [Vue3] can now compile 2021-03-19 15:22:37 +01:00
Tim Gröger 8f8da5ffd1 [Vue3] fixed some errors. first steps to reconstruct storage 2021-03-19 15:22:36 +01:00
Ferdinand Thiessen 152b86fb4f [Vue3] Fixed some eslint warnings on template part of vue files 2021-03-19 15:21:21 +01:00
Ferdinand Thiessen 6be07b1001 [Vue3] Fixed Components with components options definied. Fixed config files. 2021-03-19 15:19:52 +01:00
Ferdinand Thiessen ba0696c3c0 Bundled prettier config in package.json 2021-03-19 15:19:52 +01:00
Ferdinand Thiessen 6e50a510eb [Vue3] More fixes in setup and MainUserSettings 2021-03-19 15:19:52 +01:00
Ferdinand Thiessen 897c98c53a [Vue3] Fixed users plugin and some basic stuff, still broken. 2021-03-19 15:19:50 +01:00
Ferdinand Thiessen 117d8256be [Vue3][Quasar2] Migrated some more files. 2021-03-19 15:16:29 +01:00
Ferdinand Thiessen b7db5ea3a6 [Vue3][Quasar2] Fixed boot files and UserSessionStore 2021-03-19 15:14:36 +01:00
Ferdinand Thiessen d147e538d1 [Vue3][Quasar2] Fixed Store and Router, breaking changes in both. 2021-03-19 15:12:37 +01:00
Ferdinand Thiessen 0efe445864 [Vue3][Quasar2] Removed old composition-api, now included in Vue 2021-03-19 15:07:03 +01:00
Ferdinand Thiessen 074fae4da3 [Vue3][Quasar2] Switch to new framework versions 2021-03-19 15:06:58 +01:00
Tim Gröger fb8fc09e8d [pricelist] persistent save visible_columns 2021-03-18 22:33:25 +01:00
Tim Gröger b141c2e5c4 [pricelist] sorting 2021-03-18 21:10:54 +01:00
Tim Gröger 73f16d6cbb [prettier] format with prettier, fixed some pricelist issues 2021-03-18 17:23:57 +01:00
Ferdinand Thiessen caa09a3c2c Added correct definitions file 2021-03-18 13:08:43 +01:00
Tim Gröger 20191be5dc [pricelist] modify, delete and add for ExtraIngredients 2021-03-17 22:49:23 +01:00
Tim Gröger 57f21936c0 [pricelist] finish drinks, can add, modify and delete 2021-03-17 21:36:26 +01:00
Tim Gröger c272c9e4a5 [pricelist] add, modify, delete ingredients of volume 2021-03-16 23:28:38 +01:00
Tim Gröger 7e01ffc507 [pricelist] fixed some computed values, new interfaces 2021-03-16 18:10:37 +01:00
Tim Gröger e4851bd178 [pricelist] fixed prices. first steps for volume 2021-03-15 23:52:40 +01:00
Tim Gröger f6951bdf0b [pricelist] with backend, with errors
price can be add and deleted
2021-03-15 19:57:42 +01:00
Tim Gröger 724ae66dd7 [Pricelist] break no backend new view 2021-03-14 20:37:41 +01:00
Tim Gröger c0d57c6a71 [pricelist] add first commit for plugin (with errors) 2021-02-13 14:11:25 +01:00
Ferdinand Thiessen cd74612e6d Login compat. with backend 2021-02-10 18:09:37 +01:00
Dominik 567e994b71 Fixed minor issues 2021-02-07 20:35:08 +01:00
Dominik af79a30497 Minor Styling Changes to Eventslots and enrollment functionality 2021-02-07 20:16:21 +01:00
Ferdinand Thiessen 7fe59f67f5 Updated definitions for API 2021-02-07 14:42:27 +01:00
Dominik cc47e21a31 Finished Basic Calendar Week Functionality 2021-02-06 00:07:58 +01:00
Dominik e03b2f20ff Merge branch 'next' of groeger-clan.duckdns.org:newgeruecht-vue into next 2021-02-03 20:07:49 +01:00
Ferdinand Thiessen 3283d8a862 improved balance 2021-02-03 13:26:39 +01:00
Ferdinand Thiessen cf2fcc0664 Enable our awsome SVG favicon 2021-02-03 11:13:27 +01:00
Dominik 8dbd6cc5eb Merge branch 'next' of groeger-clan.duckdns.org:newgeruecht-vue into next 2021-02-01 20:18:41 +01:00
Tim Gröger 2de1eaf29e [icon] change favicon 2021-01-30 11:33:03 +01:00
Tim Gröger 44b50edf82 [balance] fixed transactions exists only once in store
change icons in circular progress
2021-01-30 11:23:18 +01:00
Dominik 0a2be0e5ff Merge branch 'next' of groeger-clan.duckdns.org:newgeruecht-vue into next 2021-01-30 01:30:35 +01:00
Dominik e2b4550411 added quasar calendar 2021-01-30 01:30:29 +01:00
Dominik ac30659aeb Changed Eventtype to optional 2021-01-30 01:30:02 +01:00
Ferdinand Thiessen 008d40b56a Balance Admin: Load all balances 2021-01-29 23:29:38 +01:00
Ferdinand Thiessen c9d8365def Balance Overview: Fixed issue if users are not loaded 2021-01-29 23:29:20 +01:00
Ferdinand Thiessen bda5602e9f Only safe transaction if user owns it 2021-01-29 23:03:29 +01:00
Tim Gröger b4c080fec6 [balance] fixed some typescript errors 2021-01-29 23:00:46 +01:00
Tim Gröger 006e7e4048 [balance] fixed some style 2021-01-29 22:57:10 +01:00
Tim Gröger 059142c506 Merge branch 'next' of groeger-clan.duckdns.org:newgeruecht-vue into next 2021-01-29 22:53:20 +01:00
Tim Gröger 6e406c9b2c fixed bug in IsoDateInput.vue (Props must be function for Object/Array) 2021-01-29 21:59:06 +01:00
Ferdinand Thiessen 3ace1e43da Balance: Filter shown transactions in Overview 2021-01-29 21:05:03 +01:00
Ferdinand Thiessen 2630da3ca4 Balance: Fixed paginated transactions 2021-01-29 20:11:13 +01:00
Ferdinand Thiessen 99d3acaef5 Prettier, committed to prevent conflicts 2021-01-29 20:10:53 +01:00
Tim Gröger 284533742d add new logo to cordova 2021-01-29 16:35:52 +01:00
Tim Gröger 9f53cb6cab New Logo 2021-01-29 16:24:43 +01:00
Tim Gröger feaeb3f4e4 fixed errors, persist save of server (cordova) 2021-01-29 14:06:25 +01:00
Tim Gröger 797f7dd67a add server settings for cordova 2021-01-29 11:51:56 +01:00
Ferdinand Thiessen d396940071 First try with an overview page for the balance 2021-01-29 04:38:20 +01:00
Ferdinand Thiessen 502c40329c Merged balance actions into one page 2021-01-29 04:37:56 +01:00
Ferdinand Thiessen 17e640892a Implemented load Transactions 2021-01-29 02:29:27 +01:00
Ferdinand Thiessen 4ff63b458d Move files 2021-01-29 02:24:48 +01:00
Ferdinand Thiessen 9734dc41a4 scuttlebutt: Hide debit button if user is missing the permission 2021-01-28 23:09:19 +01:00
Ferdinand Thiessen a8ad2f1da5 Code formatting with prettier 2021-01-28 23:08:34 +01:00
Dominik 1dc0603df3 Fixed IsoDateInput and Rulevalidation + CreateEvent Validation 2021-01-28 00:55:17 +01:00
Dominik 663d1d3e4d Merge branch 'next' of groeger-clan.duckdns.org:newgeruecht-vue into next 2021-01-27 22:39:21 +01:00
Dominik 0844b0997d Cleaned CreateEvent 2021-01-27 22:37:12 +01:00
Tim Gröger 69e68b92f9 add function to change session lifetime 2021-01-27 14:04:09 +01:00
Tim Gröger 693b6a11d3 prettier all files 2021-01-27 08:16:44 +01:00
Ferdinand Thiessen 91200f277c Display Balance as fixed 2021-01-27 02:47:19 +01:00
Ferdinand Thiessen a787abdbc0 Improved Transaction design and function 2021-01-27 02:41:11 +01:00
Ferdinand Thiessen e366a25838 Save transactions in store and allow setting and getting a single user from store 2021-01-27 02:41:11 +01:00
Dominik 45bf4aa223 Merge branch 'next' of groeger-clan.duckdns.org:newgeruecht-vue into next 2021-01-26 22:14:18 +01:00
Tim Gröger 51240dd98b fix Bug in IsoDateInput that you can write the date 2021-01-26 21:54:16 +01:00
Dominik aba0046c84 Merge branch 'next' of groeger-clan.duckdns.org:newgeruecht-vue into next 2021-01-26 20:49:33 +01:00
Dominik 1ccddb228d Minor changes to createEvent 2021-01-26 20:38:46 +01:00
Ferdinand Thiessen 61316dcd9f Updated Flaschengeist type definition file 2021-01-26 16:38:12 +01:00
Ferdinand Thiessen 887262ae5a Fixed some TS warnings 2021-01-26 16:37:45 +01:00
Ferdinand Thiessen 270df75fc8 Added missing component registration. 2021-01-25 23:43:18 +01:00
Ferdinand Thiessen 78857522ea Fixed wrong filename error 2021-01-25 23:33:48 +01:00
Dominik c05877fa46 Merge branch 'next' of groeger-clan.duckdns.org:newgeruecht-vue into next 2021-01-23 18:36:49 +01:00
Tim Gröger a861129e1b Add Time and Datetime support in IsoDateInput.vue
Default IsoDateInput give an Date.
You can set type to 'date', 'time', and 'datetime' to get date, time or datetime
2021-01-23 18:36:07 +01:00
Dominik d6261d8a0d Added Eventtype functionality 2021-01-23 16:06:44 +01:00
Dominik f5f9d2af61 Added Jobtype functionality 2021-01-23 14:40:35 +01:00
Ferdinand Thiessen 04237246fa Fixed shortcuts for adding balance 2021-01-22 00:17:03 +01:00
Ferdinand Thiessen ba485f87c5 Prettier 2021-01-21 21:07:49 +01:00
Tim Gröger 6e90075db3 fixed build erros (typescript errors) 2021-01-21 16:23:40 +01:00
Ferdinand Thiessen 5028d46900 Fixed balance widget 2021-01-21 15:32:13 +01:00
Ferdinand Thiessen 01143e08e8 Balance: Added Transfer and Admin view + more
* some work on reverting transactions.
* Added TODO comments on incomplete features
2021-01-21 14:24:46 +01:00
Ferdinand Thiessen 7748d2d8a3 UserSelector: Allow custom label 2021-01-20 15:45:13 +01:00
Ferdinand Thiessen 08c29c1cd6 Implemented password reset function 2021-01-18 16:05:05 +01:00
Dominik 5a97bfa413 Fixed Loading import 2020-12-19 22:13:52 +01:00
Ferdinand Thiessen eabc520762 Move non-component helper files to utils 2020-11-24 18:42:43 +01:00
Ferdinand Thiessen 17e203b5c9 Revert usage of flatRoutes, use an empty parent component instead 2020-11-24 18:35:37 +01:00
Ferdinand Thiessen d4795a549f Fixed interface for Backend, allow redirect mainLinks
* Fixed the Backend interface to match the returned data structure
* Allow empty parents in RouteConfig, use flatRoutes
2020-11-24 17:35:11 +01:00
Ferdinand Thiessen c05fc5d877 Use yarn and updated dependencies
* Default to use yarn instead of npm
* Fixed eslintignore (do not lint hidden files)
* Replaced deprecated eslint-loader with eslint-webpack-plugin
* Use current eslint (v7) and upgraded vue plugin
* Set node to 12, as 11 is not supported anymore (12 is the LTS version)
* TODO: Drop package-lock.json as soon as we do not use npm anymore
2020-11-24 17:34:12 +01:00
Ferdinand Thiessen 0b7c6feeb3 Fixed issue with avatar upload 2020-11-18 03:11:34 +01:00
Ferdinand Thiessen 7612ccde7b Definition from current backend 2020-11-18 02:49:19 +01:00
Ferdinand Thiessen bda58426e3 Fixed code style, sorry 2020-11-18 00:33:26 +01:00
Ferdinand Thiessen 4be0f56820 Fixed some vue errors and send less requests if (no need to fetch own edits) 2020-11-18 00:27:44 +01:00
Ferdinand Thiessen 06b259cd74 Fixed birthday is not a Date 2020-11-17 18:09:05 +01:00
Ferdinand Thiessen 4c9fb07f7d Show notification if unsupported filetype 2020-11-17 03:43:34 +01:00
Ferdinand Thiessen 82d88f50d0 Fixed some ts errors 2020-11-17 03:34:05 +01:00
Ferdinand Thiessen 5061d18956 Better handling of invalid credentials on login
* Notify on page as this is more appropriate
* Reset entered password
2020-11-16 14:17:26 +01:00
Ferdinand Thiessen 967458a51b Use same style for all notifications, better error message 2020-11-16 13:44:38 +01:00
Ferdinand Thiessen 1471f1a660 Limit avatar size and cleanup dependencies
* Limit avatar size to 200 KiB (really no body needs 4K resolution avatars
* Dropped cryptojs dependency as we do not use gravatar anymore
2020-11-16 13:36:55 +01:00
Ferdinand Thiessen 939dde3651 Implemented Avatar 2020-11-16 02:28:03 +01:00
Ferdinand Thiessen 01826fbc8b Show todays birthdays 2020-11-15 20:08:24 +01:00
Ferdinand Thiessen 9b19dc225b Allow setting the birthday 2020-11-15 19:47:05 +01:00
Ferdinand Thiessen c8708be39d Better offline detection (e.g. if database is offline) 2020-11-15 18:51:30 +01:00
Tim Gröger 97b60298ec Dynamische LoadingBar hinzugefügt
* mit setLoadingBoar, kann ein WatcherSource mitgeliefert werden, sodass eine LoadingBar erscheint.
* muss jeweils einmal! für alle loadings angewendet werden.
2020-11-15 14:18:28 +01:00
Tim Gröger 63b25bb3d6 [User] Rollen können umbenannt werden 2020-11-15 01:22:23 +01:00
Ferdinand Thiessen e4378af76e Retrieve users when using the UserSelector 2020-11-15 00:23:20 +01:00
Tim Gröger 9f2f632a67 Fix color of logo 2020-11-14 21:35:54 +01:00
Tim Gröger 9cdc041b13 [Notify] Fehlermeldung wenn etwas nicht ausgeführt werden konnte 2020-11-14 15:04:42 +01:00
Tim Gröger 306ae7648d [Login] Fehlermeldung bei falschen Credentials 2020-11-14 14:41:46 +01:00
Tim Gröger 60417f6585 [Plugin] Überprüfung der Abhängigkeiten geändert
* werden als promise außerhalb von loadplugins geprüft.
* TODO: version muss geprüft werden.
2020-11-14 10:58:21 +01:00
Tim Gröger d5e4571b73 [Plugin] Errorpage wenn Plugins nicht verfügbar sind. 2020-11-13 18:48:50 +01:00
Tim Gröger 5f7c515228 [Plugin] Notification, wenn Plugin im Backend nicht verfügbar.
Fixed Typo
2020-11-13 13:42:15 +01:00
Ferdinand Thiessen fde2682681 Simplyfied login guard, fixed exception in RoleSettings 2020-11-13 04:30:05 +01:00
Ferdinand Thiessen 19f91d2abf Allow multiple widgets and minor improvements
* Allow mulitple widgets for plugins, allow setting required permissions
* Split datetime formatter code for reuse
2020-11-13 04:02:25 +01:00
Tim Gröger 7b1a1c3656 [Admin] Fix einge Rollen
* rollen im store aufgeräumt
* clear-icon hinzugefügt
2020-11-13 00:04:56 +01:00
Tim Gröger 4ea0bce19d [Admin] neuer User kann hinzugefügt werden 2020-11-12 22:48:19 +01:00
Tim Gröger 338fbb97b3 [Admin] Designänderung der Adminpage 2020-11-12 14:28:02 +01:00
Tim Gröger 1ce02a67a9 [Login] Fix, Outseiten wieder öffnen
* man kann wieder auf seiten zugreifen, die garkeine berechtigungen benötigen.
2020-11-12 12:47:47 +01:00
Tim Gröger 70575c94c3 [About] Designänderung
* Es werden nur noch bei Developer Cards benutzt
* Logo wird geändert, wenn darkmode aktiv ist
* Pluginfarbe wird geändert, wenn darkmode aktiv ist
2020-11-12 12:07:40 +01:00
Ferdinand Thiessen b069361c1a Fixed issue where the login guard starts an endless loop 2020-11-12 01:55:43 +01:00
Ferdinand Thiessen e26dc6c3a9 Update current API description, added dateTime filter alias.
* Build only for iOS since 12.1 (IPhone 5s+) so babel uses more recent ECMAScript and filesizes get reduced
2020-11-11 23:53:44 +01:00
Ferdinand Thiessen 296245457d Improved Mi 11. Nov 13:22:28 CET 2020 and filter 2020-11-11 13:22:28 +01:00
Ferdinand Thiessen 390e0fc95b Fixed TS errors and warnings 2020-11-10 01:40:50 +01:00
Ferdinand Thiessen cfc46dddd3 Added Dashboard as start page
* Plugins can register widgets on the dashboard
* Added dummy widget for schedule and user ("greeting")
* Added simple widget for balance
2020-11-10 01:33:55 +01:00
Ferdinand Thiessen 31620f9681 Fixed typo that prevents permissions from being loaded, enable dark mode
if requested.
2020-11-09 04:35:07 +01:00
Ferdinand Thiessen 63e9de01e2 Fix issue where store is not cleared properly 2020-11-09 03:59:17 +01:00
Ferdinand Thiessen b479e3ad48 Some work on login process
* Forward already autheticated users
* If not auteticated forward back to login
* Clear current user if no valid session
2020-11-09 03:40:51 +01:00
Ferdinand Thiessen 8c1dffc003 Added RoleSettings, now users can edit and create roles 2020-11-09 03:39:31 +01:00
Ferdinand Thiessen e566a89860 Add warning if backend is offline (implements #416) 2020-11-06 10:52:51 +01:00
Ferdinand Thiessen 458cf81a91 Fixed order of date and time in datetime filter 2020-11-06 10:52:13 +01:00
Ferdinand Thiessen d4bc385833 Fixed users can set role in frontend without permission 2020-11-06 01:31:46 +01:00
Ferdinand Thiessen edf56c1094 Fixed permission issue with menue 2020-11-06 01:28:35 +01:00
Ferdinand Thiessen 8689e84d47 Reworked user and session store, added Admin function for user.
* Sync Login with backend
* Split Main into MainUserSettins and Settings
* Added AdminSetting to change other users, added UserSelector Component
for selecting users (can be reused for other stuff ;-) ).
* Split hasPermission into helper file for code reuse
2020-11-06 01:17:04 +01:00
Ferdinand Thiessen 5c11e02b2c Axios: Intercept 401 and logut (session expired or revoked) 2020-11-06 01:15:40 +01:00
Ferdinand Thiessen bdcf9668b7 Sync with backend 2020-11-06 01:12:03 +01:00
Ferdinand Thiessen 1d598b5787 Update dependencies 2020-11-06 01:09:03 +01:00
Ferdinand Thiessen 27b34e36e2 Added filter to format Date to string inside of templates 2020-11-05 03:55:44 +01:00
Ferdinand Thiessen 4061d84ace Use more logical seperation on user and session
* Seperated user and session more logical
* Fixed error with expired sessions
* Cache user only in SessionStore
* Use current backend responses
* Used prettier
2020-11-05 00:32:56 +01:00
Ferdinand Thiessen 245944b6a9 Sync: d.ts with backend and permissions from balance plugin 2020-11-02 17:39:05 +01:00
Ferdinand Thiessen 7b710f0bf4 Added current declaration file 2020-11-02 04:42:19 +01:00
Tim Gröger 5d1409b735 [Settings] Neue Icons für plattform 2020-10-31 22:37:28 +01:00
Tim Gröger 3247a5bb01 [Balance] Bereich zum Hinzufügen von Schulden
Es können auch schon schulden hinzugefügt werden.
2020-10-31 21:30:02 +01:00
Tim Gröger ef71481931 Fixed Typings 2020-10-31 19:33:05 +01:00
Tim Gröger a1f1be7fb6 [Balance] Plugin wird geladen 2020-10-31 17:33:40 +01:00
Tim Gröger 09c6a806c9 Fixed Permissions
Man kann nun pro Route mehrere permissions setzen.
2020-10-31 17:33:09 +01:00
Tim Gröger 555d2a871b Update Readme 2020-10-31 15:38:20 +01:00
Tim Gröger caedb5a9d2 Strukturänderung im Plugin
Plugins haben nun PluginRouteConfig für mainRoute und outRoute. Dabei werden die wirklichen routes, shortcuts und mainlinks daraus generiert.
2020-10-31 15:09:02 +01:00
Tim Gröger 22ca9b03a0 Merge remote-tracking branch 'origin/next' into next 2020-10-31 13:14:42 +01:00
Ferdinand Thiessen 45da05901b Fixed token Header 2020-10-31 02:39:10 +01:00
Ferdinand Thiessen c306f96bb8 By default the API should be on /api, change it locally for your needs 2020-10-30 17:53:13 +01:00
Tim Gröger c9a5b6d165 [User] Wenn curerntSession gelöscht wird, wird man automatisch ausgelogt
Wenn beim wechseln der Seite kein Token gefunden wird, wird ebenfalls automatisch ausgeloggt.
2020-10-30 13:27:33 +01:00
Tim Gröger cc27307835 [User] Nach updates des Users wird auch der User im frontend neu geladen 2020-10-30 12:08:33 +01:00
Tim Gröger fd71f08430 [Loading] Setze Standardeinstellungen 2020-10-30 09:57:39 +01:00
Tim Gröger 4c8f72603e Auto stash before merge of "next" and "origin/next" 2020-10-30 09:13:57 +01:00
Ferdinand Thiessen e3398c3fa5 Fixed last TypeScript issues from store 2020-10-29 01:39:06 +01:00
Ferdinand Thiessen 7c33a71c4d Fixed some TS issues related to user and session store 2020-10-29 00:19:39 +01:00
Ferdinand Thiessen 3f756437ee Fixed more TypeScript issues 2020-10-29 00:12:46 +01:00
Ferdinand Thiessen 5a4f6939d1 Fixed some more Typescript issues 2020-10-29 00:10:45 +01:00
Ferdinand Thiessen 9992ed6f2b Fixed some TS errors in user settings 2020-10-28 21:28:43 +01:00
Ferdinand Thiessen dc0107bcc9 Fixed some TS errors 2020-10-28 16:54:28 +01:00
Tim Gröger 789cf89603 Ausloggen jetzt möglich, Neuer Loading Spinner in 'Dark-Mode'
Beim ausloggen wird nun der localstorage geleert und ein delete an /auth/delete/<token> gesendet.
2020-10-28 12:55:20 +01:00
Tim Gröger 27d44b350f [User] Validation der Settingseingaben der User
Es werden die Eingaben der User bei den Einstellungen kontrolliert.
Es muss ein Passwort eingegeben werden, E-Mail muss eine E-Mail sein und die Felder dürfen nicht leer sein.
Es wurde ein Reset Button hinzugefügt und gemachte Eingaben zurückzusetzen
2020-10-28 12:19:09 +01:00
Tim Gröger 925982d700 User kann eigene Einstellungen updaten 2020-10-27 13:49:45 +01:00
Tim Gröger c6ef18b009 login.ts (check route-permission) geupdatet 2020-10-27 11:51:53 +01:00
Tim Gröger d097231dc1 Merge remote-tracking branch 'origin/next' into next 2020-10-22 10:21:12 +02:00
Ferdinand Thiessen 05fd255a51 Added backend interfaces 2020-10-21 17:39:04 +02:00
Ferdinand Thiessen e6da94ad0e Fixed displayname 2020-10-20 19:24:37 +02:00
Ferdinand Thiessen 2383e28cd8 Kompatibilität mit backend (addfb7c7c463de5f44733896b06cb5fb480bedf8) 2020-10-19 16:49:40 +02:00
Tim Gröger 4e5509fcde mainSettings hinzugefügt (readonly) 2020-10-19 14:36:07 +02:00
Tim Gröger cb9ede5b27 Fixed MergeConflicts from 52dc3057ad 2020-10-19 13:59:56 +02:00
Tim Gröger 52dc3057ad Merge remote-tracking branch 'origin/next' into next 2020-10-19 10:59:44 +02:00
Ferdinand Thiessen 3c8748f044 Offizielle Typehints, Anpassungen an aktuelles backend, kleine Fehler behoben.
* Typehints für das Backend hinzugefügt, autogeneriert mit run_flaschengeist.
* Kleinere Warnungen behoben (typing issues)
* Anpassungen an restruckturierungen des Backends
2020-10-19 01:45:06 +02:00
Ferdinand Thiessen ed41acfdd9 Remove unneeded files 2020-10-19 01:44:48 +02:00
Tim Gröger 1e64cc3f60 Title für MainLink aus dem Store
Funktion implementiert, damit Namen aus dem Store (getters) geladen werden können. Sobald im Title 'loadFromStore("<parameter>")' vorhanden ist, wird $store.dispatch(parameter) aufgerufen und als titel rausgegeben.
2020-10-17 12:56:25 +02:00
Tim Gröger 0cdfe7f11c Lädt UserStore aus LocalStorage
Sollte die seite neu geladen werden, wird aus dem LocalStorage der UserStorage geladen. Somit bleibt man angemeldet.
Axios angepasst, sodass nur noch die url eingegeben werden muss.
2020-10-16 22:37:37 +02:00
Tim Gröger 704f6fd3fe Loading hinzugefügt
Circular Progress kann nun mit Logo verwendet werden.
Beim Anmelden wird der Bildschirm beim Laden gesperrt.
2020-10-16 13:54:01 +02:00
Tim Gröger 2411fc86cd Löschen und Anzeigen von Sessions 2020-10-16 13:07:31 +02:00
Tim Gröger 644f225428 Merge branch 'transfer/next' into next 2020-10-16 09:38:14 +02:00
Tim Gröger 8409e09f19 login 2020-10-16 08:45:40 +02:00
Tim Gröger 1ad39f386e transfer to mac 2020-10-15 11:23:41 +02:00
Ferdinand Thiessen ef3fcc48a7 Merge branch 'next' of groeger-clan.duckdns.org:newgeruecht-vue into next 2020-10-15 03:40:29 +02:00
Ferdinand Thiessen 82d4b52e24 Cleanup + Login with plugin backend 2020-10-15 03:36:25 +02:00
Ferdinand Thiessen bea9f9f5dc Add baseURL to axios 2020-10-15 03:35:44 +02:00
Tim Gröger 01afa232c4 Zeigt jetzt auch die ShortCuts an.
About-Page wurde noch ein bisschen angepasst.
2020-10-14 22:41:50 +02:00
Tim Gröger 4324681b75 ShortCuts und Versionen hinzugefügt.
Code leicht aufgeräumt und verbessert.
2020-10-14 22:27:20 +02:00
Ferdinand Thiessen ee67f691d3 Aufräumen + Login Handler 2020-10-14 18:44:03 +02:00
Tim Gröger a23a17285b update invertiertes Logo 2020-10-14 17:13:03 +02:00
Tim Gröger 10c1b57c64 fixed bug: tag developer in home hatte kein 'key' 2020-10-14 15:00:13 +02:00
Tim Gröger 61a679dfb1 About-Page hinzugefügt
kwt
2020-10-13 23:13:42 +02:00
Tim Gröger c5799967af Plugins für Plugins möglich.
Man kann nun Plugins für Plugins schreiben. Dabei können die Routes vom ursprünglichen Plugin überschrieben werden.
Außerdem wird unterschieden zwischen required und optional (loadPlugins) Plugins.

routes werden nun kombiniert und die MainLinks ebenfalls.
2020-10-13 20:17:00 +02:00
Tim Gröger dddafef3a1 Fixed Error und Warnings 2020-10-13 11:27:27 +02:00
Tim Gröger 6d56d5847f Einfaches Plugin-Loading
Lädt alle Plugins aus dem Ordner src/plugins.
Bis jetzt werden nur die Routes hinzugefügt!
Es wird noch nicht mit dem Store gearbeitet.
2020-10-12 23:49:05 +02:00
Tim Gröger 2ee3cb0dbc Grobes Layout fertig
wurde Gefüllt mit Dummies für die Navigation.
Zeigt Loginpage und leitet einen weiter.
Design ist responsive.
2020-10-10 23:02:32 +02:00
Dominik 09f72a2893 grobes layout 2020-10-09 18:04:32 +02:00
Tim Gröger 4f64933555 new Project with Quasar 2020-10-02 09:13:14 +02:00
Tim Gröger f23be34a77 Merge branch 'feature/drinkList' into develop 2020-09-02 08:45:32 +02:00
Tim Gröger 0563740e9c Fehler im Verlauf der FreeDrinkList in der baransicht wurde behoben 2020-09-01 10:18:09 +02:00
Tim Gröger f212c5962c Bugfix: Beim updaten der FreeDrinkListHistory kommt kein Networkfehler 2020-09-01 10:04:21 +02:00
Tim Gröger 3d311d3677 BarUser FreeDrinkList zeigt Summe von Bandgetränken an. 2020-09-01 09:31:03 +02:00
Tim Gröger 10eccba914 Entfernen der User aus den FreeDrinkLists 2020-09-01 09:16:24 +02:00
Tim Gröger 61dd94c523 Merge branch 'develop' into feature/drinkList 2020-09-01 09:05:42 +02:00
Tim Gröger b92a94adb0 Fix sidewards-compability
Bei der Sidewards-compability wurde durch das umschreiben die Gruppen des Nutzers nicht mehr richtig gespeichert. Dieser Fehler wurde behoben.
2020-09-01 09:02:55 +02:00
Tim Gröger affaa639d8 Merge branch 'develop' into feature/drinkList 2020-08-28 11:01:10 +02:00
Ferdinand Thiessen c379656f3e Sidewards compatibility with pluginify 2020-08-25 22:15:48 +02:00
Ferdinand Thiessen c83ba2d20d Fix security issues found by npm audit 2020-08-25 04:21:16 +02:00
Tim Gröger f8e486bad9 Version und Backend geändert 2020-08-24 16:07:30 +02:00
Tim Gröger 3ea93fb800 Merge branch 'feature/drinkList' into develop 2020-08-24 15:55:55 +02:00
Tim Gröger c8a6ab7d35 Gründe für Freigetränke können erstellt und gelöscht werden
Außerdem wurd überall ein Loading balken hinzugefügt.
2020-08-24 15:18:21 +02:00
Tim Gröger 6c5c67f45b Freigetränke Listen für Dienst, AG und Band
Der Vorstand kann nun die Freigetränkelisten von Diensten, AG's und Bands anschauen.
2020-08-24 14:04:21 +02:00
Tim Gröger 0fcbbe23c2 Overview für Freigetränke
es gibt nun eine Gesamtübersicht aller Freigetränke für einen bestimmten Monat.
Für Dienstgetränke funktioniert es schon auch ist aber noch nicht vollständig.
2020-08-23 23:18:36 +02:00
Tim Gröger 8a442d029b Löschen, Modifizieren und Hinzufügen von Freigetränken
Der Vorstand kann nun Freigetränke löschen, modifizieren und hinzufügen.
2020-08-23 21:16:56 +02:00
Tim Gröger a28bbe8e0a better view for freedrinklistconfig 2020-08-22 18:10:39 +02:00
Tim Gröger 6e4d3a8a01 MainLayout für Freigetränke für den Vorstand gesetzt
Es wurde das MainLayout für die Freigetränke für den Vorstand gesetzt. Dabei wurden free_drink_types hinzugefügt und die Einstellungsseite für die Freigetränke angefangen. Bis jetzt können diese geladen werden.
2020-08-21 22:03:24 +02:00
Tim Gröger 450f691b9c Merge branch 'hotfix/bug395' into develop 2020-08-21 15:08:14 +02:00
Tim Gröger 72e2606ed2 Backendconnection für FreeDrinkListHistoryWorkgroup
Es können jetzt auch Freigetränke für mit verschiedenen Gründen und Beschreibungen angelegt werden. Count ist vernachlässigt worden und muss größer als 0 sein. Es kann ebenfalls storniert werden.
2020-08-20 22:05:20 +02:00
Tim Gröger a91384546b Snackbar hinzugefügt
Man kann nun sehen, was hinzugefügt wurde und falls ein fehler auftritt was nicht hinzugefügt wurde.
Außerdem gibt es ladebalken.
2020-08-20 11:33:47 +02:00
Tim Gröger 8ac74c9f64 Backend Connection für Freigetränke Band und Dienste
Es wurde die Verbindung zur Schnittstelle aufgebaut. Freigetränke werden Dynamisch gehalten.
2020-08-20 08:39:38 +02:00
Snowmee 22c5ebce1b add freedrink to user 2020-08-08 23:15:04 +02:00
Snowmee d5393d75c5 Dienstgetränke UI ohne backendanschluss 2020-08-08 21:09:16 +02:00
156 changed files with 3975 additions and 23756 deletions

9
.editorconfig Normal file
View File

@ -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

8
.eslintignore Normal file
View File

@ -0,0 +1,8 @@
/dist
/src-bex/www
/src-capacitor
/src-cordova
/.quasar
/node_modules
/src-ssr
.*

91
.eslintrc.js Normal file
View File

@ -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',
},
};

28
.gitignore vendored
View File

@ -1,10 +1,30 @@
.DS_Store .DS_Store
.thumbs.db
node_modules node_modules
# We use yarn, so ignore npm
package-lock.json
yarn.lock
# Quasar core related directories
.quasar
/dist /dist
# local env files # Cordova related directories and files
.env.local /src-cordova/node_modules
.env.*.local /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 # Log files
npm-debug.log* npm-debug.log*
@ -13,9 +33,7 @@ yarn-error.log*
# Editor directories and files # Editor directories and files
.idea .idea
.vscode
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw?

3
.npmignore Normal file
View File

@ -0,0 +1,3 @@
yarn-error.log
.woodpecker/

8
.postcssrc.js Normal file
View File

@ -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'),
],
};

View File

@ -1,4 +0,0 @@
module.exports = {
singleQuote: true,
semi: false
}

12
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"octref.vetur"
],
"unwantedRecommendations": [
"hookyqr.beautify",
"dbaeumer.jshint",
"ms-vscode.vscode-typescript-tslint-plugin"
]
}

30
.vscode/settings.json vendored Normal file
View File

@ -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"
}

15
.woodpecker/deploy.yml Normal file
View File

@ -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

9
.woodpecker/lint.yml Normal file
View File

@ -0,0 +1,9 @@
pipeline:
lint:
when:
branch: [main, develop]
image: node:lts-alpine
commands:
- yarn install
- yarn lint

21
LICENSE Normal file
View File

@ -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.

View File

@ -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 So on debian (buster and bullseye) you will need to install node.js and yarn beside the debian packages to meet the needed versions.
```
npm run serve ```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 ### Install the dependencies
```
npm run build ```bash
yarn install
``` ```
### Lints and fixes files Be aware npm might not work.
```
npm run lint ### 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 #### Enable / Disable a plugin
See [Configuration Reference](https://cli.vuejs.org/config/).
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).

View File

@ -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>

View File

@ -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>

View File

@ -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>

5
api/components/index.ts Normal file
View File

@ -0,0 +1,5 @@
import IsoDateInput from './IsoDateInput.vue';
import PasswordInput from './PasswordInput.vue';
import UserAvatar from './UserAvatar.vue';
export { IsoDateInput, PasswordInput, UserAvatar };

10
api/index.ts Normal file
View File

@ -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';

28
api/package.json Normal file
View File

@ -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"
}
}

6
api/shims-vue.d.ts vendored Normal file
View File

@ -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;
}

6
api/src/internal.ts Normal file
View File

@ -0,0 +1,6 @@
import axios from 'axios';
import { createPinia } from 'pinia';
export const api = axios.create();
export const pinia = createPinia();

23
api/src/stores/index.ts Normal file
View File

@ -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';

163
api/src/stores/main.ts Normal file
View File

@ -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;

71
api/src/stores/session.ts Normal file
View File

@ -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;
}
},
},
});

211
api/src/stores/user.ts Normal file
View File

@ -0,0 +1,211 @@
import { defineStore } from 'pinia';
import { api } from '../internal';
import { isAxiosError, useMainStore } from '.';
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[],
// 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) {
return state._users.filter((u) => !u.deleted);
},
},
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);
},
},
});

45
api/src/utils/datetime.ts Normal file
View File

@ -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;
}

11
api/src/utils/loading.ts Normal file
View File

@ -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;

3
api/src/utils/misc.ts Normal file
View File

@ -0,0 +1,3 @@
export function clone<T>(o: T): T {
return <T>JSON.parse(JSON.stringify(o));
}

View File

@ -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));
}

View File

@ -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());
}
}

6
api/src/utils/user.ts Normal file
View File

@ -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' : ''}`;
}

View File

@ -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.'
);
}

9
api/tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "@quasar/app/tsconfig-preset",
"target": "esnext",
"compilerOptions": {
"baseUrl": "./",
"lib": ["es2020", "dom"],
"types": ["@flaschengeist/types", "@quasar/app", "node"]
}
}

View File

@ -1,5 +1,4 @@
/* eslint-env node */
module.exports = { module.exports = {
presets: [ presets: ['@quasar/babel-preset-app'],
'@vue/cli-plugin-babel/preset' };
]
}

View File

@ -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-----

View File

@ -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-----

12003
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,61 +1,78 @@
{ {
"name": "newgruecht-vue",
"version": "1.0.1",
"private": true, "private": true,
"license": "MIT",
"version": "2.0.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": { "scripts": {
"serve": "vue-cli-service serve", "format": "prettier --config ./package.json --write '{,!(node_modules|dist|.*)/**/}*.{js,ts,vue}'",
"build": "vue-cli-service build", "lint": "eslint --ext .js,.ts,.vue ./src ./api"
"lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"@mdi/font": "^4.9.95", "@flaschengeist/api": "^1.0.0",
"@mdi/js": "^4.9.95", "@flaschengeist/balance": "^1.0.0",
"core-js": "^3.6.5", "@flaschengeist/pricelist-old": "^1.0.0",
"vue": "^2.6.10", "@flaschengeist/schedule": "^1.0.0",
"vue-router": "^3.2.0", "@flaschengeist/users": "^1.0.0",
"vuetify": "^2.2.29", "axios": "^1.4.0",
"vuex": "^3.4.0" "pinia": "^2.0.8",
"quasar": "^2.11.10",
"vue": "^3.0.0",
"vue-router": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^4.3.1", "@capacitor/core": "^5.0.0",
"@vue/cli-plugin-eslint": "^4.3.1", "@capacitor/preferences": "^5.0.0",
"@vue/cli-plugin-router": "^4.3.1", "@flaschengeist/types": "^1.0.0",
"@vue/cli-plugin-vuex": "^4.3.1", "@quasar/app-webpack": "^3.7.2",
"@vue/cli-service": "^4.3.1", "@quasar/extras": "^1.16.3",
"@vue/eslint-config-prettier": "^6.0.0", "@types/node": "^14.18.0",
"axios": "^0.19.2", "@types/webpack": "^5.28.0",
"babel-eslint": "^10.1.0", "@types/webpack-env": "^1.16.3",
"eslint": "^5.16.0", "@typescript-eslint/eslint-plugin": "^5.8.0",
"eslint-plugin-prettier": "^3.1.3", "@typescript-eslint/parser": "^5.8.0",
"eslint-plugin-vue": "^5.0.0", "@vue/devtools": "^6.5.0",
"material-design-icons-iconfont": "^5.0.1", "eslint": "^8.5.0",
"prettier": "^1.19.1", "eslint-config-prettier": "^8.3.0",
"sass": "^1.26.5", "eslint-plugin-prettier": "^4.0.0",
"sass-loader": "^8.0.2", "eslint-plugin-vue": "^9.14.1",
"vue-cli-plugin-vuetify": "^2.0.5", "eslint-webpack-plugin": "^4.0.1",
"vue-template-compiler": "^2.6.10", "modify-source-webpack-plugin": "^4.1.0",
"vuetify-loader": "^1.4.4" "prettier": "^2.5.1",
"typescript": "^4.5.4",
"vuedraggable": "^4.1.0"
}, },
"eslintConfig": { "prettier": {
"root": true, "singleQuote": true,
"env": { "semi": true,
"node": true "printWidth": 100,
}, "arrowParens": "always"
"extends": [ },
"plugin:vue/essential", "browserslist": {
"plugin:prettier/recommended", "defaults": [
"@vue/prettier", "Firefox esr",
"eslint:recommended" "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": { "cordova": [
"no-console": "off" "iOS >= 13.0",
}, "Android >= 76",
"parserOptions": { "ChromeAndroid >= 76"
"parser": "babel-eslint" ]
}
}, },
"browserslist": [ "engines": {
"> 1%", "node": ">= 14.18.1",
"last 2 versions" "npm": ">= 6.14.12",
] "yarn": ">= 1.22.0"
}
} }

6
plugin.config.js Normal file
View File

@ -0,0 +1,6 @@
// You can add your plugins here
module.exports = [
// '@flaschengeist/balance',
// '@flaschengeist/schedule',
// '@flaschengeist/pricelist',
]

BIN
public/favicon-128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
public/favicon-256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -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

View File

@ -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>

168
public/no-image.svg Normal file
View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

218
quasar.conf.js Normal file
View File

@ -0,0 +1,218 @@
/*
* 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-v6',
// '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'
// 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
},
},
};
});

10
src-capacitor/capacitor-flag.d.ts vendored Normal file
View File

@ -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;
}
}

View File

@ -0,0 +1,13 @@
{
"appId": "dev.flaschengeist",
"appName": "flaschengeist-frontend",
"bundledWebRuntime": false,
"npmClient": "yarn",
"webDir": "www",
"android": {
"minWebViewVersion": 71
},
"ios": {
"allowsLinkPreview": false
}
}

35
src-capacitor/index.html Normal file
View File

@ -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>

View File

@ -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"
}
}

10
src-electron/electron-flag.d.ts vendored Normal file
View File

@ -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;
}
}

View File

@ -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()
}
})

View File

@ -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: () => {}
* })
*/

Binary file not shown.

BIN
src-electron/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -1,232 +1,10 @@
<template> <template>
<v-app> <router-view />
<TitleBar />
<router-view />
<v-footer app>
<span class="px-4 d-none d-sm-flex"
>&copy; {{ 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> </template>
<script lang="ts">
import { defineComponent } from 'vue';
<script> export default defineComponent({
import TitleBar from './components/TitleBar'
import { mapGetters, mapActions } from 'vuex'
export default {
name: 'App', 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> </script>

0
src/assets/.gitkeep Normal file
View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

109
src/boot/axios.ts Normal file
View File

@ -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;
}
);
});

97
src/boot/init.ts Normal file
View File

@ -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);
}
}
});

33
src/boot/login.ts Normal file
View File

@ -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;
});
});

288
src/boot/plugins.ts Normal file
View File

@ -0,0 +1,288 @@
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);
}
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: [],
};
// 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;
}

9
src/boot/store.ts Normal file
View File

@ -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);
});

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,14 @@
<template>
<router-view />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'EmptyParent',
setup() {
return {};
},
});
</script>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More