release v2.0.0 #4

Merged
crimsen merged 481 commits from develop into master 2024-01-18 15:15:08 +00:00
26 changed files with 375 additions and 290 deletions
Showing only changes of commit 663d1d3e4d - Show all commits

View File

@ -24,5 +24,6 @@
"sortAttributes": false
}
},
"vetur.format.defaultFormatter.ts": "prettier-tslint"
"vetur.format.defaultFormatter.ts": "prettier-tslint",
"typescript.format.enable": false
}

View File

@ -9,6 +9,6 @@ export default defineComponent({
name: 'EmptyParent',
setup() {
return {};
},
}
});
</script>

View File

@ -49,18 +49,14 @@ export default defineComponent({
if (props.title.includes('loadFromStore')) {
const startIndex = props.title.indexOf('(') + 1;
const endIndex = props.title.indexOf(')');
const substring = props.title
.substring(startIndex, endIndex)
.replace(/"/g, '');
const substring = props.title.substring(startIndex, endIndex).replace(/"/g, '');
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return <string>root.$store.getters[substring];
}
return props.title;
});
const isGranted = computed(() =>
hasPermissions(props.permissions || [], root.$store)
);
const isGranted = computed(() => hasPermissions(props.permissions || [], root.$store));
return { realTitle: title, isGranted };
}

View File

@ -22,9 +22,7 @@ export default defineComponent({
}
},
setup(props, { root }) {
const isGranted = computed(() =>
hasPermissions(props.permissions || [], root.$store)
);
const isGranted = computed(() => hasPermissions(props.permissions || [], root.$store));
return { isGranted };
}
});

View File

@ -49,18 +49,18 @@ export default defineComponent({
name: 'IsoDateInput',
props: {
value: {
required: true,
required: true
},
label: {},
readonly: {
default: false,
default: false
},
type: {
default: 'date',
validator: function(value: string) {
return ['date', 'time', 'datetime'].indexOf(value) !== -1;
},
},
}
}
},
setup(props: Props, { emit }: { emit: any }) {
function getDateTime() {
@ -183,8 +183,8 @@ export default defineComponent({
rules,
timeChanged,
placeholder,
dateTimeChanged,
dateTimeChanged
};
},
}
});
</script>

View File

@ -112,23 +112,23 @@ const links = [
name: 'about',
title: 'Über Flaschengeist',
link: 'about',
icon: 'mdi-information',
},
icon: 'mdi-information'
}
];
const shortcuts = [
{
link: 'about',
icon: 'mdi-information',
icon: 'mdi-information'
},
{
link: 'user',
icon: 'mdi-account',
icon: 'mdi-account'
},
{
link: 'user-plugin1',
icon: 'mdi-account-plus',
},
icon: 'mdi-account-plus'
}
];
declare module 'vue/types/vue' {
@ -146,7 +146,7 @@ export default defineComponent({
const leftDrawerOpen = ref(
computed({
get: () => (leftDrawer.value || Screen.gt.sm ? true : false),
set: (val: boolean) => (leftDrawer.value = val),
set: (val: boolean) => (leftDrawer.value = val)
})
);
const leftDrawerMini = ref(false);
@ -177,9 +177,7 @@ export default defineComponent({
function logout() {
Loading.show({ message: 'Session wird abgemeldet' });
(<Store<StateInterface>>ctx.root.$store)
.dispatch('session/logout')
.finally(() => {
(<Store<StateInterface>>ctx.root.$store).dispatch('session/logout').finally(() => {
Loading.hide();
});
}
@ -191,8 +189,8 @@ export default defineComponent({
links,
pluginChildLinks,
shortcuts,
logout,
logout
};
},
}
});
</script>

View File

@ -49,6 +49,6 @@ import ShortCutLink from 'components/navigation/ShortCutLink.vue';
export default defineComponent({
name: 'OutLayout',
components: { ShortCutLink },
components: { ShortCutLink }
});
</script>

View File

@ -4,11 +4,7 @@
style="grid-auto-rows: 1fr;"
class="fit row justify-around items-start q-col-gutter-sm"
>
<div
v-for="(item, index) in widgets"
:key="index"
class="col-4 full-height col-sm-6 col-xs-12"
>
<div v-for="(item, index) in widgets" :key="index" class="col-4 full-height col-sm-6 col-xs-12">
<component v-bind:is="item" />
</div>
</q-page>
@ -25,15 +21,14 @@ export default defineComponent({
const widgets = ref<Array<AsyncComponentPromise>>([]);
onMounted(() => {
root.$flaschengeistPlugins.widgets.forEach((widget) => {
if (hasPermissions(widget.permissions, root.$store))
widgets.value.push(widget.widget);
root.$flaschengeistPlugins.widgets.forEach(widget => {
if (hasPermissions(widget.permissions, root.$store)) widgets.value.push(widget.widget);
});
});
return {
widgets,
widgets
};
},
}
});
</script>

View File

@ -12,14 +12,16 @@
fill="white"
>
<g>
<circle
cx="87.493"
cy="25.907"
r="25.907"
<circle cx="87.493" cy="25.907" r="25.907" />
<path
d="m194.386,209.531l-42.802-36.895c-1.825-1.573-4.001-2.683-6.345-3.237l-72.533-17.136 47.755,.703v-34.439l-46.525-26.18 50.36,14.829c4.016,1.182 8.356,0.276 11.564-2.414l38.264-32.093c1.1-0.923 2.001-1.994 2.698-3.161 2.656-4.442 2.36-10.26-1.154-14.449-4.437-5.29-12.32-5.981-17.61-1.544l-33.127,27.785-45.605-13.428 41.402,1.292 16.027-13.442-6.021-2.955-3.631,3.945-18.134,2.86h-37.216c-9.665,0-17.501,7.835-17.501,17.501v90.395l.029-.024c0.252,6.569 4.819,12.433 11.527,14.019l68.966,16.292 40.024,34.501c2.834,2.442 6.318,3.639 9.787,3.639 4.213,0 8.402-1.765 11.368-5.206 5.41-6.278 4.708-15.75-1.567-21.158z"
/>
<path
d="m233.888,50.21l-43.49-21.349c-2.17-1.065-4.545-1.612-6.94-1.612-0.861,0-1.724,0.071-2.581,0.213l-13.243,2.2-24.213-11.886c-6.197-3.043-13.688-0.485-16.728,5.713-3.042,6.197-0.484,13.687 5.713,16.729l14.402,7.069 3.539-2.969c4.405-3.694 9.994-5.729 15.738-5.729 7.266,0 14.11,3.192 18.777,8.756 6.702,7.991 7.61,19.371 2.258,28.32-0.529,0.884-1.127,1.717-1.759,2.523l28.035,13.762c0.916,0.45 1.914,0.664 2.967,0.664 5.79,0 13.263-6.495 18.05-16.247 5.66-11.524 5.424-23.236-0.525-26.157z"
/>
<path
d="m102.363,202.426l2.531,6.9-13.835,65.324c-1.716,8.105 3.463,16.065 11.567,17.782 1.048,0.222 2.092,0.328 3.122,0.328 6.936-0.001 13.165-4.839 14.66-11.896l14.265-67.357-5.513-4.752-26.797-6.329z"
/>
<path d="m194.386,209.531l-42.802-36.895c-1.825-1.573-4.001-2.683-6.345-3.237l-72.533-17.136 47.755,.703v-34.439l-46.525-26.18 50.36,14.829c4.016,1.182 8.356,0.276 11.564-2.414l38.264-32.093c1.1-0.923 2.001-1.994 2.698-3.161 2.656-4.442 2.36-10.26-1.154-14.449-4.437-5.29-12.32-5.981-17.61-1.544l-33.127,27.785-45.605-13.428 41.402,1.292 16.027-13.442-6.021-2.955-3.631,3.945-18.134,2.86h-37.216c-9.665,0-17.501,7.835-17.501,17.501v90.395l.029-.024c0.252,6.569 4.819,12.433 11.527,14.019l68.966,16.292 40.024,34.501c2.834,2.442 6.318,3.639 9.787,3.639 4.213,0 8.402-1.765 11.368-5.206 5.41-6.278 4.708-15.75-1.567-21.158z" />
<path d="m233.888,50.21l-43.49-21.349c-2.17-1.065-4.545-1.612-6.94-1.612-0.861,0-1.724,0.071-2.581,0.213l-13.243,2.2-24.213-11.886c-6.197-3.043-13.688-0.485-16.728,5.713-3.042,6.197-0.484,13.687 5.713,16.729l14.402,7.069 3.539-2.969c4.405-3.694 9.994-5.729 15.738-5.729 7.266,0 14.11,3.192 18.777,8.756 6.702,7.991 7.61,19.371 2.258,28.32-0.529,0.884-1.127,1.717-1.759,2.523l28.035,13.762c0.916,0.45 1.914,0.664 2.967,0.664 5.79,0 13.263-6.495 18.05-16.247 5.66-11.524 5.424-23.236-0.525-26.157z" />
<path d="m102.363,202.426l2.531,6.9-13.835,65.324c-1.716,8.105 3.463,16.065 11.567,17.782 1.048,0.222 2.092,0.328 3.122,0.328 6.936-0.001 13.165-4.839 14.66-11.896l14.265-67.357-5.513-4.752-26.797-6.329z" />
</g>
</svg>
</div>
@ -27,7 +29,8 @@
Der Admin is über's Kabel gestolpert!
</div>
<div>
Aktuell kann der Backend Server nicht erreicht werden, wir versuchen es in {{reload}} Sekunden erneut.
Aktuell kann der Backend Server nicht erreicht werden, wir versuchen es in
{{ reload }} Sekunden erneut.
</div>
</div>
</div>
@ -49,6 +52,6 @@ export default defineComponent({
}, 1000);
onUnmounted(() => clearInterval(ival));
return { reload };
},
}
});
</script>

View File

@ -2,12 +2,26 @@
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
<div>
<div>
<svg style="max-width: 400px;" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 292.761 292.761" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 292.761 292.761" fill="white">
<svg
style="max-width: 400px;"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 292.761 292.761"
xmlns:xlink="http://www.w3.org/1999/xlink"
enable-background="new 0 0 292.761 292.761"
fill="white"
>
<g>
<circle cx="87.493" cy="25.907" r="25.907"/>
<path d="m194.386,209.531l-42.802-36.895c-1.825-1.573-4.001-2.683-6.345-3.237l-72.533-17.136 47.755,.703v-34.439l-46.525-26.18 50.36,14.829c4.016,1.182 8.356,0.276 11.564-2.414l38.264-32.093c1.1-0.923 2.001-1.994 2.698-3.161 2.656-4.442 2.36-10.26-1.154-14.449-4.437-5.29-12.32-5.981-17.61-1.544l-33.127,27.785-45.605-13.428 41.402,1.292 16.027-13.442-6.021-2.955-3.631,3.945-18.134,2.86h-37.216c-9.665,0-17.501,7.835-17.501,17.501v90.395l.029-.024c0.252,6.569 4.819,12.433 11.527,14.019l68.966,16.292 40.024,34.501c2.834,2.442 6.318,3.639 9.787,3.639 4.213,0 8.402-1.765 11.368-5.206 5.41-6.278 4.708-15.75-1.567-21.158z"/>
<path d="m233.888,50.21l-43.49-21.349c-2.17-1.065-4.545-1.612-6.94-1.612-0.861,0-1.724,0.071-2.581,0.213l-13.243,2.2-24.213-11.886c-6.197-3.043-13.688-0.485-16.728,5.713-3.042,6.197-0.484,13.687 5.713,16.729l14.402,7.069 3.539-2.969c4.405-3.694 9.994-5.729 15.738-5.729 7.266,0 14.11,3.192 18.777,8.756 6.702,7.991 7.61,19.371 2.258,28.32-0.529,0.884-1.127,1.717-1.759,2.523l28.035,13.762c0.916,0.45 1.914,0.664 2.967,0.664 5.79,0 13.263-6.495 18.05-16.247 5.66-11.524 5.424-23.236-0.525-26.157z"/>
<path d="m102.363,202.426l2.531,6.9-13.835,65.324c-1.716,8.105 3.463,16.065 11.567,17.782 1.048,0.222 2.092,0.328 3.122,0.328 6.936-0.001 13.165-4.839 14.66-11.896l14.265-67.357-5.513-4.752-26.797-6.329z"/>
<circle cx="87.493" cy="25.907" r="25.907" />
<path
d="m194.386,209.531l-42.802-36.895c-1.825-1.573-4.001-2.683-6.345-3.237l-72.533-17.136 47.755,.703v-34.439l-46.525-26.18 50.36,14.829c4.016,1.182 8.356,0.276 11.564-2.414l38.264-32.093c1.1-0.923 2.001-1.994 2.698-3.161 2.656-4.442 2.36-10.26-1.154-14.449-4.437-5.29-12.32-5.981-17.61-1.544l-33.127,27.785-45.605-13.428 41.402,1.292 16.027-13.442-6.021-2.955-3.631,3.945-18.134,2.86h-37.216c-9.665,0-17.501,7.835-17.501,17.501v90.395l.029-.024c0.252,6.569 4.819,12.433 11.527,14.019l68.966,16.292 40.024,34.501c2.834,2.442 6.318,3.639 9.787,3.639 4.213,0 8.402-1.765 11.368-5.206 5.41-6.278 4.708-15.75-1.567-21.158z"
/>
<path
d="m233.888,50.21l-43.49-21.349c-2.17-1.065-4.545-1.612-6.94-1.612-0.861,0-1.724,0.071-2.581,0.213l-13.243,2.2-24.213-11.886c-6.197-3.043-13.688-0.485-16.728,5.713-3.042,6.197-0.484,13.687 5.713,16.729l14.402,7.069 3.539-2.969c4.405-3.694 9.994-5.729 15.738-5.729 7.266,0 14.11,3.192 18.777,8.756 6.702,7.991 7.61,19.371 2.258,28.32-0.529,0.884-1.127,1.717-1.759,2.523l28.035,13.762c0.916,0.45 1.914,0.664 2.967,0.664 5.79,0 13.263-6.495 18.05-16.247 5.66-11.524 5.424-23.236-0.525-26.157z"
/>
<path
d="m102.363,202.426l2.531,6.9-13.835,65.324c-1.716,8.105 3.463,16.065 11.567,17.782 1.048,0.222 2.092,0.328 3.122,0.328 6.936-0.001 13.165-4.839 14.66-11.896l14.265-67.357-5.513-4.752-26.797-6.329z"
/>
</g>
</svg>
</div>
@ -15,20 +29,19 @@
Der Admin war betrunken!!
</div>
<div>
Einige Plugins konnten nicht geladen werden.<br />Sollte diese Seite jemals auftauchen, kontaktiere einen nüchternen Admin.
Einige Plugins konnten nicht geladen werden.<br />Sollte diese Seite jemals auftauchen,
kontaktiere einen nüchternen Admin.
</div>
</div>
</div>
</template>
<script lang="ts">
import {defineComponent} from '@vue/composition-api';
import { defineComponent } from '@vue/composition-api';
export default defineComponent({
name: 'PluginError.vue'
})
});
</script>
<style scoped>
</style>
<style scoped></style>

View File

@ -6,7 +6,7 @@
>
<div class="fit row justify-center content-center items-center">
<q-img
:src="$q.dark.isActive? 'logo.svg' : 'logo-dark.svg'"
:src="$q.dark.isActive ? 'logo.svg' : 'logo-dark.svg'"
class="col-12 q-ma-md"
style="min-width: 200px; max-width: 400px"
/>
@ -17,16 +17,14 @@
</div>
</div>
<div class="col-12 text-center q-ma-sm" style="max-width: 600px;">
Flaschengeist ist ein dynamischen Managementsystem für Studentenclubs.
Es ermöglicht unter anderem die Mitgliederverwaltung, Dienstverwaltung,
Arbeitsgruppenverwaltung und vieles meher. Es kann fast alles ermöglich
werden, wenn ein Plugin dafür geschrieben wird. Jeder Club hat die
Möglichkeit sein eigenes Flaschengeist zu hosten. Ziel ist später
Flaschengeist ist ein dynamischen Managementsystem für Studentenclubs. Es ermöglicht unter
anderem die Mitgliederverwaltung, Dienstverwaltung, Arbeitsgruppenverwaltung und vieles
meher. Es kann fast alles ermöglich werden, wenn ein Plugin dafür geschrieben wird. Jeder
Club hat die Möglichkeit sein eigenes Flaschengeist zu hosten. Ziel ist später
Clubübergreifend dezentralisiert miteinander zu arbeiten.
</div>
<q-separator/>
<q-separator />
<div class="col-12 text-h6 q-pa-sm" v-if="$route.name == 'about'">
Geladene Plugins:
</div>
@ -44,14 +42,12 @@
</q-chip>
</q-chip>
</div>
<q-separator/>
<q-separator />
<div class="col-12 text-h6 q-pa-sm">
Entwickler:
</div>
<div
class="fit row inline wrap justify-around items-start content-start"
>
<div class="fit row inline wrap justify-around items-start content-start">
<developer
v-for="(developer, index) in developers"
:key="'dev' + index"
@ -69,7 +65,7 @@
</template>
<script lang="ts">
import {defineComponent} from '@vue/composition-api';
import { defineComponent } from '@vue/composition-api';
import Developer from 'components/about/Developer.vue';
const developers = [
@ -105,9 +101,9 @@ const developers = [
];
export default defineComponent({
// name: 'PageName'
components: {Developer},
components: { Developer },
setup() {
return {developers};
return { developers };
}
});
</script>

View File

@ -1,24 +1,33 @@
<template>
<q-card>
<q-card-actions align="right">
<q-card v-bind:class="{ 'bg-grey': isReversed }">
<q-card-section class="row items-start justify-between">
<div class="col text-center">
<div
v-bind:class="{ 'text-negative': isNegative() }"
class="text-weight-bold"
style="font-size: 2em"
>
<span v-if="isNegative()">-</span>{{ transaction.amount.toFixed(2) }}&#8239;
</div>
<div>{{ text }}</div>
<div>{{ timeStr }}</div>
</div>
<div class="col" style="text-align: right">
<q-btn
:color="color()"
color="negative"
aria-label="Reverse transaction"
icon="mdi-trash-can"
aria-label="Löschen"
:disable="disabled()"
@click="reverse(transaction)"
square
:disable="!canReverse"
@click="reverse"
/>
</q-card-actions>
<q-card-section>
<span>{{ timeStr }}: {{ transaction.amount }}</span>
</div>
</q-card-section>
</q-card>
</template>
<script lang="ts">
// TODO: Better styling
import { ref, computed, defineComponent, onUnmounted } from '@vue/composition-api';
import { ref, computed, defineComponent, onUnmounted, onMounted } from '@vue/composition-api';
import { hasPermission } from 'src/utils/permission';
import { formatDateTime } from 'src/utils/datetime';
import { StateInterfaceBalance } from 'src/plugins/balance/store/balance';
@ -30,36 +39,60 @@ interface Props {
export default defineComponent({
name: 'Transaction',
props: ['transaction'],
props: {
transaction: {
type: Object,
required: true
}
},
watch: { transaction: 'refreshText' },
setup(props: Props, { root, emit }) {
const now = ref(Date.now());
const ival = setInterval(() => (now.value = Date.now()), 1000);
const store: Store<StateInterfaceBalance> = <Store<StateInterfaceBalance>>root.$store;
const text = ref('');
onUnmounted(() => clearInterval(ival));
onMounted(() => refreshText());
function canReverse(transaction: FG.Transaction) {
return (
hasPermission('balance_reversal', store) ||
(transaction.sender_id === store.state.user.currentUser?.userid &&
Date.now() - transaction.time.getTime() < 10000)
const isNegative = () => props.transaction.sender_id === store.state.user.currentUser?.userid;
const refreshText = async () => {
if (isNegative()) {
text.value = 'Anschreiben';
if (props.transaction.receiver_id !== null) {
const user = <FG.User>await store.dispatch('user/getUser', {
userid: props.transaction.receiver_id
});
text.value = `Gesendet an ${user.display_name}`;
}
} else {
text.value = 'Gutschrift';
if (props.transaction.sender_id !== null) {
const user = <FG.User>await store.dispatch('user/getUser', {
userid: props.transaction.sender_id
});
text.value = `Bekommen von ${user.display_name}`;
}
}
};
const isReversed = computed(() => props.transaction.reversal != undefined);
const canReverse = computed(
() =>
!isReversed.value &&
(hasPermission('balance_reversal', store) ||
(props.transaction.sender_id === store.state.user.currentUser?.userid &&
now.value - props.transaction.time.getTime() < 10000))
);
}
function color() {
return canReverse(props.transaction) ? 'negative' : 'grey';
}
function disabled() {
return !canReverse(props.transaction);
}
function reverse(transaction: FG.Transaction) {
if (canReverse(transaction))
function reverse() {
if (canReverse.value)
store
.dispatch('balance/revert', transaction)
.dispatch('balance/revert', props.transaction)
.then(() => {
emit('reversed', transaction.id);
emit('update:transaction', props.transaction);
})
.catch(error => console.log(error));
}
@ -70,7 +103,7 @@ export default defineComponent({
return formatDateTime(props.transaction.time, elapsed > 12 * 60 * 60, true, true) + ' Uhr';
});
return { timeStr, disabled, color, reverse };
return { timeStr, reverse, isNegative, text, refreshText, canReverse, isReversed };
}
});
</script>

View File

@ -56,8 +56,9 @@
</q-card-section>
</q-card>
</div>
<div v-for="(transaction, index) in transactions" v-bind:key="index" class="col-sm-4 col-xs-6">
<Transaction :transaction="transaction" @reversed="reversed" />
<div v-for="(transaction, index) in transactions" v-bind:key="index" class="col-md-4 col-sm-6">
<!-- TODO: In Vue3 use v-model:transaction="..." -->
<Transaction :transaction.sync="transactions[index]" />
</div>
</q-page>
</template>
@ -80,7 +81,7 @@ export default defineComponent({
const amount = ref<number>(0);
const showAddShortcut = ref(false);
const transactions = ref<FG.Transaction[]>([]);
const transactions = computed(() => store.state.balance.transactions.slice().reverse());
const user = ref(store.state.user.currentUser);
const shortCuts = ref(store.state.balance.shortcuts);
@ -89,7 +90,7 @@ export default defineComponent({
);
function addShortcut() {
void store.dispatch('balance/addShortcut', amount.value * -1);
if (amount.value != 0) void store.dispatch('balance/addShortcut', amount.value * -1);
}
function removeShortcut(shortcut: number) {
void store.dispatch('balance/removeShortcut', shortcut);
@ -100,19 +101,9 @@ export default defineComponent({
function changeBalance(amount: number) {
store
.dispatch('balance/changeBalance', { amount: amount, user: user.value?.userid })
.then((transaction: FG.Transaction) => {
if (transactions.value.length > 5) transactions.value.pop();
transaction.time = new Date(transaction.time);
transactions.value.unshift(transaction);
console.log(transactions.value);
})
.catch(err => console.log(err));
}
function reversed(id: number) {
transactions.value = transactions.value.filter(t => t.id != id);
}
return {
user,
addShortcut,
@ -123,7 +114,6 @@ export default defineComponent({
amount,
showSelector,
shortCuts,
reversed,
userUpdated
};
}

View File

@ -40,7 +40,12 @@ export default defineComponent({
align: 'left',
sortable: true
},
{ name: 'balance', label: 'Kontostand', field: 'balance' },
{
name: 'balance',
label: 'Kontostand',
field: 'balance',
format: (val: number) => val.toFixed(2)
},
{
name: 'limit',
label: 'Limit',

View File

@ -3,10 +3,7 @@
<q-page padding v-if="checkMain">
<q-card>
<q-card-section>
<q-list
v-for="(mainRoute, index) in mainRoutes"
:key="'mainRoute' + index"
>
<q-list v-for="(mainRoute, index) in mainRoutes" :key="'mainRoute' + index">
<essential-link
v-for="(route, index2) in mainRoute.children"
:key="'route' + index2"

View File

@ -31,7 +31,7 @@
</q-card>
</div>
<div v-for="(transaction, index) in transactions" v-bind:key="index" class="col-sm-4 col-xs-6">
<Transaction :transaction="transaction" @reversed="reversed" />
<Transaction :transaction.sync="transactions[index]" />
</div>
</q-page>
</template>
@ -56,7 +56,7 @@ export default defineComponent({
const sender = ref(store.state.user.currentUser);
const receiver = ref<FG.User | undefined>(undefined);
const amount = ref<number>(0);
const transactions = ref<FG.Transaction[]>([]);
const transactions = computed(() => store.state.balance.transactions.slice().reverse());
const sendDisabled = computed(() => {
return !(
@ -75,10 +75,6 @@ export default defineComponent({
receiver.value = selectedUser;
}
function reversed(id: number) {
transactions.value = transactions.value.filter(value => value.id != id);
}
function sendAmount() {
store
.dispatch('balance/changeBalance', {
@ -86,11 +82,6 @@ export default defineComponent({
sender: sender.value?.userid,
user: receiver.value?.userid
})
.then((transaction: FG.Transaction) => {
if (transactions.value.length > 5) transactions.value.pop();
transaction.time = new Date(transaction.time);
transactions.value.unshift(transaction);
})
.catch(err => console.log(err));
}
@ -103,8 +94,7 @@ export default defineComponent({
showSelector,
senderUpdated,
receiverUpdated,
sendDisabled,
reversed
sendDisabled
};
}
});

View File

@ -16,6 +16,7 @@ export interface UserBalance extends BalanceResponse {
export interface BalanceInterface {
balances: Map<string, UserBalance>;
shortcuts: Array<number>;
transactions: Array<FG.Transaction>;
loading: number;
}
@ -26,6 +27,7 @@ export interface StateInterfaceBalance extends StateInterface {
const state: BalanceInterface = {
balances: new Map<string, UserBalance>(),
shortcuts: [],
transactions: [],
loading: 0
};
@ -53,6 +55,15 @@ const mutations: MutationTree<BalanceInterface> = {
},
setShortcuts(state, data: Array<number>) {
state.shortcuts.splice(0, state.shortcuts.length, ...data);
},
addTransaction(state, data: FG.Transaction) {
state.transactions.push(data);
},
reverseTransaction(state, data: { transaction: FG.Transaction; reversal: FG.Transaction }) {
const idx = state.transactions.findIndex(value => value.id === data.transaction.id);
data.transaction.reversal = data.reversal;
if (idx > -1) state.transactions[idx] = data.transaction;
else state.transactions.push(data.transaction);
}
};
@ -111,8 +122,11 @@ const actions: ActionTree<BalanceInterface, StateInterface> = {
})
.finally(() => commit('setLoading', false));
},
revert({ dispatch }, transaction: FG.Transaction) {
return axios.delete(`/balance/${transaction.id}`).then(() => {
revert({ dispatch, commit }, transaction: FG.Transaction) {
return axios
.delete(`/balance/${transaction.id}`)
.then((response: AxiosResponse<FG.Transaction>) => {
commit('reverseTransaction', { transaction: transaction, reversal: response.data });
dispatch('getBalance').catch(err => console.warn(err));
});
},
@ -121,6 +135,9 @@ const actions: ActionTree<BalanceInterface, StateInterface> = {
return axios
.put(`/users/${data.user}/balance`, data)
.then((response: AxiosResponse<FG.Transaction>) => {
const transaction = response.data;
transaction.time = new Date(transaction.time);
commit('addTransaction', transaction);
commit(state.balances.has(data.user) ? 'changeBalance' : 'setBalance', {
userid: data.user,
amount: data.amount
@ -130,12 +147,12 @@ const actions: ActionTree<BalanceInterface, StateInterface> = {
userid: data.sender,
amount: -1 * data.amount
});
return response.data;
return transaction;
})
.catch(err => {
console.debug(err);
// Maybe Balance changed
dispatch('getBalance').catch(err => console.warn(err));
console.warn(err);
return dispatch('getBalance', data.sender ? data.sender : data.user);
})
.finally(() => commit('setLoading', false));
}

View File

@ -5,16 +5,10 @@
Benutzereinstellungen
</div>
<div class="col-xs-12 col-sm-6 q-pa-sm">
<UserSelector
:user="user"
@update:user="userUpdated"
/>
<UserSelector :user="user" @update:user="userUpdated" />
</div>
</q-card-section>
<MainUserSettings
:user="user"
@update:user="updateUser"
/>
<MainUserSettings :user="user" @update:user="updateUser" />
</q-card>
</template>
@ -39,7 +33,7 @@ export default defineComponent({
};
function updateUser(value: FG.User) {
store.dispatch('user/updateUser', value).catch((error) => {
store.dispatch('user/updateUser', value).catch(error => {
console.warn(error);
});
}
@ -47,9 +41,9 @@ export default defineComponent({
return {
user,
userUpdated,
updateUser,
updateUser
};
},
}
});
</script>

View File

@ -9,12 +9,16 @@
</div>
</div>
<div class="col-8">
<span class="text-h6">Hallo {{ name }}</span><br />
<span class="text-h6">Hallo {{ name }}</span
><br />
<span v-if="hasBirthday">Herzlichen Glückwunsch zum Geburtstag!<br /></span>
<span v-if="birthday.length > 0">Heute <span v-if="birthday.length === 1">hat </span><span v-else>haben </span><span
v-for="(user, index) in birthday"
v-bind:key="index"
>{{user.display_name}}<span v-if="index < (birthday.length-1)">, </span></span> Geburtstag.</span>
<span v-if="birthday.length > 0"
>Heute <span v-if="birthday.length === 1">hat </span><span v-else>haben </span
><span v-for="(user, index) in birthday" v-bind:key="index"
>{{ user.display_name }}<span v-if="index < birthday.length - 1">, </span></span
>
Geburtstag.</span
>
<span v-else>Heute stehen keine Geburtstage an</span>
</div>
</q-card-section>
@ -22,12 +26,7 @@
</template>
<script lang="ts">
import {
computed,
defineComponent,
onMounted,
ref,
} from '@vue/composition-api';
import { computed, defineComponent, onMounted, ref } from '@vue/composition-api';
import { Store } from 'vuex';
import { StateInterface } from 'src/store';
@ -57,10 +56,10 @@ export default defineComponent({
const birthday = computed(() =>
store.state.user.users
.filter(userHasBirthday)
.filter((user) => user.userid !== store.state.user.currentUser?.userid)
.filter(user => user.userid !== store.state.user.currentUser?.userid)
);
return { avatarLink, name, hasBirthday, birthday };
},
}
});
</script>

View File

@ -2,9 +2,7 @@
<div>
<q-card class="col-12">
<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">
<span class="col-xs-12 col-sm-6 text-center text-h6">
Rollen und Berechtigungen
</span>
@ -26,17 +24,9 @@
/>
</q-card-section>
<q-separator />
<q-card-section
v-if="role"
class="fit row justify-start content-center items-center"
>
<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
filled
v-model="newRoleName"
label="neuer Name"
v-if="role.id != -1"
/>
<q-input filled v-model="newRoleName" label="neuer Name" v-if="role.id != -1" />
<q-option-group
:value="role.permissions"
@input="updatePermissions"
@ -57,12 +47,7 @@
</template>
<script lang="ts">
import {
computed,
defineComponent,
ref,
onBeforeMount,
} from '@vue/composition-api';
import { computed, defineComponent, ref, onBeforeMount } from '@vue/composition-api';
import { Store } from 'vuex';
import { StateInterface } from 'src/store';
@ -72,10 +57,10 @@ export default defineComponent({
const store = <Store<StateInterface>>root.$store;
onBeforeMount(() => {
store.dispatch('user/getRoles').catch((error) => {
store.dispatch('user/getRoles').catch(error => {
console.warn(error);
});
store.dispatch('user/getPermissions').catch((error) => {
store.dispatch('user/getPermissions').catch(error => {
console.warn(error);
});
});
@ -83,20 +68,17 @@ export default defineComponent({
const role = ref<FG.Role | null>(null);
const roles = computed(() => store.state.user.roles);
const permissions = computed(() =>
store.state.user.permissions.map((perm) => {
store.state.user.permissions.map(perm => {
return {
value: perm,
label: perm,
label: perm
};
})
);
const newRoleName = ref<string>('');
function createRole(
name: string,
done: (arg0: string, arg1: string) => void
): void {
function createRole(name: string, done: (arg0: string, arg1: string) => void): void {
role.value = { name: name, permissions: [], id: -1 };
done(name, 'add-unique');
}
@ -116,16 +98,14 @@ export default defineComponent({
role.value = {
id: rl.id,
name: rl.name,
permissions: Array.from(rl.permissions),
permissions: Array.from(rl.permissions)
};
}
function save() {
if (role.value) {
if (role.value.id === -1)
void store
.dispatch('user/newRole', role.value)
.then((createdRole: FG.Role) => {
void store.dispatch('user/newRole', role.value).then((createdRole: FG.Role) => {
console.log(createdRole);
role.value = createdRole;
});
@ -139,9 +119,7 @@ export default defineComponent({
function reset() {
if (role.value && role.value.id !== -1) {
const original = roles.value.find(
(value) => value.name === role.value?.name
);
const original = roles.value.find(value => value.name === role.value?.name);
if (original) updateRole(original);
} else {
role.value = null;
@ -156,7 +134,7 @@ export default defineComponent({
store
.dispatch('user/deleteRole', role.value)
.then(() => (role.value = null))
.catch((error) => console.warn(error));
.catch(error => console.warn(error));
}
}
}
@ -172,8 +150,8 @@ export default defineComponent({
reset,
removeRole,
remove,
newRoleName,
newRoleName
};
},
}
});
</script>

View File

@ -16,30 +16,42 @@
{{ session.platform }}
</div>
</div>
<div class="row">
<div class="row" v-if="!isEdit">
<div class="col-xs-12 col-sm-6">
Lebenszeit:
{{ session.lifetime }}
</div>
<div class="col-xs-12 col-sm-6">
Läuft aus: {{ session.expires | dateTime(true) }}
<div class="col-xs-12 col-sm-6">Läuft aus: {{ session.expires | dateTime(true) }}</div>
</div>
<div class="row q-my-sm" v-else>
<q-input
class="col-xs-12 col-sm-6 q-px-sm"
v-model="computedLifetime"
type="number"
label="Zeit"
filled
/>
<q-select
class="col-xs-12 col-sm-6 q-px-sm"
:options="options"
v-model="option"
filled
/>
</div>
</q-card-section>
<q-card-actions align="right">
<q-btn
flat
round
dense
icon="mdi-delete"
@click="deleteSession(session.token)"
/>
<q-card-actions align="right" v-if="!isEdit">
<q-btn flat round dense icon="mdi-pencil" @click="edit(true)" />
<q-btn flat round dense icon="mdi-delete" @click="deleteSession(session.token)" />
</q-card-actions>
<q-card-actions align="right" v-else>
<q-btn flat dense label="Abbrechen" @click="edit(false)" />
<q-btn flat dense label="Speichern" @click="save" />
</q-card-actions>
</q-card>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent, ref, computed } from '@vue/composition-api';
import { Store } from 'vuex';
import { StateInterface } from 'src/store';
@ -47,12 +59,14 @@ export default defineComponent({
name: 'Sessions',
props: {
session: {
required: true
}
required: true,
},
setup(_, { root }) {
},
setup(props: {session: FG.Session}, { root }) {
const store = <Store<StateInterface>>root.$store;
const options = ref(['Minuten', 'Stunden', 'Tage']);
const option = ref<string>(options.value[0]);
const lifetime = ref(0);
function getBrowserIcon(browser: string) {
return browser == 'firefox'
? 'mdi-firefox'
@ -78,7 +92,7 @@ export default defineComponent({
}
function deleteSession(token: string) {
store.dispatch('session/deleteSession', token).catch(error => {
store.dispatch('session/deleteSession', token).catch((error) => {
console.warn(error);
});
}
@ -86,12 +100,67 @@ export default defineComponent({
return store.state.session.currentSession?.token === token;
}
const isEdit = ref(false);
const computedLifetime = computed({
get: () => {
switch (option.value) {
case options.value[0]:
return (lifetime.value / 60).toFixed(2);
case options.value[1]:
return (lifetime.value / (60 * 60)).toFixed(2);
case options.value[2]:
return (lifetime.value / (60 * 60 * 24)).toFixed(2);
}
},
set: (val) => {
if (val) {
switch (option.value) {
case options.value[0]:
lifetime.value = parseFloat(val) * 60;
break;
case options.value[1]:
lifetime.value = parseFloat(val) * 60 * 60;
break;
case options.value[2]:
lifetime.value = parseFloat(val) * 60 * 60 * 24;
break;
}
}
},
});
function edit(value: boolean) {
lifetime.value = props.session.lifetime;
isEdit.value = value;
}
function save() {
console.log(lifetime.value);
isEdit.value = false;
void store
.dispatch(
'session/updateSession',
{lifetime: lifetime.value, token: props.session.token}
)
.catch((error) => {
console.log(error);
});
}
return {
getBrowserIcon,
getPlatformIcon,
isThisSession,
deleteSession
deleteSession,
isEdit,
edit,
options,
option,
lifetime,
computedLifetime,
save,
};
}
},
});
</script>

View File

@ -1,9 +1,6 @@
<template>
<div>
<q-tabs
v-model="tab"
v-if="$q.screen.gt.sm"
>
<q-tabs v-model="tab" v-if="$q.screen.gt.sm">
<q-tab
v-for="(tabindex, index) in tabs"
:key="'tab' + index"
@ -11,23 +8,10 @@
:label="tabindex.label"
/>
</q-tabs>
<div
class="fit row justify-end"
v-else
>
<q-btn
flat
round
icon="mdi-menu"
@click="showDrawer = !showDrawer"
/>
<div class="fit row justify-end" v-else>
<q-btn flat round icon="mdi-menu" @click="showDrawer = !showDrawer" />
</div>
<q-drawer
side="right"
v-model="showDrawer"
@click="showDrawer = !showDrawer"
behavior="mobile"
>
<q-drawer side="right" v-model="showDrawer" @click="showDrawer = !showDrawer" behavior="mobile">
<q-list v-model="tab">
<q-item
v-for="(tabindex, index) in tabs"
@ -40,10 +24,7 @@
</q-item>
</q-list>
</q-drawer>
<q-page
padding
class="fit row justify-center content-start items-start q-gutter-sm"
>
<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;"
@ -81,9 +62,7 @@ export default defineComponent({
setup(_, { root }) {
const store = <Store<StateInterface>>root.$store;
const canEditRoles = computed(() =>
hasPermission(PERMISSIONS.ROLES_EDIT, store)
);
const canEditRoles = computed(() => hasPermission(PERMISSIONS.ROLES_EDIT, store));
interface Tab {
name: string;
@ -93,7 +72,7 @@ export default defineComponent({
const tabs: Tab[] = [
{ name: 'user', label: 'Mitglieder' },
{ name: 'newUser', label: 'Neues Mitglied' },
{ name: 'roles', label: 'Rollen' },
{ name: 'roles', label: 'Rollen' }
];
const drawer = ref<boolean>(false);
@ -104,7 +83,7 @@ export default defineComponent({
},
set: (val: boolean) => {
drawer.value = val;
},
}
});
const tab = ref<string>('user');
@ -113,8 +92,8 @@ export default defineComponent({
canEditRoles,
showDrawer,
tab,
tabs,
tabs
};
},
}
});
</script>

View File

@ -3,10 +3,7 @@
<q-page padding v-if="checkMain">
<q-card>
<q-card-section>
<q-list
v-for="(mainRoute, index) in mainRoutes"
:key="'mainRoute' + index"
>
<q-list v-for="(mainRoute, index) in mainRoutes" :key="'mainRoute' + index">
<essential-link
v-for="(route, index2) in mainRoute.children"
:key="'route' + index2"

View File

@ -14,12 +14,7 @@
</template>
<script lang="ts">
import {
computed,
defineComponent,
onBeforeMount,
ref
} from '@vue/composition-api';
import { computed, defineComponent, onBeforeMount, ref } from '@vue/composition-api';
import Sessions from '../components/settings/Sessions.vue';
import MainUserSettings from '../components/settings/MainUserSettings.vue';
import { Store } from 'vuex';
@ -40,9 +35,7 @@ export default defineComponent({
const currentUser = ref(<FG.User>store.state.user.currentUser);
const sessions = computed(() => store.state.session.sessions);
const loading = computed(
() => store.state.session.loading || store.state.user.loading > 0
);
const loading = computed(() => store.state.session.loading || store.state.user.loading > 0);
function updateUser(value: FG.User) {
store.dispatch('user/updateUser', value).catch(error => {
console.warn(error);

View File

@ -25,7 +25,7 @@ function loadCurrentSession() {
const state: SessionInterface = {
sessions: [],
currentSession: loadCurrentSession() || undefined,
loading: false
loading: false,
};
const mutations: MutationTree<SessionInterface> = {
@ -42,7 +42,13 @@ const mutations: MutationTree<SessionInterface> = {
},
setLoading(state, value: boolean) {
state.loading = value;
},
updateSession(state, session: FG.Session) {
const index = state.sessions.findIndex((x) => x.token == session.token);
if (index > -1) {
state.sessions[index] = session;
}
},
};
const actions: ActionTree<SessionInterface, StateInterface> = {
@ -59,7 +65,7 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
commit('setCurrentSession', response.data.session);
commit('user/setCurrentUser', response.data.user, { root: true });
commit('user/setCurrentPermissions', response.data.permissions, {
root: true
root: true,
});
})
.catch((error: AxiosError) => {
@ -72,7 +78,7 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
*/
logout({ dispatch, rootState }) {
if (rootState.session.currentSession) {
dispatch('deleteSession', rootState.session.currentSession.token).catch(error => {
dispatch('deleteSession', rootState.session.currentSession.token).catch((error) => {
console.log(error);
void dispatch('clearCurrent', false);
});
@ -91,7 +97,7 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
if (token === rootState.session.currentSession?.token) {
void dispatch('clearCurrent', false);
} else {
dispatch('getSessions').catch(error => {
dispatch('getSessions').catch((error) => {
throw error;
});
}
@ -110,7 +116,7 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
void Router.push({
name: 'login',
query: redirect ? { redirect: Router.currentRoute.fullPath } : {},
params: { logout: 'true' }
params: { logout: 'true' },
}).then(() => {
commit('clearCurrentSession');
commit('user/clearCurrentUser', null, { root: true });
@ -126,7 +132,7 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
axios
.get('/auth')
.then((response: AxiosResponse<FG.Session[]>) => {
response.data.forEach(session => {
response.data.forEach((session) => {
session.expires = new Date(session.expires);
});
commit('setSessions', response.data);
@ -137,13 +143,29 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
commit('setCurrentSession', currentSession);
}
})
.catch(error => {
.catch((error) => {
throw error;
})
.finally(() => {
commit('setLoading', false);
});
},
updateSession({ commit, state }, data: { lifetime: number; token: string }) {
commit('setLoading', true);
axios
.put(`auth/${data.token}`, { value: data.lifetime })
.then((response: AxiosResponse<FG.Session>) => {
response.data.expires = new Date(response.data.expires);
if (state.currentSession?.token == response.data.token) {
commit('setCurrentSession', response.data);
}
})
.catch((err) => console.log(err))
.finally(() => {
commit('setLoading', false);
});
console.log('updateSession', data);
},
requestPasswordReset({}, data) {
return axios.post('/auth/reset', data);
},
@ -151,7 +173,7 @@ const actions: ActionTree<SessionInterface, StateInterface> = {
return axios.post('/auth/reset', data).catch((error: AxiosError) => {
return Promise.reject(error.response);
});
}
},
};
const getters: GetterTree<SessionInterface, StateInterface> = {
@ -163,7 +185,7 @@ const getters: GetterTree<SessionInterface, StateInterface> = {
},
loading(state) {
return state.loading;
}
},
};
const sessions: Module<SessionInterface, StateInterface> = {
@ -171,7 +193,7 @@ const sessions: Module<SessionInterface, StateInterface> = {
state,
mutations,
actions,
getters
getters,
};
export default sessions;

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { Module, MutationTree, ActionTree, GetterTree } from 'vuex';
import { StateInterface } from 'src/store';
import { axios } from 'boot/axios';
@ -48,6 +49,11 @@ const mutations: MutationTree<UserStateInterface> = {
setUsers(state, data: FG.User[]) {
state.users = data;
},
setUser(state, data: FG.User) {
const index = state.users.findIndex(x => x.userid === data.userid);
if (index > -1) state.users[index] = data;
else state.users.push(data);
},
setRoles(state, data: FG.Role[]) {
state.roles = data;
},
@ -221,10 +227,26 @@ const actions: ActionTree<UserStateInterface, StateInterface> = {
commit('setPermissions', response.data);
})
.finally(() => commit('setLoading', false));
},
getUser({ commit, getters }, data: { userid: string; force?: boolean }) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const user = <FG.User | undefined>getters['getUser'](data.userid);
if (user === undefined || data.force === true) {
return axios.get(`/users/${data.userid}`).then((response: AxiosResponse<FG.User>) => {
commit('setUser', response.data);
return response.data;
});
} else {
return Promise.resolve(user);
}
}
};
const getters: GetterTree<UserStateInterface, StateInterface> = {
getUser: state => (userid: string) => {
const user = state.users.filter(usr => usr.userid === userid);
return user.length > 0 ? user[0] : undefined;
},
currentUser({ currentUser }) {
return currentUser;
},