Merge remote-tracking branch 'origin/develop' into feature/pricelist
This commit is contained in:
commit
741216ac3e
|
@ -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.
|
75
README.md
75
README.md
|
@ -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
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Plugins
|
||||
### Build a Plugin
|
||||
### Configure Plugins
|
||||
|
||||
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`.
|
||||
It needs a `plugin.ts` File which exports a plugin with the following interface:
|
||||
|
||||
```
|
||||
name: string;
|
||||
mainRoutes?: PluginRouteConfig[];
|
||||
outRoutes?: PluginRouteConfig[];
|
||||
store?: Map<string, Module<any, StateInterface>>;
|
||||
requiredModules: string[];
|
||||
version: string;
|
||||
```
|
||||
|
||||
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).
|
||||
-->
|
||||
|
|
50
package.json
50
package.json
|
@ -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,
|
||||
"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": {
|
||||
"format": "prettier --config ./package.json --write '{,!(node_modules)/**/}*.ts'",
|
||||
"lint": "eslint --ext .js,.ts,.vue ./src"
|
||||
|
@ -12,32 +17,31 @@
|
|||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"cordova": "^10.0.0",
|
||||
"core-js": "^3.9.1",
|
||||
"pinia": "^2.0.0-alpha.7",
|
||||
"pinia": "^2.0.0-alpha.10",
|
||||
"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": {
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"printWidth": 100,
|
||||
"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": [
|
||||
"last 10 Chrome versions",
|
||||
"last 10 Firefox versions",
|
||||
|
|
|
@ -37,16 +37,16 @@ module.exports = configure(function (/* ctx */) {
|
|||
|
||||
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||
extras: [
|
||||
// 'ionicons-v4',
|
||||
'mdi-v5',
|
||||
// 'fontawesome-v5',
|
||||
// 'eva-icons',
|
||||
// 'themify',
|
||||
// 'fontawesome-v5',
|
||||
// 'ionicons-v4',
|
||||
// 'line-awesome',
|
||||
// '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
|
||||
'material-icons', // optional, you are not bound to it
|
||||
],
|
||||
|
||||
// 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) {
|
||||
chain.plugin('eslint-webpack-plugin')
|
||||
.use(ESLintPlugin, [{
|
||||
extensions: [ 'js', 'vue' ],
|
||||
extensions: [ 'ts', 'js', 'vue' ],
|
||||
exclude: 'node_modules'
|
||||
}])
|
||||
},
|
||||
|
@ -90,7 +90,7 @@ module.exports = configure(function (/* ctx */) {
|
|||
|
||||
// https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework
|
||||
framework: {
|
||||
iconSet: 'material-icons', // Quasar icon set
|
||||
iconSet: 'mdi-v5', // Quasar icon set
|
||||
lang: 'de', // Quasar language pack
|
||||
config: {
|
||||
dark: 'auto',
|
||||
|
@ -134,7 +134,7 @@ module.exports = configure(function (/* ctx */) {
|
|||
manifest: {
|
||||
name: 'Flaschengeist',
|
||||
short_name: 'Flaschengeist',
|
||||
description: 'Dynamischen Managementsystem für Studentenclubs',
|
||||
description: 'Modular student club administration system',
|
||||
display: 'standalone',
|
||||
orientation: 'portrait',
|
||||
background_color: '#ffffff',
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
:placeholder="placeholder"
|
||||
:rules="customRules"
|
||||
:clearable="clearable"
|
||||
v-bind="attrs"
|
||||
@clear="dateTime = ''"
|
||||
>
|
||||
<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-date v-model="date" mask="YYYY-MM-DD">
|
||||
<div class="row items-center justify-end">
|
||||
|
@ -58,7 +59,7 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
emits: { 'update:modelValue': (date?: Date) => !!date || !date },
|
||||
setup(props, { emit }) {
|
||||
setup(props, { emit, attrs }) {
|
||||
const customRules = computed(() => [
|
||||
props.type == 'date' ? stringIsDate : props.type == 'time' ? stringIsTime : stringIsDateTime,
|
||||
...props.rules,
|
||||
|
@ -138,12 +139,13 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
return {
|
||||
attrs,
|
||||
clearable,
|
||||
date,
|
||||
time,
|
||||
dateTime,
|
||||
customRules,
|
||||
date,
|
||||
dateTime,
|
||||
placeholder,
|
||||
time,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -1,15 +1,8 @@
|
|||
<template>
|
||||
<q-layout view="hHh lpr lFf">
|
||||
<q-layout view="hHh Lpr lff">
|
||||
<q-header elevated class="bg-primary text-white">
|
||||
<q-toolbar>
|
||||
<q-btn
|
||||
v-if="!leftDrawerOpen"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
icon="menu"
|
||||
@click="leftDrawerOpen = !leftDrawerOpen"
|
||||
/>
|
||||
<q-btn dense flat round icon="mdi-menu" @click="openMenu" />
|
||||
|
||||
<q-toolbar-title>
|
||||
<router-link :to="{ name: 'dashboard' }" style="text-decoration: none; color: inherit">
|
||||
|
@ -21,7 +14,7 @@
|
|||
</q-toolbar-title>
|
||||
|
||||
<!-- 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-menu style="max-height: 400px; overflow: auto">
|
||||
<q-btn
|
||||
|
@ -50,12 +43,11 @@
|
|||
</q-header>
|
||||
|
||||
<q-drawer
|
||||
v-model="leftDrawerOpen"
|
||||
show-if-above
|
||||
v-model="leftDrawer"
|
||||
side="left"
|
||||
bordered
|
||||
:mini="leftDrawerMini"
|
||||
@click.capture="leftDrawerClicker"
|
||||
@click.capture="openMenu"
|
||||
>
|
||||
<!-- Plugins -->
|
||||
<q-list>
|
||||
|
@ -66,28 +58,13 @@
|
|||
/>
|
||||
<q-separator />
|
||||
<!-- Plugin functions -->
|
||||
|
||||
<essential-link
|
||||
v-for="(entry, index) in subLinks"
|
||||
:key="'childPlugin' + index"
|
||||
:entry="entry"
|
||||
/>
|
||||
</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 />
|
||||
|
||||
<essential-link
|
||||
v-for="(entry, index) in essentials"
|
||||
:key="'essential' + index"
|
||||
|
@ -126,7 +103,7 @@ export default defineComponent({
|
|||
const router = useRouter();
|
||||
const mainStore = useMainStore();
|
||||
const flaschengeist = inject<FG_Plugin.Flaschengeist>('flaschengeist');
|
||||
const leftDrawer = ref(false);
|
||||
const leftDrawer = ref(true);
|
||||
const leftDrawerMini = ref(false);
|
||||
const shortcuts = flaschengeist?.shortcuts || [];
|
||||
const mainLinks = flaschengeist?.menuLinks || [];
|
||||
|
@ -135,22 +112,27 @@ export default defineComponent({
|
|||
const useNative = 'Notification' in window && window.Notification !== undefined;
|
||||
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 matched = router.currentRoute.value.matched[1];
|
||||
return flaschengeist?.menuLinks.find((link) => matched.name == link.link)?.children;
|
||||
});
|
||||
|
||||
function leftDrawerClicker() {
|
||||
if (leftDrawerMini.value) {
|
||||
leftDrawerMini.value = false;
|
||||
onBeforeMount(() => {
|
||||
polling.value = window.setInterval(() => pollNotification(), config.pollingInterval);
|
||||
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() {
|
||||
polling.value = window.setInterval(() => {
|
||||
void mainStore
|
||||
.loadNotifications(<FG_Plugin.Flaschengeist>flaschengeist)
|
||||
.then((notifications) => {
|
||||
if (useNative && !noPermission.value)
|
||||
notifications.forEach(
|
||||
(notif) =>
|
||||
new window.Notification(notif.text, {
|
||||
timestamp: notif.time.getTime(),
|
||||
})
|
||||
);
|
||||
});
|
||||
}, config.pollingInterval);
|
||||
void mainStore
|
||||
.loadNotifications(<FG_Plugin.Flaschengeist>flaschengeist)
|
||||
.then((notifications) => {
|
||||
if (useNative && !noPermission.value)
|
||||
notifications.forEach(
|
||||
(notif) =>
|
||||
new window.Notification(notif.text, {
|
||||
timestamp: notif.time.getTime(),
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
essentials,
|
||||
leftDrawerOpen,
|
||||
leftDrawer,
|
||||
leftDrawerMini,
|
||||
leftDrawerClicker,
|
||||
logout,
|
||||
mainLinks,
|
||||
notifications,
|
||||
noPermission,
|
||||
openMenu,
|
||||
remove,
|
||||
requestPermission,
|
||||
shortcuts,
|
||||
|
|
|
@ -6,20 +6,21 @@
|
|||
</q-toolbar>
|
||||
|
||||
<q-card-section>
|
||||
<q-form ref="LoginForm" class="q-gutter-md" @submit="doLogin">
|
||||
<q-form class="q-gutter-md" @submit="doLogin">
|
||||
<q-input
|
||||
v-model="userid"
|
||||
filled
|
||||
label="Benutzername oder E-Mail"
|
||||
:rules="rules"
|
||||
autocomplete="username"
|
||||
:rules="[notEmpty]"
|
||||
tabindex="1"
|
||||
/>
|
||||
<q-input
|
||||
<password-input
|
||||
v-model="password"
|
||||
filled
|
||||
type="password"
|
||||
label="Password"
|
||||
:rules="rules"
|
||||
label="Passwort"
|
||||
autocomplete="cureent-password"
|
||||
:rules="[notEmpty]"
|
||||
tabindex="2"
|
||||
/>
|
||||
<div class="row justify-between">
|
||||
|
@ -54,15 +55,18 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useMainStore } from 'src/stores';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Loading, Notify } from 'quasar';
|
||||
import { useMainStore } from 'src/stores';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import { setBaseURL, api } from 'boot/axios';
|
||||
import { notEmpty } from 'src/utils/validators';
|
||||
import { useUserStore } from 'src/plugins/user/store';
|
||||
import PasswordInput from 'src/components/utils/PasswordInput.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Login',
|
||||
components: { PasswordInput },
|
||||
setup() {
|
||||
const mainStore = useMainStore();
|
||||
const mainRoute = { name: 'dashboard' };
|
||||
|
@ -71,7 +75,6 @@ export default defineComponent({
|
|||
/* Stuff for the real login page */
|
||||
const userid = 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 visible = ref(false);
|
||||
|
||||
|
@ -138,15 +141,15 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
return {
|
||||
userid,
|
||||
password,
|
||||
changeUrl,
|
||||
doLogin,
|
||||
doReset,
|
||||
rules,
|
||||
server,
|
||||
changeUrl,
|
||||
visible,
|
||||
notEmpty,
|
||||
openServerSettings,
|
||||
password,
|
||||
server,
|
||||
userid,
|
||||
visible,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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-tab
|
||||
v-for="(tabindex, index) in tabs"
|
||||
|
@ -37,32 +37,30 @@
|
|||
</div>
|
||||
</q-list>
|
||||
</q-drawer>
|
||||
<q-page padding class="fit row justify-left q-col-gutter-sm">
|
||||
<q-tab-panels
|
||||
v-model="tab"
|
||||
style="background-color: transparent"
|
||||
class="q-pa-none col-12"
|
||||
animated
|
||||
>
|
||||
<q-tab-panel name="add" class="q-px-xs">
|
||||
<BalanceAdd
|
||||
@open-history="
|
||||
showDrawer = !showDrawer;
|
||||
show = true;
|
||||
"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="transfer" class="q-px-xs">
|
||||
<BalanceTransfer
|
||||
@open-history="
|
||||
showDrawer = !showDrawer;
|
||||
show = true;
|
||||
"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-page>
|
||||
</div>
|
||||
<q-tab-panels
|
||||
v-model="tab"
|
||||
style="background-color: transparent"
|
||||
class="q-pa-none col-12"
|
||||
animated
|
||||
>
|
||||
<q-tab-panel name="add" class="q-px-xs">
|
||||
<BalanceAdd
|
||||
@open-history="
|
||||
showDrawer = !showDrawer;
|
||||
show = true;
|
||||
"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="transfer" class="q-px-xs">
|
||||
<BalanceTransfer
|
||||
@open-history="
|
||||
showDrawer = !showDrawer;
|
||||
show = true;
|
||||
"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<q-card style="text-align: center">
|
||||
<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">
|
||||
<q-avatar style="position: absolute; top: 0; left: 0; width: 100%; height: 100%">
|
||||
<img :src="avatarLink" />
|
||||
<img :src="avatarLink" :onerror="error" />
|
||||
</q-avatar>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -39,9 +39,14 @@ export default defineComponent({
|
|||
// Ensure users are loaded,so we can query birthdays
|
||||
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);
|
||||
|
||||
function error() {
|
||||
avatar.value = false;
|
||||
}
|
||||
|
||||
function userHasBirthday(user: FG.User) {
|
||||
const today = new Date();
|
||||
return (
|
||||
|
@ -61,7 +66,7 @@ export default defineComponent({
|
|||
.filter((user) => user.userid !== mainStore.currentUser.userid)
|
||||
);
|
||||
|
||||
return { avatarLink, name, hasBirthday, birthday };
|
||||
return { avatar, avatarLink, error, name, hasBirthday, birthday };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -2,44 +2,48 @@
|
|||
<q-form @submit="save" @reset="reset">
|
||||
<q-card-section class="fit row justify-start content-center items-center">
|
||||
<q-input
|
||||
v-model="user_model.firstname"
|
||||
v-model="userModel.firstname"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
label="Vorname"
|
||||
:rules="[notEmpty]"
|
||||
autocomplete="given-name"
|
||||
filled
|
||||
/>
|
||||
<q-input
|
||||
v-model="user_model.lastname"
|
||||
v-model="userModel.lastname"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
label="Nachname"
|
||||
:rules="[notEmpty]"
|
||||
autocomplete="family-name"
|
||||
filled
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-model="user_model.display_name"
|
||||
v-model="userModel.display_name"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
label="Angezeigter Name"
|
||||
:rules="[notEmpty]"
|
||||
autocomplete="nickname"
|
||||
filled
|
||||
/>
|
||||
<q-input
|
||||
v-model="user_model.mail"
|
||||
v-model="userModel.mail"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
label="E-Mail"
|
||||
:rules="[isEmail, notEmpty]"
|
||||
autocomplete="email"
|
||||
filled
|
||||
/>
|
||||
<q-input
|
||||
v-model="user_model.userid"
|
||||
v-model="userModel.userid"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
label="Benutzername"
|
||||
:readonly="!newUser"
|
||||
:rules="[isUseridUsed, notEmpty]"
|
||||
:rules="newUser ? [isFreeUID, notEmpty] : []"
|
||||
autocomplete="username"
|
||||
filled
|
||||
/>
|
||||
<q-select
|
||||
v-model="user_model.roles"
|
||||
v-model="userModel.roles"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
label="Rollen"
|
||||
filled
|
||||
|
@ -51,9 +55,10 @@
|
|||
option-value="name"
|
||||
/>
|
||||
<IsoDateInput
|
||||
v-model="user_model.birthday"
|
||||
v-model="userModel.birthday"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
label="Geburtstag"
|
||||
autocomplete="bday"
|
||||
/>
|
||||
<q-file
|
||||
v-model="avatar"
|
||||
|
@ -72,31 +77,22 @@
|
|||
</q-card-section>
|
||||
<q-separator v-if="!newUser" />
|
||||
<q-card-section v-if="!newUser" class="fit row justify-start content-center items-center">
|
||||
<q-input
|
||||
<PasswordInput
|
||||
v-if="isCurrentUser"
|
||||
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]"
|
||||
filled
|
||||
label="Passwort"
|
||||
autocomplete="current-password"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
hint="Passwort muss immer eingetragen werden"
|
||||
/>
|
||||
<q-input
|
||||
v-model="new_password"
|
||||
class="col-xs-12 col-sm-6 col-md-4 q-pa-sm"
|
||||
<PasswordInput
|
||||
v-model="newPassword"
|
||||
filled
|
||||
label="Neues Password"
|
||||
type="password"
|
||||
filled
|
||||
/>
|
||||
<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
|
||||
autocomplete="new-password"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
|
@ -109,14 +105,16 @@
|
|||
<script lang="ts">
|
||||
import { Notify } from 'quasar';
|
||||
import { hasPermission } from 'src/utils/permission';
|
||||
import { notEmpty, isEmail } from 'src/utils/validators';
|
||||
import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
|
||||
import { 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 { useMainStore } from 'src/stores';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MainUserSettings',
|
||||
components: { IsoDateInput },
|
||||
components: { IsoDateInput, PasswordInput },
|
||||
props: {
|
||||
user: {
|
||||
required: true,
|
||||
|
@ -131,17 +129,25 @@ export default defineComponent({
|
|||
const userStore = useUserStore();
|
||||
const mainStore = useMainStore();
|
||||
|
||||
const user_model = ref(props.user);
|
||||
|
||||
onBeforeMount(() => {
|
||||
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 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() {
|
||||
Notify.create({
|
||||
group: false,
|
||||
|
@ -151,84 +157,61 @@ export default defineComponent({
|
|||
progress: true,
|
||||
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() {
|
||||
let changed = user_model.value;
|
||||
let changed = userModel.value;
|
||||
if (typeof changed.birthday === 'string') changed.birthday = new Date(changed.birthday);
|
||||
changed = Object.assign(changed, {
|
||||
password: password.value,
|
||||
});
|
||||
if (new_password.value != '') {
|
||||
if (newPassword.value != '') {
|
||||
changed = Object.assign(changed, {
|
||||
new_password: new_password.value,
|
||||
new_password: newPassword.value,
|
||||
});
|
||||
}
|
||||
|
||||
emit('update:user', changed);
|
||||
|
||||
if (avatar.value && (avatar.value.length > 0 || avatar.value instanceof File))
|
||||
userStore.uploadAvatar(changed, avatar.value[0]).catch((response: Response) => {
|
||||
if (response && response.status == 400) {
|
||||
onAvatarRejected();
|
||||
}
|
||||
});
|
||||
if (avatar.value)
|
||||
userStore
|
||||
.uploadAvatar(changed, avatar.value instanceof File ? avatar.value : avatar.value[0])
|
||||
.catch((response: Response) => {
|
||||
if (response && response.status == 400) {
|
||||
onAvatarRejected();
|
||||
}
|
||||
});
|
||||
reset();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
user_model.value = props.user;
|
||||
userModel.value = props.user;
|
||||
password.value = '';
|
||||
new_password.value = '';
|
||||
new_password2.value = '';
|
||||
newPassword.value = '';
|
||||
}
|
||||
|
||||
function samePassword(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) {
|
||||
function isFreeUID(val: string) {
|
||||
return (
|
||||
!val || /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w\w+)+$/.test(val) || 'E-Mail ist nicht valide.'
|
||||
);
|
||||
}
|
||||
|
||||
function isUseridUsed(val: string) {
|
||||
return (
|
||||
!userStore.users.find((user: FG.User) => {
|
||||
return user.userid == val;
|
||||
}) ||
|
||||
!props.newUser ||
|
||||
userStore.users.findIndex((user) => user.userid === val) === -1 ||
|
||||
'Benutzername ist schon vergeben'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
avatar,
|
||||
user_model,
|
||||
onAvatarRejected,
|
||||
allRoles,
|
||||
avatar,
|
||||
canSetRoles,
|
||||
password,
|
||||
new_password,
|
||||
new_password2,
|
||||
samePassword,
|
||||
isCurrentUser,
|
||||
isEmail,
|
||||
isFreeUID,
|
||||
newPassword,
|
||||
notEmpty,
|
||||
isUseridUsed,
|
||||
save,
|
||||
onAvatarRejected,
|
||||
password,
|
||||
reset,
|
||||
save,
|
||||
userModel,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -22,15 +22,15 @@
|
|||
/>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section v-if="role" class="fit row justify-start content-center items-center">
|
||||
<q-scroll-area style="height: 20em; width: 100%">
|
||||
<q-input v-if="role.id != -1" v-model="newRoleName" filled label="neuer Name" />
|
||||
<q-card-section v-if="role">
|
||||
<q-input v-if="role.id !== -1" v-model="newRoleName" filled label="neuer Name" />
|
||||
<q-scroll-area style="height: 40vh; width: 100%" class="background-like-input">
|
||||
<q-option-group
|
||||
:model-value="role.permissions"
|
||||
:options="permissions"
|
||||
color="primary"
|
||||
type="checkbox"
|
||||
@input="updatePermissions"
|
||||
@update:modelValue="updatePermissions"
|
||||
/>
|
||||
</q-scroll-area>
|
||||
</q-card-section>
|
||||
|
@ -143,3 +143,18 @@ export default defineComponent({
|
|||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
// Same colors like qinput with filled attribute set
|
||||
.body--light .background-like-input
|
||||
background-color: rgba(0, 0, 0, 0.05)
|
||||
&:hover
|
||||
background: rgba(0,0,0,.1)
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.42)
|
||||
|
||||
.body--dark .background-like-input
|
||||
background-color: rgba(255, 255, 255, 0.07)
|
||||
&:hover
|
||||
background: rgba(255, 255, 255, 0.14)
|
||||
border-bottom: 1px solid #fff
|
||||
</style>
|
|
@ -1,5 +1,5 @@
|
|||
<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-tab
|
||||
v-for="(tabindex, index) in tabs"
|
||||
|
@ -24,25 +24,23 @@
|
|||
</q-item>
|
||||
</q-list>
|
||||
</q-drawer>
|
||||
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
|
||||
<q-tab-panels
|
||||
v-model="tab"
|
||||
style="background-color: transparent"
|
||||
class="q-ma-none q-pa-none fit row justify-center content-start items-start"
|
||||
animated
|
||||
>
|
||||
<q-tab-panel name="user">
|
||||
<UpdateUser />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="newUser">
|
||||
<NewUser />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="roles">
|
||||
<RoleSettings v-if="canEditRoles" />
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-page>
|
||||
</div>
|
||||
<q-tab-panels
|
||||
v-model="tab"
|
||||
style="background-color: transparent"
|
||||
class="q-ma-none q-pa-none fit row justify-center content-start items-start"
|
||||
animated
|
||||
>
|
||||
<q-tab-panel name="user">
|
||||
<UpdateUser />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="newUser">
|
||||
<NewUser />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="roles">
|
||||
<RoleSettings v-if="canEditRoles" />
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -64,7 +64,7 @@ export const useUserStore = defineStore({
|
|||
return data;
|
||||
},
|
||||
|
||||
async uploadAvatar(user: FG.User, file: string) {
|
||||
async uploadAvatar(user: FG.User, file: string | File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
await api.post(`/users/${user.userid}/avatar`, formData, {
|
||||
|
|
|
@ -15,3 +15,9 @@ export function stringIsTime(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.';
|
||||
}
|
||||
|
||||
export function isEmail(val: string) {
|
||||
return (
|
||||
!val || /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w\w+)+$/.test(val) || 'E-Mail ist nicht gültig.'
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue