Merge remote-tracking branch 'origin/develop' into feature/pricelist

This commit is contained in:
Tim Gröger 2021-04-03 20:51:41 +02:00
commit 741216ac3e
16 changed files with 923 additions and 539 deletions

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,47 +1,62 @@
# Flaschengeist (flaschengeist-frontend) # Flaschengeist (frontend)
Dynamischen Managementsystem für Studentenclubs Modular student club administration system, licensed under the MIT license.
## Installation
### Install the dependencies
## Install the dependencies
```bash ```bash
yarn install yarn install
``` ```
## Plugins ### Configure Plugins
### Build a Plugin
You can activate and deactive Plugins in `src/boot/plugins.ts`.
You have to set the name of the Plugin into `config.loadModules`.
### Build the application
```bash
yarn quasar build
```
## Development
### Icons used
We are using the `mdi-v5` icon set, so feel free to use any icon from it.
A list can be found [here](https://materialdesignicons.com/)
### Commands useful for development
#### Start the app in development mode
Provides hot-code reloading, error reporting, etc.
```bash
yarn quasar dev
```
#### File linting
```bash
yarn run lint
```
### Plugins
#### Build a Plugin
A Flaschengeist-Frontend-Plugin should be placed in `src/plugins`. A Flaschengeist-Frontend-Plugin should be placed in `src/plugins`.
It needs a `plugin.ts` File which exports a plugin with the following interface: It needs a `plugin.ts` File which exports a plugin with the following interface:
``` ```
name: string; name: string;
mainRoutes?: PluginRouteConfig[]; mainRoutes?: PluginRouteConfig[];
outRoutes?: PluginRouteConfig[]; outRoutes?: PluginRouteConfig[];
store?: Map<string, Module<any, StateInterface>>;
requiredModules: string[]; requiredModules: string[];
version: string; version: string;
``` ```
You have to import `FG_Plugin` from `plugins.d.ts`. You have to import `FG_Plugin` from `plugins.d.ts`.
### Configure Plugin
You can activate and deactive Plugins in `src/boot/plugins.ts`. You have to set the name of the Plugin into `config.loadModules`.
The order of the plugins is importend!
### Start the app in development mode (hot-code reloading, error reporting, etc.)
```bash
yarn quasar dev
```
### Lint the files
```bash
yarn run lint
```
### Build the app for production
```bash
yarn quasar build
```
<!--
### Customize the configuration
See [Configuring quasar.conf.js](https://quasar.dev/quasar-cli/quasar-conf-js).
-->

View File

@ -1,10 +1,15 @@
{ {
"name": "flaschengeist-frontend",
"version": "0.1.0-alpha.1",
"description": "Dynamischen Managementsystem für Studentenclubs",
"productName": "Flaschengeist",
"author": "Tim Gröger <tim@groeger-clan.de>",
"private": true, "private": true,
"license": "MIT",
"version": "2.0.0-alpha.1",
"productName": "Flaschengeist",
"name": "flaschengeist-frontend",
"author": "Tim Gröger <flaschengeist@wu5.de>",
"homepage": "https://flaschengeist.dev/Flaschengeist",
"description": "Modular student club administration system",
"bugs": {
"url" : "https://flaschengeist.dev/Flaschengeist/flaschengeist-frontend/issues"
},
"scripts": { "scripts": {
"format": "prettier --config ./package.json --write '{,!(node_modules)/**/}*.ts'", "format": "prettier --config ./package.json --write '{,!(node_modules)/**/}*.ts'",
"lint": "eslint --ext .js,.ts,.vue ./src" "lint": "eslint --ext .js,.ts,.vue ./src"
@ -12,32 +17,31 @@
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
"cordova": "^10.0.0", "cordova": "^10.0.0",
"core-js": "^3.9.1", "pinia": "^2.0.0-alpha.10",
"pinia": "^2.0.0-alpha.7",
"quasar": "^2.0.0-beta.11" "quasar": "^2.0.0-beta.11"
}, },
"devDependencies": {
"@quasar/app": "^3.0.0-beta.10",
"@quasar/extras": "^1.10.2",
"@quasar/quasar-app-extension-qcalendar": "file:deps/quasar-ui-qcalendar/app-extension",
"@types/node": "^12.20.7",
"@types/webpack": "^4.41.27",
"@types/webpack-env": "^1.16.0",
"@typescript-eslint/eslint-plugin": "^4.20.0",
"@typescript-eslint/parser": "^4.20.0",
"eslint": "^7.23.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-vue": "^7.8.0",
"eslint-webpack-plugin": "^2.5.3",
"prettier": "^2.2.1",
"typescript": "^4.2.3"
},
"prettier": { "prettier": {
"singleQuote": true, "singleQuote": true,
"semi": true, "semi": true,
"printWidth": 100, "printWidth": 100,
"arrowParens": "always" "arrowParens": "always"
}, },
"devDependencies": {
"@quasar/app": "^3.0.0-beta.10",
"@quasar/extras": "^1.10.0",
"@quasar/quasar-app-extension-qcalendar": "file:deps/quasar-ui-qcalendar/app-extension",
"@types/node": "^12.20.6",
"@types/webpack": "^4.41.26",
"@types/webpack-env": "^1.16.0",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"eslint": "^7.22.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-vue": "^7.7.0",
"eslint-webpack-plugin": "^2.5.2",
"prettier": "^2.2.1",
"typescript": "^4.2.3"
},
"browserslist": [ "browserslist": [
"last 10 Chrome versions", "last 10 Chrome versions",
"last 10 Firefox versions", "last 10 Firefox versions",

View File

@ -37,16 +37,16 @@ module.exports = configure(function (/* ctx */) {
// https://github.com/quasarframework/quasar/tree/dev/extras // https://github.com/quasarframework/quasar/tree/dev/extras
extras: [ extras: [
// 'ionicons-v4',
'mdi-v5',
// 'fontawesome-v5',
// 'eva-icons', // 'eva-icons',
// 'themify', // 'fontawesome-v5',
// 'ionicons-v4',
// 'line-awesome', // 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! // 'material-icons',
'mdi-v5',
// 'themify',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
'roboto-font', // optional, you are not bound to it 'roboto-font', // optional, you are not bound to it
'material-icons', // optional, you are not bound to it
], ],
// Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build
@ -74,7 +74,7 @@ module.exports = configure(function (/* ctx */) {
chainWebpack (chain) { chainWebpack (chain) {
chain.plugin('eslint-webpack-plugin') chain.plugin('eslint-webpack-plugin')
.use(ESLintPlugin, [{ .use(ESLintPlugin, [{
extensions: [ 'js', 'vue' ], extensions: [ 'ts', 'js', 'vue' ],
exclude: 'node_modules' exclude: 'node_modules'
}]) }])
}, },
@ -90,7 +90,7 @@ module.exports = configure(function (/* ctx */) {
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
framework: { framework: {
iconSet: 'material-icons', // Quasar icon set iconSet: 'mdi-v5', // Quasar icon set
lang: 'de', // Quasar language pack lang: 'de', // Quasar language pack
config: { config: {
dark: 'auto', dark: 'auto',
@ -134,7 +134,7 @@ module.exports = configure(function (/* ctx */) {
manifest: { manifest: {
name: 'Flaschengeist', name: 'Flaschengeist',
short_name: 'Flaschengeist', short_name: 'Flaschengeist',
description: 'Dynamischen Managementsystem für Studentenclubs', description: 'Modular student club administration system',
display: 'standalone', display: 'standalone',
orientation: 'portrait', orientation: 'portrait',
background_color: '#ffffff', background_color: '#ffffff',

View File

@ -7,10 +7,11 @@
:placeholder="placeholder" :placeholder="placeholder"
:rules="customRules" :rules="customRules"
:clearable="clearable" :clearable="clearable"
v-bind="attrs"
@clear="dateTime = ''" @clear="dateTime = ''"
> >
<template #append> <template #append>
<q-icon v-if="'date' || type == 'datetime'" name="event" class="cursor-pointer"> <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-popup-proxy ref="qDateProxy" transition-show="scale" transition-hide="scale">
<q-date v-model="date" mask="YYYY-MM-DD"> <q-date v-model="date" mask="YYYY-MM-DD">
<div class="row items-center justify-end"> <div class="row items-center justify-end">
@ -58,7 +59,7 @@ export default defineComponent({
}, },
}, },
emits: { 'update:modelValue': (date?: Date) => !!date || !date }, emits: { 'update:modelValue': (date?: Date) => !!date || !date },
setup(props, { emit }) { setup(props, { emit, attrs }) {
const customRules = computed(() => [ const customRules = computed(() => [
props.type == 'date' ? stringIsDate : props.type == 'time' ? stringIsTime : stringIsDateTime, props.type == 'date' ? stringIsDate : props.type == 'time' ? stringIsTime : stringIsDateTime,
...props.rules, ...props.rules,
@ -138,12 +139,13 @@ export default defineComponent({
} }
return { return {
attrs,
clearable, clearable,
date,
time,
dateTime,
customRules, customRules,
date,
dateTime,
placeholder, placeholder,
time,
}; };
}, },
}); });

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

@ -1,15 +1,8 @@
<template> <template>
<q-layout view="hHh lpr lFf"> <q-layout view="hHh Lpr lff">
<q-header elevated class="bg-primary text-white"> <q-header elevated class="bg-primary text-white">
<q-toolbar> <q-toolbar>
<q-btn <q-btn dense flat round icon="mdi-menu" @click="openMenu" />
v-if="!leftDrawerOpen"
dense
flat
round
icon="menu"
@click="leftDrawerOpen = !leftDrawerOpen"
/>
<q-toolbar-title> <q-toolbar-title>
<router-link :to="{ name: 'dashboard' }" style="text-decoration: none; color: inherit"> <router-link :to="{ name: 'dashboard' }" style="text-decoration: none; color: inherit">
@ -21,7 +14,7 @@
</q-toolbar-title> </q-toolbar-title>
<!-- Hier kommen die Shortlinks hin --> <!-- Hier kommen die Shortlinks hin -->
<q-btn icon="message" flat dense <q-btn icon="mdi-message-bulleted" flat dense
><q-badge color="negative" floating>{{ notifications.length }}</q-badge> ><q-badge color="negative" floating>{{ notifications.length }}</q-badge>
<q-menu style="max-height: 400px; overflow: auto"> <q-menu style="max-height: 400px; overflow: auto">
<q-btn <q-btn
@ -50,12 +43,11 @@
</q-header> </q-header>
<q-drawer <q-drawer
v-model="leftDrawerOpen" v-model="leftDrawer"
show-if-above
side="left" side="left"
bordered bordered
:mini="leftDrawerMini" :mini="leftDrawerMini"
@click.capture="leftDrawerClicker" @click.capture="openMenu"
> >
<!-- Plugins --> <!-- Plugins -->
<q-list> <q-list>
@ -66,28 +58,13 @@
/> />
<q-separator /> <q-separator />
<!-- Plugin functions --> <!-- Plugin functions -->
<essential-link <essential-link
v-for="(entry, index) in subLinks" v-for="(entry, index) in subLinks"
:key="'childPlugin' + index" :key="'childPlugin' + index"
:entry="entry" :entry="entry"
/> />
</q-list> </q-list>
<div class="q-mini-drawer-hide absolute" style="top: 15px; right: -11px">
<q-btn
size="sm"
dense
round
unelevated
color="secondary"
icon="chevron_left"
@click="leftDrawerMini = true"
/>
</div>
<q-separator /> <q-separator />
<essential-link <essential-link
v-for="(entry, index) in essentials" v-for="(entry, index) in essentials"
:key="'essential' + index" :key="'essential' + index"
@ -126,7 +103,7 @@ export default defineComponent({
const router = useRouter(); const router = useRouter();
const mainStore = useMainStore(); const mainStore = useMainStore();
const flaschengeist = inject<FG_Plugin.Flaschengeist>('flaschengeist'); const flaschengeist = inject<FG_Plugin.Flaschengeist>('flaschengeist');
const leftDrawer = ref(false); const leftDrawer = ref(true);
const leftDrawerMini = ref(false); const leftDrawerMini = ref(false);
const shortcuts = flaschengeist?.shortcuts || []; const shortcuts = flaschengeist?.shortcuts || [];
const mainLinks = flaschengeist?.menuLinks || []; const mainLinks = flaschengeist?.menuLinks || [];
@ -135,22 +112,27 @@ export default defineComponent({
const useNative = 'Notification' in window && window.Notification !== undefined; const useNative = 'Notification' in window && window.Notification !== undefined;
const noPermission = ref(!useNative || window.Notification.permission !== 'granted'); const noPermission = ref(!useNative || window.Notification.permission !== 'granted');
onBeforeMount(() => pollNotification());
onBeforeUnmount(() => window.clearInterval(polling.value));
const leftDrawerOpen = computed({
get: () => (leftDrawer.value || Screen.gt.sm ? true : false),
set: (val: boolean) => (leftDrawer.value = val),
});
const subLinks = computed(() => { const subLinks = computed(() => {
const matched = router.currentRoute.value.matched[1]; const matched = router.currentRoute.value.matched[1];
return flaschengeist?.menuLinks.find((link) => matched.name == link.link)?.children; return flaschengeist?.menuLinks.find((link) => matched.name == link.link)?.children;
}); });
function leftDrawerClicker() { onBeforeMount(() => {
if (leftDrawerMini.value) { polling.value = window.setInterval(() => pollNotification(), config.pollingInterval);
leftDrawerMini.value = false; pollNotification();
});
onBeforeUnmount(() => window.clearInterval(polling.value));
function openMenu(event: { target: HTMLInputElement }) {
if (event.target.nodeName === 'DIV') leftDrawerMini.value = false;
else {
if (!leftDrawer.value || leftDrawerMini.value) {
leftDrawer.value = true;
leftDrawerMini.value = false;
} else {
leftDrawerMini.value = Screen.gt.sm && !leftDrawerMini.value;
leftDrawer.value = leftDrawerMini.value;
}
} }
} }
@ -170,30 +152,28 @@ export default defineComponent({
} }
function pollNotification() { function pollNotification() {
polling.value = window.setInterval(() => { void mainStore
void mainStore .loadNotifications(<FG_Plugin.Flaschengeist>flaschengeist)
.loadNotifications(<FG_Plugin.Flaschengeist>flaschengeist) .then((notifications) => {
.then((notifications) => { if (useNative && !noPermission.value)
if (useNative && !noPermission.value) notifications.forEach(
notifications.forEach( (notif) =>
(notif) => new window.Notification(notif.text, {
new window.Notification(notif.text, { timestamp: notif.time.getTime(),
timestamp: notif.time.getTime(), })
}) );
); });
});
}, config.pollingInterval);
} }
return { return {
essentials, essentials,
leftDrawerOpen, leftDrawer,
leftDrawerMini, leftDrawerMini,
leftDrawerClicker,
logout, logout,
mainLinks, mainLinks,
notifications, notifications,
noPermission, noPermission,
openMenu,
remove, remove,
requestPermission, requestPermission,
shortcuts, shortcuts,

View File

@ -6,20 +6,21 @@
</q-toolbar> </q-toolbar>
<q-card-section> <q-card-section>
<q-form ref="LoginForm" class="q-gutter-md" @submit="doLogin"> <q-form class="q-gutter-md" @submit="doLogin">
<q-input <q-input
v-model="userid" v-model="userid"
filled filled
label="Benutzername oder E-Mail" label="Benutzername oder E-Mail"
:rules="rules" autocomplete="username"
:rules="[notEmpty]"
tabindex="1" tabindex="1"
/> />
<q-input <password-input
v-model="password" v-model="password"
filled filled
type="password" label="Passwort"
label="Password" autocomplete="cureent-password"
:rules="rules" :rules="[notEmpty]"
tabindex="2" tabindex="2"
/> />
<div class="row justify-between"> <div class="row justify-between">
@ -54,15 +55,18 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { useMainStore } from 'src/stores';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { Loading, Notify } from 'quasar'; import { Loading, Notify } from 'quasar';
import { useMainStore } from 'src/stores';
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
import { setBaseURL, api } from 'boot/axios'; import { setBaseURL, api } from 'boot/axios';
import { notEmpty } from 'src/utils/validators';
import { useUserStore } from 'src/plugins/user/store'; import { useUserStore } from 'src/plugins/user/store';
import PasswordInput from 'src/components/utils/PasswordInput.vue';
export default defineComponent({ export default defineComponent({
name: 'Login', name: 'Login',
components: { PasswordInput },
setup() { setup() {
const mainStore = useMainStore(); const mainStore = useMainStore();
const mainRoute = { name: 'dashboard' }; const mainRoute = { name: 'dashboard' };
@ -71,7 +75,6 @@ export default defineComponent({
/* Stuff for the real login page */ /* Stuff for the real login page */
const userid = ref(''); const userid = ref('');
const password = ref(''); const password = ref('');
const rules = [(val: string) => (val && val.length > 0) || 'Feld darf nicht leer sein!'];
const server = ref<string | undefined>(api.defaults.baseURL); const server = ref<string | undefined>(api.defaults.baseURL);
const visible = ref(false); const visible = ref(false);
@ -138,15 +141,15 @@ export default defineComponent({
} }
return { return {
userid, changeUrl,
password,
doLogin, doLogin,
doReset, doReset,
rules, notEmpty,
server,
changeUrl,
visible,
openServerSettings, openServerSettings,
password,
server,
userid,
visible,
}; };
}, },
}); });

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
<q-tabs v-if="$q.screen.gt.sm" v-model="tab"> <q-tabs v-if="$q.screen.gt.sm" v-model="tab">
<q-tab <q-tab
v-for="(tabindex, index) in tabs" v-for="(tabindex, index) in tabs"
@ -37,32 +37,30 @@
</div> </div>
</q-list> </q-list>
</q-drawer> </q-drawer>
<q-page padding class="fit row justify-left q-col-gutter-sm"> <q-tab-panels
<q-tab-panels v-model="tab"
v-model="tab" style="background-color: transparent"
style="background-color: transparent" class="q-pa-none col-12"
class="q-pa-none col-12" animated
animated >
> <q-tab-panel name="add" class="q-px-xs">
<q-tab-panel name="add" class="q-px-xs"> <BalanceAdd
<BalanceAdd @open-history="
@open-history=" showDrawer = !showDrawer;
showDrawer = !showDrawer; show = true;
show = true; "
" />
/> </q-tab-panel>
</q-tab-panel> <q-tab-panel name="transfer" class="q-px-xs">
<q-tab-panel name="transfer" class="q-px-xs"> <BalanceTransfer
<BalanceTransfer @open-history="
@open-history=" showDrawer = !showDrawer;
showDrawer = !showDrawer; show = true;
show = true; "
" />
/> </q-tab-panel>
</q-tab-panel> </q-tab-panels>
</q-tab-panels> </q-page>
</q-page>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">

View File

@ -1,10 +1,10 @@
<template> <template>
<q-card style="text-align: center"> <q-card style="text-align: center">
<q-card-section class="row justify-center content-stretch"> <q-card-section class="row justify-center content-stretch">
<div class="col-4"> <div v-if="avatar" class="col-4">
<div style="width: 100%; padding-bottom: 100%; position: relative"> <div style="width: 100%; padding-bottom: 100%; position: relative">
<q-avatar style="position: absolute; top: 0; left: 0; width: 100%; height: 100%"> <q-avatar style="position: absolute; top: 0; left: 0; width: 100%; height: 100%">
<img :src="avatarLink" /> <img :src="avatarLink" :onerror="error" />
</q-avatar> </q-avatar>
</div> </div>
</div> </div>
@ -39,9 +39,14 @@ export default defineComponent({
// Ensure users are loaded,so we can query birthdays // Ensure users are loaded,so we can query birthdays
onMounted(() => userStore.getUsers(false)); onMounted(() => userStore.getUsers(false));
const name = ref(mainStore.currentUser.display_name); const avatar = ref(true);
const name = ref(mainStore.currentUser.firstname);
const avatarLink = ref(mainStore.currentUser.avatar_url); const avatarLink = ref(mainStore.currentUser.avatar_url);
function error() {
avatar.value = false;
}
function userHasBirthday(user: FG.User) { function userHasBirthday(user: FG.User) {
const today = new Date(); const today = new Date();
return ( return (
@ -61,7 +66,7 @@ export default defineComponent({
.filter((user) => user.userid !== mainStore.currentUser.userid) .filter((user) => user.userid !== mainStore.currentUser.userid)
); );
return { avatarLink, name, hasBirthday, birthday }; return { avatar, avatarLink, error, name, hasBirthday, birthday };
}, },
}); });
</script> </script>

View File

@ -2,44 +2,48 @@
<q-form @submit="save" @reset="reset"> <q-form @submit="save" @reset="reset">
<q-card-section class="fit row justify-start content-center items-center"> <q-card-section class="fit row justify-start content-center items-center">
<q-input <q-input
v-model="user_model.firstname" v-model="userModel.firstname"
class="col-xs-12 col-sm-6 q-pa-sm" class="col-xs-12 col-sm-6 q-pa-sm"
label="Vorname" label="Vorname"
:rules="[notEmpty]" :rules="[notEmpty]"
autocomplete="given-name"
filled filled
/> />
<q-input <q-input
v-model="user_model.lastname" v-model="userModel.lastname"
class="col-xs-12 col-sm-6 q-pa-sm" class="col-xs-12 col-sm-6 q-pa-sm"
label="Nachname" label="Nachname"
:rules="[notEmpty]" :rules="[notEmpty]"
autocomplete="family-name"
filled filled
/> />
<q-input <q-input
v-model="user_model.display_name" v-model="userModel.display_name"
class="col-xs-12 col-sm-6 q-pa-sm" class="col-xs-12 col-sm-6 q-pa-sm"
label="Angezeigter Name" label="Angezeigter Name"
:rules="[notEmpty]" :rules="[notEmpty]"
autocomplete="nickname"
filled filled
/> />
<q-input <q-input
v-model="user_model.mail" v-model="userModel.mail"
class="col-xs-12 col-sm-6 q-pa-sm" class="col-xs-12 col-sm-6 q-pa-sm"
label="E-Mail" label="E-Mail"
:rules="[isEmail, notEmpty]" :rules="[isEmail, notEmpty]"
autocomplete="email"
filled filled
/> />
<q-input <q-input
v-model="user_model.userid" v-model="userModel.userid"
class="col-xs-12 col-sm-6 q-pa-sm" class="col-xs-12 col-sm-6 q-pa-sm"
label="Benutzername" label="Benutzername"
:readonly="!newUser" :readonly="!newUser"
:rules="[isUseridUsed, notEmpty]" :rules="newUser ? [isFreeUID, notEmpty] : []"
autocomplete="username"
filled filled
/> />
<q-select <q-select
v-model="user_model.roles" v-model="userModel.roles"
class="col-xs-12 col-sm-6 q-pa-sm" class="col-xs-12 col-sm-6 q-pa-sm"
label="Rollen" label="Rollen"
filled filled
@ -51,9 +55,10 @@
option-value="name" option-value="name"
/> />
<IsoDateInput <IsoDateInput
v-model="user_model.birthday" v-model="userModel.birthday"
class="col-xs-12 col-sm-6 q-pa-sm" class="col-xs-12 col-sm-6 q-pa-sm"
label="Geburtstag" label="Geburtstag"
autocomplete="bday"
/> />
<q-file <q-file
v-model="avatar" v-model="avatar"
@ -72,31 +77,22 @@
</q-card-section> </q-card-section>
<q-separator v-if="!newUser" /> <q-separator v-if="!newUser" />
<q-card-section v-if="!newUser" class="fit row justify-start content-center items-center"> <q-card-section v-if="!newUser" class="fit row justify-start content-center items-center">
<q-input <PasswordInput
v-if="isCurrentUser" v-if="isCurrentUser"
v-model="password" v-model="password"
class="col-xs-12 col-sm-6 col-md-4 q-pa-sm"
label="Password"
type="password"
hint="Password muss immer eingetragen werden"
:rules="[notEmpty]" :rules="[notEmpty]"
filled filled
label="Passwort"
autocomplete="current-password"
class="col-xs-12 col-sm-6 q-pa-sm"
hint="Passwort muss immer eingetragen werden"
/> />
<q-input <PasswordInput
v-model="new_password" v-model="newPassword"
class="col-xs-12 col-sm-6 col-md-4 q-pa-sm" filled
label="Neues Password" label="Neues Password"
type="password" autocomplete="new-password"
filled class="col-xs-12 col-sm-6 q-pa-sm"
/>
<q-input
v-model="new_password2"
class="col-xs-12 col-sm-6 col-md-4 q-pa-sm"
label="Wiederhole neues Password"
type="password"
:disable="new_password.length == 0"
:rules="[samePassword]"
filled
/> />
</q-card-section> </q-card-section>
<q-card-actions align="right"> <q-card-actions align="right">
@ -109,14 +105,16 @@
<script lang="ts"> <script lang="ts">
import { Notify } from 'quasar'; import { Notify } from 'quasar';
import { hasPermission } from 'src/utils/permission'; import { hasPermission } from 'src/utils/permission';
import { notEmpty, isEmail } from 'src/utils/validators';
import IsoDateInput from 'src/components/utils/IsoDateInput.vue'; import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
import { defineComponent, computed, ref, onBeforeMount, PropType } from 'vue'; import PasswordInput from 'src/components/utils/PasswordInput.vue';
import { defineComponent, computed, ref, onBeforeMount, PropType, watch } from 'vue';
import { useUserStore } from '../../store'; import { useUserStore } from '../../store';
import { useMainStore } from 'src/stores'; import { useMainStore } from 'src/stores';
export default defineComponent({ export default defineComponent({
name: 'MainUserSettings', name: 'MainUserSettings',
components: { IsoDateInput }, components: { IsoDateInput, PasswordInput },
props: { props: {
user: { user: {
required: true, required: true,
@ -131,17 +129,25 @@ export default defineComponent({
const userStore = useUserStore(); const userStore = useUserStore();
const mainStore = useMainStore(); const mainStore = useMainStore();
const user_model = ref(props.user);
onBeforeMount(() => { onBeforeMount(() => {
void userStore.getRoles(false); void userStore.getRoles(false);
}); });
const isCurrentUser = computed(() => user_model.value.userid === mainStore.currentUser.userid); const password = ref('');
const newPassword = ref('');
const avatar = ref<File | FileList | string[]>();
const userModel = ref(props.user);
const canSetRoles = computed(() => hasPermission('users_set_roles')); const canSetRoles = computed(() => hasPermission('users_set_roles'));
const allRoles = computed(() => userStore.roles.map((role) => role.name));
const isCurrentUser = computed(() => userModel.value.userid === mainStore.currentUser.userid);
/* Reset model if props changed */
watch(
() => props.user,
() => (userModel.value = props.user)
);
const avatar = ref([] as string[]);
function onAvatarRejected() { function onAvatarRejected() {
Notify.create({ Notify.create({
group: false, group: false,
@ -151,84 +157,61 @@ export default defineComponent({
progress: true, progress: true,
actions: [{ icon: 'mdi-close', color: 'white' }], actions: [{ icon: 'mdi-close', color: 'white' }],
}); });
avatar.value = []; avatar.value = undefined;
} }
const allRoles = computed(() => userStore.roles.map((role) => role.name));
const password = ref('');
const new_password = ref('');
const new_password2 = ref('');
function save() { function save() {
let changed = user_model.value; let changed = userModel.value;
if (typeof changed.birthday === 'string') changed.birthday = new Date(changed.birthday); if (typeof changed.birthday === 'string') changed.birthday = new Date(changed.birthday);
changed = Object.assign(changed, { changed = Object.assign(changed, {
password: password.value, password: password.value,
}); });
if (new_password.value != '') { if (newPassword.value != '') {
changed = Object.assign(changed, { changed = Object.assign(changed, {
new_password: new_password.value, new_password: newPassword.value,
}); });
} }
emit('update:user', changed); emit('update:user', changed);
if (avatar.value && (avatar.value.length > 0 || avatar.value instanceof File)) if (avatar.value)
userStore.uploadAvatar(changed, avatar.value[0]).catch((response: Response) => { userStore
if (response && response.status == 400) { .uploadAvatar(changed, avatar.value instanceof File ? avatar.value : avatar.value[0])
onAvatarRejected(); .catch((response: Response) => {
} if (response && response.status == 400) {
}); onAvatarRejected();
}
});
reset(); reset();
} }
function reset() { function reset() {
user_model.value = props.user; userModel.value = props.user;
password.value = ''; password.value = '';
new_password.value = ''; newPassword.value = '';
new_password2.value = '';
} }
function samePassword(val: string) { function isFreeUID(val: string) {
return val == new_password.value || 'Passwörter sind nicht identisch!';
}
function notEmpty(val: string) {
return !!val || 'Feld darf nicht leer sein!';
}
function isEmail(val: string | null) {
return ( return (
!val || /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w\w+)+$/.test(val) || 'E-Mail ist nicht valide.' userStore.users.findIndex((user) => user.userid === val) === -1 ||
);
}
function isUseridUsed(val: string) {
return (
!userStore.users.find((user: FG.User) => {
return user.userid == val;
}) ||
!props.newUser ||
'Benutzername ist schon vergeben' 'Benutzername ist schon vergeben'
); );
} }
return { return {
avatar,
user_model,
onAvatarRejected,
allRoles, allRoles,
avatar,
canSetRoles, canSetRoles,
password,
new_password,
new_password2,
samePassword,
isCurrentUser, isCurrentUser,
isEmail, isEmail,
isFreeUID,
newPassword,
notEmpty, notEmpty,
isUseridUsed, onAvatarRejected,
save, password,
reset, reset,
save,
userModel,
}; };
}, },
}); });

View File

@ -22,15 +22,15 @@
/> />
</q-card-section> </q-card-section>
<q-separator /> <q-separator />
<q-card-section v-if="role" class="fit row justify-start content-center items-center"> <q-card-section v-if="role">
<q-scroll-area style="height: 20em; width: 100%"> <q-input v-if="role.id !== -1" v-model="newRoleName" filled label="neuer Name" />
<q-input v-if="role.id != -1" v-model="newRoleName" filled label="neuer Name" /> <q-scroll-area style="height: 40vh; width: 100%" class="background-like-input">
<q-option-group <q-option-group
:model-value="role.permissions" :model-value="role.permissions"
:options="permissions" :options="permissions"
color="primary" color="primary"
type="checkbox" type="checkbox"
@input="updatePermissions" @update:modelValue="updatePermissions"
/> />
</q-scroll-area> </q-scroll-area>
</q-card-section> </q-card-section>
@ -143,3 +143,18 @@ export default defineComponent({
}, },
}); });
</script> </script>
<style lang="sass" scoped>
// Same colors like qinput with filled attribute set
.body--light .background-like-input
background-color: rgba(0, 0, 0, 0.05)
&:hover
background: rgba(0,0,0,.1)
border-bottom: 1px solid rgba(0, 0, 0, 0.42)
.body--dark .background-like-input
background-color: rgba(255, 255, 255, 0.07)
&:hover
background: rgba(255, 255, 255, 0.14)
border-bottom: 1px solid #fff
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
<q-tabs v-if="$q.screen.gt.sm" v-model="tab"> <q-tabs v-if="$q.screen.gt.sm" v-model="tab">
<q-tab <q-tab
v-for="(tabindex, index) in tabs" v-for="(tabindex, index) in tabs"
@ -24,25 +24,23 @@
</q-item> </q-item>
</q-list> </q-list>
</q-drawer> </q-drawer>
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm"> <q-tab-panels
<q-tab-panels v-model="tab"
v-model="tab" style="background-color: transparent"
style="background-color: transparent" class="q-ma-none q-pa-none fit row justify-center content-start items-start"
class="q-ma-none q-pa-none fit row justify-center content-start items-start" animated
animated >
> <q-tab-panel name="user">
<q-tab-panel name="user"> <UpdateUser />
<UpdateUser /> </q-tab-panel>
</q-tab-panel> <q-tab-panel name="newUser">
<q-tab-panel name="newUser"> <NewUser />
<NewUser /> </q-tab-panel>
</q-tab-panel> <q-tab-panel name="roles">
<q-tab-panel name="roles"> <RoleSettings v-if="canEditRoles" />
<RoleSettings v-if="canEditRoles" /> </q-tab-panel>
</q-tab-panel> </q-tab-panels>
</q-tab-panels> </q-page>
</q-page>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">

View File

@ -64,7 +64,7 @@ export const useUserStore = defineStore({
return data; return data;
}, },
async uploadAvatar(user: FG.User, file: string) { async uploadAvatar(user: FG.User, file: string | File) {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
await api.post(`/users/${user.userid}/avatar`, formData, { await api.post(`/users/${user.userid}/avatar`, formData, {

View File

@ -15,3 +15,9 @@ export function stringIsTime(val: string) {
export function stringIsDateTime(val: string) { 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.'; 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.'
);
}

841
yarn.lock

File diff suppressed because it is too large Load Diff