Compare commits
41 commits
Author | SHA1 | Date | |
---|---|---|---|
291d04182c | |||
37d73d7c74 | |||
31c0b2a4c1 | |||
defa732212 | |||
1452456138 | |||
0e0f86adce | |||
d9a24eb723 | |||
d1ff313754 | |||
87cb4252ec | |||
90f5ef3b1a | |||
0db141cd13 | |||
583d4913d9 | |||
f6252901cd | |||
ff53d2d4d9 | |||
05ec4afadb | |||
3b89262ce9 | |||
516c6a9e92 | |||
ec0222ff2f | |||
0defc9b0ba | |||
caf8e71a51 | |||
d11f0d50c6 | |||
2ce66da1d1 | |||
f3913a906c | |||
cfe621debd | |||
5b7a9a3ace | |||
12b1d08ea4 | |||
04c01b6780 | |||
4ee16c624a | |||
35fd8a8e82 | |||
832f5053a0 | |||
362fc80891 | |||
2d35e2416b | |||
738765bcb4 | |||
a39044dffc | |||
369a67abda | |||
9bac6e2f97 | |||
f64397862c | |||
18d52e4bab | |||
b4fdd5fc60 | |||
c17355fcd1 | |||
fa5fb54876 |
141 changed files with 2099 additions and 1513 deletions
17
README.md
17
README.md
|
@ -8,12 +8,9 @@ Dieses Repository dient hauptsächlich zur Verwaltung der Mitgliederdaten, aber
|
|||
|
||||
Eine Demo dieser Seite finden Sie unter [https://admin-demo.ff-admin.de](https://admin-demo.ff-admin.de).
|
||||
|
||||
Für die Verwendung muss ein TOTP-Code eingegeben werden.
|
||||
Die Zugangsdaten (Lesebeschränkt) sind unterhalb dem Login angegeben.
|
||||
|
||||
Die Zugangsdaten (Lesebeschränkt) sind:\
|
||||
EMAIL: demo-besucher\
|
||||
TOTP: \
|
||||
TOTP-Code: FBMDAJKFOYQXM2DNH47GWWBGJ5KWOUCW
|
||||
Das Handbuch zur Anwendung finden sie unter [https://ff-admin.de/ff-admin-handbook](https://ff-admin.de/ff-admin-handbook).
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -29,17 +26,9 @@ services:
|
|||
image: docker.registry.jk-effects.cloud/ehrenamt/ff-admin/app:latest
|
||||
container_name: ff_admin
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
#environment:
|
||||
# - SERVERADDRESS=<backend_url (https://... | http://...)> # wichtig: ohne Pfad
|
||||
# - APPNAMEOVERWRITE=<appname> # ersetzt den Namen FF-Admin auf der Login-Seite und sonstigen Positionen in der Oberfläche
|
||||
# - IMPRINTLINK=<imprint link>
|
||||
# - PRIVACYLINK=<privacy link>
|
||||
# - CUSTOMLOGINMESSAGE=betrieben von xy
|
||||
#volumes:
|
||||
# - <volume|local path>/favicon.ico:/usr/share/nginx/html/favicon.ico # 48x48 px Auflösung
|
||||
# - <volume|local path>/favicon.png:/usr/share/nginx/html/favicon.png # 512x512 px Auflösung - wird als pwa Icon genutzt
|
||||
# - <volume|local path>/Logo.png:/usr/share/nginx/html/Logo.png
|
||||
```
|
||||
|
||||
Wenn keine Server-Adresse angegeben wird, wird versucht das Backend unter der URL des Frontends zu erreichen. Dazu muss das Backend auf der gleichen URL wie das Frontend laufen. Zur Unterscheidung von Frontend und Backend bei gleicher URL müssen alle Anfragen mit dem PathPrefix `/api` an das Backend weitergeleitet werden.
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.9 KiB |
1213
package-lock.json
generated
1213
package-lock.json
generated
File diff suppressed because it is too large
Load diff
24
package.json
24
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ff-admin",
|
||||
"version": "1.5.0",
|
||||
"version": "1.7.0",
|
||||
"description": "Feuerwehr/Verein Mitgliederverwaltung UI",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
@ -32,7 +32,7 @@
|
|||
"@fullcalendar/vue3": "^6.1.17",
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@tailwindcss/vite": "^4.1.5",
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"@vueup/vue-quill": "^1.2.0",
|
||||
"axios": "^1.9.0",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
|
@ -49,19 +49,19 @@
|
|||
"markdown-it-prism": "^3.0.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pdf-dist": "^1.0.0",
|
||||
"pinia": "^3.0.2",
|
||||
"pinia": "^3.0.3",
|
||||
"pwacompat": "^2.0.17",
|
||||
"qrcode": "^1.5.4",
|
||||
"qs": "^6.14.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"unplugin-vue-markdown": "^28.3.1",
|
||||
"uuid": "^11.1.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue": "^3.5.16",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.11.0",
|
||||
"@tailwindcss/postcss": "^4.1.5",
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"@tsconfig/node20": "^20.1.5",
|
||||
"@types/eslint": "~9.6.1",
|
||||
"@types/event-source-polyfill": "^1.0.5",
|
||||
|
@ -70,21 +70,21 @@
|
|||
"@types/lodash.differencewith": "^4.5.9",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^22.15.12",
|
||||
"@types/node": "^22.15.30",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/qs": "^6.9.18",
|
||||
"@types/qs": "^6.14.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vite-pwa/assets-generator": "^1.0.0",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.5.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-plugin-vue": "^10.1.0",
|
||||
"npm-run-all2": "^8.0.1",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-plugin-vue": "^10.2.0",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"prettier": "^3.5.3",
|
||||
"tailwindcss": "^4.1.5",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-pwa": "^1.0.0",
|
||||
|
|
BIN
public/admin-logo.png
Normal file
BIN
public/admin-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
public/icon.png
Normal file
BIN
public/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
|
@ -12,6 +12,7 @@
|
|||
<Teleport to="head">
|
||||
<title>{{ clubName }}</title>
|
||||
<link rel="icon" type="image/ico" :href="config.server_address + '/api/public/favicon.ico'" />
|
||||
<link rel="icon" type="image/png" href="/icon.png" />
|
||||
<link rel="manifest" :href="config.server_address + '/api/public/manifest.webmanifest'" />
|
||||
</Teleport>
|
||||
</template>
|
||||
|
@ -29,6 +30,7 @@ import Modal from "./components/Modal.vue";
|
|||
import Notification from "./components/Notification.vue";
|
||||
import { config } from "./config";
|
||||
import { useConfigurationStore } from "@/stores/configuration";
|
||||
import { resetAllPiniaStores } from "@/helpers/piniaReset";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -38,6 +40,7 @@ export default defineComponent({
|
|||
...mapState(useConfigurationStore, ["clubName"]),
|
||||
},
|
||||
mounted() {
|
||||
resetAllPiniaStores();
|
||||
this.configure();
|
||||
|
||||
if (!this.authCheck && localStorage.getItem("access_token")) {
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
<template>
|
||||
<img ref="icon" :src="url + '/api/public/icon.png'" alt="LOGO" class="h-full w-auto" />
|
||||
<img v-if="useFallback" ref="fallback" src="/icon.png" alt="LOGO" class="h-full w-auto" />
|
||||
<img
|
||||
v-else
|
||||
ref="icon"
|
||||
:src="url + '/api/public/icon.png'"
|
||||
alt="LOGO"
|
||||
class="h-full w-auto"
|
||||
@error="useFallback = true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -16,6 +24,11 @@ export default defineComponent({
|
|||
(this.$refs.icon as HTMLImageElement).src = url + "/api/public/icon.png?" + new Date().getTime();
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
useFallback: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useSettingStore, ["readSetting"]),
|
||||
icon() {
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
<template>
|
||||
<img ref="logo" :src="url + '/api/public/applogo.png'" alt="LOGO" class="h-full w-auto" />
|
||||
<img v-if="useFallback" ref="fallback" src="/admin-logo.png" alt="LOGO" class="h-full w-auto" />
|
||||
<img
|
||||
v-else
|
||||
ref="logo"
|
||||
:src="url + '/api/public/applogo.png'"
|
||||
alt="LOGO"
|
||||
class="h-full w-auto"
|
||||
@error="useFallback = true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -16,6 +24,11 @@ export default defineComponent({
|
|||
(this.$refs.logo as HTMLImageElement).src = url + "/api/public/applogo.png?t=" + new Date().getTime();
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
useFallback: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useSettingStore, ["readSetting"]),
|
||||
logo() {
|
||||
|
|
64
src/components/DoubleConfirmClick.vue
Normal file
64
src/components/DoubleConfirmClick.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div
|
||||
class="cursor-pointer"
|
||||
:class="{ 'text-white': light, 'animate-pulse': isSensitive }"
|
||||
:title="`2 mal klicken für ${action}`"
|
||||
@click.prevent="handleClick"
|
||||
>
|
||||
<slot :isSensitive="isSensitive">
|
||||
<CursorArrowRaysIcon v-if="!isSensitive" class="h-5 w-5" />
|
||||
<CursorArrowRippleIcon v-else class="h-5 w-5" />
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CursorArrowRaysIcon, CursorArrowRippleIcon } from "@heroicons/vue/24/outline";
|
||||
import { defineComponent } from "vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
light: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
default: "Bestätigung",
|
||||
},
|
||||
},
|
||||
emits: ["click:first", "click:submit", "click:reset"],
|
||||
data() {
|
||||
return {
|
||||
isSensitive: false as boolean,
|
||||
timeout: undefined as any,
|
||||
};
|
||||
},
|
||||
beforeUnmount() {
|
||||
try {
|
||||
clearTimeout(this.timeout);
|
||||
} catch (error) {}
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
if (this.isSensitive) {
|
||||
clearTimeout(this.timeout);
|
||||
this.isSensitive = false;
|
||||
this.$emit("click:submit");
|
||||
} else {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.isSensitive = true;
|
||||
this.$emit("click:first");
|
||||
|
||||
this.timeout = setTimeout(() => {
|
||||
this.isSensitive = false;
|
||||
this.$emit("click:reset");
|
||||
}, 2000);
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -42,7 +42,7 @@ import UserMenu from "./UserMenu.vue";
|
|||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import AppLogo from "./AppLogo.vue";
|
||||
import { useConfigurationStore } from "../stores/configuration";
|
||||
import { useConfigurationStore } from "@/stores/configuration";
|
||||
export default defineComponent({
|
||||
computed: {
|
||||
...mapState(useAuthStore, ["authCheck"]),
|
||||
|
|
|
@ -48,7 +48,7 @@ import Spinner from "@/components/Spinner.vue";
|
|||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import TextCopy from "@/components/TextCopy.vue";
|
||||
import { hashString } from "../../helpers/crypto";
|
||||
import { hashString } from "@/helpers/crypto";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -84,7 +84,7 @@ export default defineComponent({
|
|||
this.changeStatus = "loading";
|
||||
this.changeError = "";
|
||||
this.$http
|
||||
.post(`/user/changeToPW`, {
|
||||
.patch(`/user/changeToPW`, {
|
||||
newpassword: await hashString(formData.new.value),
|
||||
})
|
||||
.then((result) => {
|
||||
|
|
|
@ -69,7 +69,7 @@ export default defineComponent({
|
|||
this.verifyStatus = "loading";
|
||||
this.verifyError = "";
|
||||
this.$http
|
||||
.post(`/user/changeToTOTP`, {
|
||||
.patch(`/user/changeToTOTP`, {
|
||||
otp: this.otp,
|
||||
totp: formData.totp.value,
|
||||
})
|
||||
|
|
|
@ -58,7 +58,7 @@ import MainTemplate from "@/templates/Main.vue";
|
|||
import Spinner from "@/components/Spinner.vue";
|
||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { hashString } from "../../helpers/crypto";
|
||||
import { hashString } from "@/helpers/crypto";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -87,7 +87,7 @@ export default defineComponent({
|
|||
this.changeStatus = "loading";
|
||||
this.changeError = "";
|
||||
this.$http
|
||||
.post(`/user/changepw`, {
|
||||
.patch(`/user/changepw`, {
|
||||
current: await hashString(formData.current.value),
|
||||
newpassword: await hashString(formData.new.value),
|
||||
})
|
||||
|
|
|
@ -87,7 +87,7 @@ import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
|||
import { useMemberStore } from "@/stores/admin/club/member/member";
|
||||
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||
import difference from "lodash.difference";
|
||||
import Spinner from "../Spinner.vue";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -18,22 +18,22 @@
|
|||
<div class="flex flex-row border border-white rounded-md overflow-hidden">
|
||||
<EyeIcon
|
||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||
:class="_can(permissionUpdate, 'read', section) ? 'bg-success' : ''"
|
||||
:class="_canSection(permissionUpdate, 'read', section) ? 'bg-success' : ''"
|
||||
@click="togglePermission('read', section)"
|
||||
/>
|
||||
<PlusIcon
|
||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||
:class="_can(permissionUpdate, 'create', section) ? 'bg-success' : ''"
|
||||
:class="_canSection(permissionUpdate, 'create', section) ? 'bg-success' : ''"
|
||||
@click="togglePermission('create', section)"
|
||||
/>
|
||||
<PencilIcon
|
||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||
:class="_can(permissionUpdate, 'update', section) ? 'bg-success' : ''"
|
||||
:class="_canSection(permissionUpdate, 'update', section) ? 'bg-success' : ''"
|
||||
@click="togglePermission('update', section)"
|
||||
/>
|
||||
<TrashIcon
|
||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||
:class="_can(permissionUpdate, 'delete', section) ? 'bg-success' : ''"
|
||||
:class="_canSection(permissionUpdate, 'delete', section) ? 'bg-success' : ''"
|
||||
@click="togglePermission('delete', section)"
|
||||
/>
|
||||
</div>
|
||||
|
@ -132,7 +132,7 @@ export default defineComponent({
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useAbilityStore, ["_can"]),
|
||||
...mapState(useAbilityStore, ["_can", "_canSection"]),
|
||||
canSaveOrReset(): boolean {
|
||||
return isEqual(this.permissions, this.permissionUpdate);
|
||||
},
|
||||
|
|
|
@ -80,6 +80,10 @@
|
|||
|
||||
<input type="text" id="internalId" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="note">Notiz (optional)</label>
|
||||
<textarea type="text" id="note" />
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
|
||||
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||
|
@ -109,8 +113,8 @@ import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } f
|
|||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import { useMemberStore } from "@/stores/admin/club/member/member";
|
||||
import type { CreateMemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||
import { useSalutationStore } from "../../../../stores/admin/configuration/salutation";
|
||||
import type { SalutationViewModel } from "../../../../viewmodels/admin/configuration/salutation.models";
|
||||
import { useSalutationStore } from "@/stores/admin/configuration/salutation";
|
||||
import type { SalutationViewModel } from "@/viewmodels/admin/configuration/salutation.models";
|
||||
import { InformationCircleIcon } from "@heroicons/vue/24/outline";
|
||||
</script>
|
||||
|
||||
|
@ -154,6 +158,7 @@ export default defineComponent({
|
|||
nameaffix: formData.nameaffix.value,
|
||||
birthdate: formData.birthdate.value,
|
||||
internalId: formData.internalId.value,
|
||||
note: formData.note.value,
|
||||
};
|
||||
this.status = "loading";
|
||||
this.createMember(createMember)
|
||||
|
|
164
src/components/admin/club/member/MemberEducationCreateModal.vue
Normal file
164
src/components/admin/club/member/MemberEducationCreateModal.vue
Normal file
|
@ -0,0 +1,164 @@
|
|||
<template>
|
||||
<div class="w-full md:max-w-md">
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-xl font-medium">Mitglied-Aus-/Fortbildung hinzufügen</p>
|
||||
</div>
|
||||
<br />
|
||||
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
|
||||
<div>
|
||||
<Listbox v-model="selectedEducation" name="education">
|
||||
<ListboxLabel>Aus-/Fortbildung</ListboxLabel>
|
||||
<div class="relative mt-1">
|
||||
<ListboxButton
|
||||
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||
>
|
||||
<span class="block truncate w-full text-start">
|
||||
{{
|
||||
educations.length != 0
|
||||
? (selectedEducation?.education ?? "bitte auswählen")
|
||||
: "keine Auswahl vorhanden"
|
||||
}}</span
|
||||
>
|
||||
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition duration-100 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||
>
|
||||
<ListboxOption v-if="educations.length == 0" disabled as="template">
|
||||
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||
<span :class="['font-normal', 'block truncate']">keine Auswahl vorhanden</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
<ListboxOption
|
||||
v-slot="{ active, selected }"
|
||||
v-for="education in educations"
|
||||
:key="education.id"
|
||||
:value="education"
|
||||
as="template"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-red-200 text-amber-900' : 'text-gray-900',
|
||||
'relative cursor-default select-none py-2 pl-10 pr-4',
|
||||
]"
|
||||
>
|
||||
<span :class="[selected ? 'font-medium' : 'font-normal', 'block truncate']">{{
|
||||
education.education
|
||||
}}</span>
|
||||
<span v-if="selected" class="absolute inset-y-0 left-0 flex items-center pl-3 text-primary">
|
||||
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
<div>
|
||||
<label for="start">Start</label>
|
||||
<input type="date" id="start" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="end">Ende (optional)</label>
|
||||
<input type="date" id="end" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="place">Ort (optional)</label>
|
||||
<input type="text" id="place" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="note">Notiz (optional)</label>
|
||||
<input type="text" id="note" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
|
||||
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="flex flex-row justify-end">
|
||||
<div class="flex flex-row gap-4 py-2">
|
||||
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||
abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import { useEducationStore } from "@/stores/admin/configuration/education";
|
||||
import type { EducationViewModel } from "@/viewmodels/admin/configuration/education.models";
|
||||
import type { CreateMemberEducationViewModel } from "@/viewmodels/admin/club/member/memberEducation.models";
|
||||
import { useMemberEducationStore } from "@/stores/admin/club/member/memberEducation";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||
timeout: undefined as any,
|
||||
selectedEducation: undefined as undefined | EducationViewModel,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useEducationStore, ["educations"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchEducations();
|
||||
},
|
||||
beforeUnmount() {
|
||||
try {
|
||||
clearTimeout(this.timeout);
|
||||
} catch (error) {}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["closeModal"]),
|
||||
...mapActions(useMemberEducationStore, ["createMemberEducation"]),
|
||||
...mapActions(useEducationStore, ["fetchEducations"]),
|
||||
triggerCreate(e: any) {
|
||||
if (this.selectedEducation == undefined) return;
|
||||
let formData = e.target.elements;
|
||||
let createMemberEducation: CreateMemberEducationViewModel = {
|
||||
start: formData.start.value,
|
||||
end: formData.end.value,
|
||||
note: formData.note.value,
|
||||
place: formData.place.value,
|
||||
educationId: this.selectedEducation.id,
|
||||
};
|
||||
this.status = "loading";
|
||||
this.createMemberEducation(createMemberEducation)
|
||||
.then(() => {
|
||||
this.status = { status: "success" };
|
||||
this.timeout = setTimeout(() => {
|
||||
this.closeModal();
|
||||
}, 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<div class="w-full md:max-w-md">
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-xl font-medium">Mitglied-Aus-/Fortbildung löschen</p>
|
||||
</div>
|
||||
<br />
|
||||
<p class="text-center">Aus-/Fortbildung {{ memberEducation?.education }} löschen?</p>
|
||||
<br />
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<button
|
||||
primary
|
||||
type="submit"
|
||||
:disabled="status == 'loading' || status?.status == 'success'"
|
||||
@click="triggerDelete"
|
||||
>
|
||||
löschen
|
||||
</button>
|
||||
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end">
|
||||
<div class="flex flex-row gap-4 py-2">
|
||||
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||
abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { useMemberEducationStore } from "@/stores/admin/club/member/memberEducation";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||
timeout: undefined as any,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useModalStore, ["data"]),
|
||||
...mapState(useMemberEducationStore, ["memberEducations"]),
|
||||
memberEducation() {
|
||||
return this.memberEducations.find((m) => m.id == this.data);
|
||||
},
|
||||
},
|
||||
beforeUnmount() {
|
||||
try {
|
||||
clearTimeout(this.timeout);
|
||||
} catch (error) {}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["closeModal"]),
|
||||
...mapActions(useMemberEducationStore, ["deleteMemberEducation"]),
|
||||
triggerDelete() {
|
||||
this.status = "loading";
|
||||
this.deleteMemberEducation(this.data)
|
||||
.then(() => {
|
||||
this.status = { status: "success" };
|
||||
this.timeout = setTimeout(() => {
|
||||
this.closeModal();
|
||||
}, 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
198
src/components/admin/club/member/MemberEducationEditModal.vue
Normal file
198
src/components/admin/club/member/MemberEducationEditModal.vue
Normal file
|
@ -0,0 +1,198 @@
|
|||
<template>
|
||||
<div class="w-full md:max-w-md">
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-xl font-medium">Mitglied-Aus-/Fortbildung bearbeiten</p>
|
||||
</div>
|
||||
<br />
|
||||
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||
<p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">↺ laden fehlgeschlagen</p>
|
||||
<form v-else-if="memberEducation != null" class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
|
||||
<div>
|
||||
<Listbox v-model="memberEducation.educationId" name="education">
|
||||
<ListboxLabel>Aus-/Fortbildung</ListboxLabel>
|
||||
<div class="relative mt-1">
|
||||
<ListboxButton
|
||||
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||
>
|
||||
<span class="block truncate w-full text-start">
|
||||
{{
|
||||
educations.length != 0 ? (selectedEducation ?? "bitte auswählen") : "keine Auswahl vorhanden"
|
||||
}}</span
|
||||
>
|
||||
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition duration-100 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||
>
|
||||
<ListboxOption v-if="educations.length == 0" disabled as="template">
|
||||
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||
<span :class="['font-normal', 'block truncate']">keine Auswahl vorhanden</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
<ListboxOption
|
||||
v-slot="{ active, selected }"
|
||||
v-for="education in educations"
|
||||
:key="education.id"
|
||||
:value="education.id"
|
||||
as="template"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'bg-red-200 text-amber-900' : 'text-gray-900',
|
||||
'relative cursor-default select-none py-2 pl-10 pr-4',
|
||||
]"
|
||||
>
|
||||
<span :class="[selected ? 'font-medium' : 'font-normal', 'block truncate']">{{
|
||||
education.education
|
||||
}}</span>
|
||||
<span v-if="selected" class="absolute inset-y-0 left-0 flex items-center pl-3 text-primary">
|
||||
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
<div>
|
||||
<label for="start">Start</label>
|
||||
<input type="date" id="start" required v-model="memberEducation.start" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="end">Ende (optional)</label>
|
||||
<input type="date" id="end" v-model="memberEducation.end" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="place">Ort (optional)</label>
|
||||
<input type="text" id="place" v-model="memberEducation.place" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="note">Notiz (optional)</label>
|
||||
<input type="text" id="note" v-model="memberEducation.note" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<button primary-outline type="reset" :disabled="canSaveOrReset" @click="resetForm">verwerfen</button>
|
||||
<button primary type="submit" :disabled="status == 'loading' || canSaveOrReset">speichern</button>
|
||||
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="flex flex-row justify-end">
|
||||
<div class="flex flex-row gap-4 py-2">
|
||||
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||
schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import { useEducationStore } from "@/stores/admin/configuration/education";
|
||||
import type {
|
||||
CreateMemberEducationViewModel,
|
||||
MemberEducationViewModel,
|
||||
UpdateMemberEducationViewModel,
|
||||
} from "@/viewmodels/admin/club/member/memberEducation.models";
|
||||
import { useMemberEducationStore } from "@/stores/admin/club/member/memberEducation";
|
||||
import isEqual from "lodash.isequal";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||
origin: null as null | MemberEducationViewModel,
|
||||
memberEducation: null as null | MemberEducationViewModel,
|
||||
timeout: undefined as any,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useEducationStore, ["educations"]),
|
||||
...mapState(useModalStore, ["data"]),
|
||||
canSaveOrReset(): boolean {
|
||||
return isEqual(this.origin, this.memberEducation);
|
||||
},
|
||||
selectedEducation() {
|
||||
return this.educations.find((ms) => ms.id == this.memberEducation?.educationId)?.education;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchEducations();
|
||||
this.fetchItem();
|
||||
},
|
||||
beforeUnmount() {
|
||||
try {
|
||||
clearTimeout(this.timeout);
|
||||
} catch (error) {}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["closeModal"]),
|
||||
...mapActions(useMemberEducationStore, ["updateMemberEducation", "fetchMemberEducationById"]),
|
||||
...mapActions(useEducationStore, ["fetchEducations"]),
|
||||
resetForm() {
|
||||
this.memberEducation = cloneDeep(this.origin);
|
||||
},
|
||||
fetchItem() {
|
||||
this.fetchMemberEducationById(this.data)
|
||||
.then((result) => {
|
||||
this.memberEducation = result.data;
|
||||
this.origin = cloneDeep(result.data);
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
triggerCreate(e: any) {
|
||||
if (this.memberEducation == null) return;
|
||||
let formData = e.target.elements;
|
||||
let updateMemberEducation: UpdateMemberEducationViewModel = {
|
||||
id: this.memberEducation.id,
|
||||
start: formData.start.value,
|
||||
end: formData.end.value,
|
||||
note: formData.note.value,
|
||||
place: formData.place.value,
|
||||
educationId: this.memberEducation.educationId,
|
||||
};
|
||||
this.status = "loading";
|
||||
this.updateMemberEducation(updateMemberEducation)
|
||||
.then(() => {
|
||||
this.fetchItem();
|
||||
this.status = { status: "success" };
|
||||
})
|
||||
.catch((err) => {
|
||||
this.status = { status: "failed" };
|
||||
})
|
||||
.finally(() => {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.status = null;
|
||||
}, 2000);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
54
src/components/admin/club/member/MemberEducationListItem.vue
Normal file
54
src/components/admin/club/member/MemberEducationListItem.vue
Normal file
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
|
||||
<div class="bg-primary p-2 text-white flex flex-row gap-2 justify-between items-center">
|
||||
<p class="grow">{{ education.education }}</p>
|
||||
<PencilIcon v-if="can('update', 'club', 'member')" class="w-5 h-5 cursor-pointer" @click="openEditModal" />
|
||||
<TrashIcon v-if="can('delete', 'club', 'member')" class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<p>
|
||||
besucht: {{ education.start }} <span v-if="education.end">bis {{ education.end }}</span>
|
||||
</p>
|
||||
<p v-if="education.place">Ort: {{ education.place }}</p>
|
||||
<p v-if="education.note">Notiz: {{ education.note }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import type { MemberEducationViewModel } from "@/viewmodels/admin/club/member/memberEducation.models";
|
||||
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
education: {
|
||||
type: Object as PropType<MemberEducationViewModel>,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
openEditModal() {
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberEducationEditModal.vue"))),
|
||||
this.education.id
|
||||
);
|
||||
},
|
||||
openDeleteModal() {
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberEducationDeleteModal.vue"))),
|
||||
this.education.id
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -1,17 +1,19 @@
|
|||
<template>
|
||||
<RouterLink
|
||||
<RouterLink
|
||||
:to="{ name: 'admin-club-member-overview', params: { memberId: member.id } }"
|
||||
class="flex flex-col h-fit w-full border border-primary rounded-md"
|
||||
>
|
||||
<div
|
||||
class="bg-primary p-2 text-white flex flex-row justify-between items-center"
|
||||
>
|
||||
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
||||
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<p v-if="member.internalId">Interne ID: {{ member.internalId }}</p>
|
||||
<p v-if="member.note">Notiz: {{ member.note }}</p>
|
||||
<p>beigetreten: {{ member.firstMembershipEntry?.start }}</p>
|
||||
<p v-if="member.lastMembershipEntry?.end">ausgetreten: {{ member.lastMembershipEntry?.end }}, da {{member.lastMembershipEntry?.terminationReason ?? '- kein Grund angegeben'}}</p>
|
||||
<p v-if="member.lastMembershipEntry?.end">
|
||||
ausgetreten: {{ member.lastMembershipEntry?.end }}, da
|
||||
{{ member.lastMembershipEntry?.terminationReason ?? "- kein Grund angegeben" }}
|
||||
</p>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
|
|
@ -36,8 +36,8 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
|||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { useProtocolStore } from "@/stores/admin/club/protocol/protocol";
|
||||
import type { CreateProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models";
|
||||
import { useNewsletterStore } from "../../../../stores/admin/club/newsletter/newsletter";
|
||||
import type { CreateNewsletterViewModel } from "../../../../viewmodels/admin/club/newsletter/newsletter.models";
|
||||
import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter";
|
||||
import type { CreateNewsletterViewModel } from "@/viewmodels/admin/club/newsletter/newsletter.models";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -63,11 +63,12 @@ export default defineComponent({
|
|||
};
|
||||
this.status = "loading";
|
||||
this.createNewsletter(createNewsletter)
|
||||
.then(() => {
|
||||
.then((res) => {
|
||||
this.status = { status: "success" };
|
||||
this.timeout = setTimeout(() => {
|
||||
(this.$refs.form as HTMLFormElement).reset();
|
||||
this.closeModal();
|
||||
this.$router.push({ name: "admin-club-newsletter-overview", params: { newsletterId: res.data } });
|
||||
}, 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
|
@ -4,7 +4,15 @@
|
|||
:to="{ name: 'admin-club-newsletter-overview', params: { newsletterId: newsletter.id } }"
|
||||
class="bg-primary p-2 text-white flex flex-row justify-between items-center"
|
||||
>
|
||||
<p>{{ newsletter.title }}</p>
|
||||
<p>
|
||||
{{ newsletter.title }} ({{
|
||||
new Date(newsletter.createdAt).toLocaleDateString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
})
|
||||
}})
|
||||
</p>
|
||||
<PaperAirplaneIcon v-if="newsletter.isSent" class="w-5 h-5" />
|
||||
</RouterLink>
|
||||
<div class="p-2 max-h-48 overflow-y-auto">
|
||||
|
|
|
@ -66,11 +66,12 @@ export default defineComponent({
|
|||
};
|
||||
this.status = "loading";
|
||||
this.createProtocol(createProtocol)
|
||||
.then(() => {
|
||||
.then((res) => {
|
||||
this.status = { status: "success" };
|
||||
this.timeout = setTimeout(() => {
|
||||
(this.$refs.form as HTMLFormElement).reset();
|
||||
this.closeModal();
|
||||
this.$router.push({ name: "admin-club-protocol-overview", params: { protocolId: res.data } });
|
||||
}, 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
<template>
|
||||
<div class="w-full md:max-w-md">
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-xl font-medium">Aus-/Fortbildung erstellen</p>
|
||||
</div>
|
||||
<br />
|
||||
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
|
||||
<div>
|
||||
<label for="education">Bezeichnung</label>
|
||||
<input type="text" id="education" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="description">Beschreibung (optional)</label>
|
||||
<input type="text" id="description" />
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
|
||||
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="flex flex-row justify-end">
|
||||
<div class="flex flex-row gap-4 py-2">
|
||||
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||
abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { useEducationStore } from "@/stores/admin/configuration/education";
|
||||
import type { CreateEducationViewModel } from "@/viewmodels/admin/configuration/education.models";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||
timeout: undefined as any,
|
||||
};
|
||||
},
|
||||
beforeUnmount() {
|
||||
try {
|
||||
clearTimeout(this.timeout);
|
||||
} catch (error) {}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["closeModal"]),
|
||||
...mapActions(useEducationStore, ["createEducation"]),
|
||||
triggerCreate(e: any) {
|
||||
let formData = e.target.elements;
|
||||
let createEducation: CreateEducationViewModel = {
|
||||
education: formData.education.value,
|
||||
description: formData.description.value,
|
||||
};
|
||||
this.status = "loading";
|
||||
this.createEducation(createEducation)
|
||||
.then(() => {
|
||||
this.status = { status: "success" };
|
||||
this.timeout = setTimeout(() => {
|
||||
this.closeModal();
|
||||
}, 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<div class="w-full md:max-w-md">
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-xl font-medium">Aus-/Fortbildung {{ education?.education }} löschen?</p>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<button primary :disabled="status == 'loading' || status?.status == 'success'" @click="triggerDelete">
|
||||
unwiederuflich löschen
|
||||
</button>
|
||||
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end">
|
||||
<div class="flex flex-row gap-4 py-2">
|
||||
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||
abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { useEducationStore } from "@/stores/admin/configuration/education";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||
timeout: undefined as any,
|
||||
};
|
||||
},
|
||||
beforeUnmount() {
|
||||
try {
|
||||
clearTimeout(this.timeout);
|
||||
} catch (error) {}
|
||||
},
|
||||
computed: {
|
||||
...mapState(useModalStore, ["data"]),
|
||||
...mapState(useEducationStore, ["educations"]),
|
||||
education() {
|
||||
return this.educations.find((r) => r.id == this.data);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["closeModal"]),
|
||||
...mapActions(useEducationStore, ["deleteEducation"]),
|
||||
triggerDelete() {
|
||||
this.status = "loading";
|
||||
this.deleteEducation(this.data)
|
||||
.then(() => {
|
||||
this.status = { status: "success" };
|
||||
this.timeout = setTimeout(() => {
|
||||
this.closeModal();
|
||||
}, 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
|
||||
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
||||
<p>{{ education.education }}</p>
|
||||
<div class="flex flex-row">
|
||||
<RouterLink
|
||||
v-if="can('update', 'configuration', 'education')"
|
||||
:to="{ name: 'admin-configuration-education-edit', params: { id: education.id } }"
|
||||
>
|
||||
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
||||
</RouterLink>
|
||||
<div v-if="can('delete', 'configuration', 'education')" @click="openDeleteModal">
|
||||
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col p-2">
|
||||
<div class="flex flex-row gap-2">
|
||||
<p class="min-w-16">Beschreibung:</p>
|
||||
<p class="grow overflow-hidden">{{ education.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, defineAsyncComponent, markRaw, type PropType } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import type { EducationViewModel } from "@/viewmodels/admin/configuration/education.models";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
education: { type: Object as PropType<EducationViewModel>, default: {} },
|
||||
},
|
||||
computed: {
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
openDeleteModal() {
|
||||
this.openModal(
|
||||
markRaw(
|
||||
defineAsyncComponent(() => import("@/components/admin/configuration/education/DeleteEducationModal.vue"))
|
||||
),
|
||||
this.education.id
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -20,7 +20,7 @@ import { mapState, mapActions } from "pinia";
|
|||
import { ArchiveBoxArrowDownIcon, ArrowDownTrayIcon, BarsArrowUpIcon } from "@heroicons/vue/24/outline";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import { useBackupStore } from "../../../../stores/admin/management/backup";
|
||||
import { useBackupStore } from "@/stores/admin/management/backup";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -57,9 +57,9 @@ import Spinner from "@/components/Spinner.vue";
|
|||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { useBackupStore } from "@/stores/admin/management/backup";
|
||||
import type { BackupRestoreViewModel } from "../../../../viewmodels/admin/management/backup.models";
|
||||
import type { BackupRestoreViewModel } from "@/viewmodels/admin/management/backup.models";
|
||||
import { InformationCircleIcon } from "@heroicons/vue/24/outline";
|
||||
import { backupSections, type BackupSection } from "../../../../types/backupTypes";
|
||||
import { backupSections, type BackupSection } from "@/types/backupTypes";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -58,7 +58,7 @@ export default defineComponent({
|
|||
},
|
||||
{
|
||||
key: "app.show_link_to_calendar",
|
||||
value: formData.show_link_to_calendar.checked || null,
|
||||
value: formData.show_link_to_calendar.checked,
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
|
|
@ -39,7 +39,7 @@ import Spinner from "@/components/Spinner.vue";
|
|||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { useWebapiStore } from "@/stores/admin/management/webapi";
|
||||
import type { CreateWebapiViewModel } from "../../../../viewmodels/admin/management/webapi.models";
|
||||
import type { CreateWebapiViewModel } from "@/viewmodels/admin/management/webapi.models";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -28,7 +28,7 @@ import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
|||
import TextCopy from "@/components/TextCopy.vue";
|
||||
import { CalendarDaysIcon, InformationCircleIcon } from "@heroicons/vue/24/outline";
|
||||
import { host } from "@/serverCom";
|
||||
import { useWebapiStore } from "../../../../stores/admin/management/webapi";
|
||||
import { useWebapiStore } from "@/stores/admin/management/webapi";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -149,7 +149,6 @@ export default defineComponent({
|
|||
this.value.id = uuid();
|
||||
}
|
||||
if (!this.value.type) {
|
||||
console.log("setting type");
|
||||
this.type = "defined";
|
||||
}
|
||||
},
|
||||
|
|
|
@ -34,7 +34,7 @@ import Spinner from "@/components/Spinner.vue";
|
|||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { mapActions } from "pinia";
|
||||
import { useSetupStore } from "../../stores/setup";
|
||||
import { useSetupStore } from "@/stores/setup";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -32,7 +32,7 @@ import Spinner from "@/components/Spinner.vue";
|
|||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { mapActions } from "pinia";
|
||||
import { useSetupStore } from "../../stores/setup";
|
||||
import { useSetupStore } from "@/stores/setup";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -63,7 +63,7 @@ import Spinner from "@/components/Spinner.vue";
|
|||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { mapActions } from "pinia";
|
||||
import { useSetupStore } from "../../stores/setup";
|
||||
import { useSetupStore } from "@/stores/setup";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -37,7 +37,7 @@ import Spinner from "@/components/Spinner.vue";
|
|||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { mapActions } from "pinia";
|
||||
import { useSetupStore } from "../../stores/setup";
|
||||
import { useSetupStore } from "@/stores/setup";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -57,7 +57,7 @@ import Spinner from "@/components/Spinner.vue";
|
|||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { mapActions } from "pinia";
|
||||
import { useSetupStore } from "../../stores/setup";
|
||||
import { useSetupStore } from "@/stores/setup";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { joinTableFormatter, type FieldType, type QueryResult } from "../types/dynamicQueries";
|
||||
import { joinTableFormatter, type FieldType, type QueryResult } from "@/types/dynamicQueries";
|
||||
|
||||
export function joinTableName(name: string): string {
|
||||
let normalized = joinTableFormatter[name];
|
||||
|
|
|
@ -12,7 +12,14 @@ export async function abilityAndNavUpdate(to: any, from: any, next: any) {
|
|||
let section = to.meta.section;
|
||||
let module = to.meta.module;
|
||||
|
||||
if ((admin && ability.isAdmin()) || ability.can(type, section, module)) {
|
||||
if (to.name == "admin-default") {
|
||||
navigation.activeNavigation = "club";
|
||||
navigation.activeLink = null;
|
||||
navigation.updateTopLevel();
|
||||
navigation.updateNavigation();
|
||||
NProgress.done();
|
||||
next();
|
||||
} else if ((admin && ability.isAdmin()) || ability.can(type, section, module)) {
|
||||
NProgress.done();
|
||||
navigation.activeNavigation = to.name.split("-")[1];
|
||||
navigation.activeLink = to.name.split("-")[2];
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useBackupStore } from "../stores/admin/management/backup";
|
||||
import { useBackupStore } from "@/stores/admin/management/backup";
|
||||
|
||||
export async function setBackupPage(to: any, from: any, next: any) {
|
||||
const backup = useBackupStore();
|
||||
|
|
|
@ -15,7 +15,7 @@ const router = createRouter({
|
|||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
redirect: { name: "admin" },
|
||||
redirect: { name: "admin-default" },
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
|
@ -76,12 +76,13 @@ const router = createRouter({
|
|||
path: "/admin",
|
||||
name: "admin",
|
||||
component: () => import("@/views/admin/View.vue"),
|
||||
beforeEnter: [isAuthenticated],
|
||||
beforeEnter: [isAuthenticated, abilityAndNavUpdate],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "admin-default",
|
||||
component: () => import("@/views/admin/ViewSelect.vue"),
|
||||
beforeEnter: [abilityAndNavUpdate],
|
||||
},
|
||||
{
|
||||
path: "club",
|
||||
|
@ -141,6 +142,12 @@ const router = createRouter({
|
|||
component: () => import("@/views/admin/club/members/MemberAwards.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "educations",
|
||||
name: "admin-club-member-educations",
|
||||
component: () => import("@/views/admin/club/members/MemberEducations.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "qualifications",
|
||||
name: "admin-club-member-qualifications",
|
||||
|
@ -360,6 +367,30 @@ const router = createRouter({
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "education",
|
||||
name: "admin-configuration-education-route",
|
||||
component: () => import("@/views/RouterView.vue"),
|
||||
meta: { type: "read", section: "configuration", module: "education" },
|
||||
beforeEnter: [abilityAndNavUpdate],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "admin-configuration-education",
|
||||
component: () => import("@/views/admin/configuration/education/Education.vue"),
|
||||
meta: { type: "read", section: "configuration", module: "education" },
|
||||
beforeEnter: [abilityAndNavUpdate],
|
||||
},
|
||||
{
|
||||
path: ":id/edit",
|
||||
name: "admin-configuration-education-edit",
|
||||
component: () => import("@/views/admin/configuration/education/EducationEdit.vue"),
|
||||
meta: { type: "update", section: "configuration", module: "education" },
|
||||
beforeEnter: [abilityAndNavUpdate],
|
||||
props: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "executive-position",
|
||||
name: "admin-configuration-executive_position-route",
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useMemberAwardStore } from "@/stores/admin/club/member/memberAward";
|
|||
import { useMemberExecutivePositionStore } from "@/stores/admin/club/member/memberExecutivePosition";
|
||||
import { useMemberQualificationStore } from "@/stores/admin/club/member/memberQualification";
|
||||
import { useMembershipStore } from "@/stores/admin/club/member/membership";
|
||||
import { useMemberEducationStore } from "../stores/admin/club/member/memberEducation";
|
||||
|
||||
export async function setMemberId(to: any, from: any, next: any) {
|
||||
const member = useMemberStore();
|
||||
|
@ -14,6 +15,7 @@ export async function setMemberId(to: any, from: any, next: any) {
|
|||
useMemberAwardStore().$reset();
|
||||
useMemberExecutivePositionStore().$reset();
|
||||
useMemberQualificationStore().$reset();
|
||||
useMemberEducationStore().$reset();
|
||||
|
||||
next();
|
||||
}
|
||||
|
@ -28,6 +30,7 @@ export async function resetMemberStores(to: any, from: any, next: any) {
|
|||
useMemberAwardStore().$reset();
|
||||
useMemberExecutivePositionStore().$reset();
|
||||
useMemberQualificationStore().$reset();
|
||||
useMemberEducationStore().$reset();
|
||||
|
||||
next();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter";
|
||||
import { useNewsletterDatesStore } from "@/stores/admin/club/newsletter/newsletterDates";
|
||||
import { useNewsletterRecipientsStore } from "@/stores/admin/club/newsletter/newsletterRecipients";
|
||||
import { useNewsletterPrintoutStore } from "../stores/admin/club/newsletter/newsletterPrintout";
|
||||
import { useNewsletterPrintoutStore } from "@/stores/admin/club/newsletter/newsletterPrintout";
|
||||
|
||||
export async function setNewsletterId(to: any, from: any, next: any) {
|
||||
const newsletter = useNewsletterStore();
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useProtocolAgendaStore } from "@/stores/admin/club/protocol/protocolAge
|
|||
import { useProtocolDecisionStore } from "@/stores/admin/club/protocol/protocolDecision";
|
||||
import { useProtocolPresenceStore } from "@/stores/admin/club/protocol/protocolPresence";
|
||||
import { useProtocolVotingStore } from "@/stores/admin/club/protocol/protocolVoting";
|
||||
import { useProtocolPrintoutStore } from "../stores/admin/club/protocol/protocolPrintout";
|
||||
import { useProtocolPrintoutStore } from "@/stores/admin/club/protocol/protocolPrintout";
|
||||
|
||||
export async function setProtocolId(to: any, from: any, next: any) {
|
||||
const protocol = useProtocolStore();
|
||||
|
|
|
@ -11,21 +11,18 @@ export const useAbilityStore = defineStore("ability", {
|
|||
getters: {
|
||||
can:
|
||||
(state) =>
|
||||
(type: PermissionType | "admin", section: PermissionSection, module?: PermissionModule): boolean => {
|
||||
(type: PermissionType | "admin", section: PermissionSection, module: PermissionModule): boolean => {
|
||||
const permissions = state.permissions;
|
||||
if (state.isOwner) return true;
|
||||
if (type == "admin") return permissions?.admin ?? false;
|
||||
if (permissions?.admin) return true;
|
||||
if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false;
|
||||
if (permissions?.admin || permissions?.adminByOwner) return true;
|
||||
if (
|
||||
(!module &&
|
||||
permissions[section] != undefined &&
|
||||
(permissions[section]?.all == "*" || permissions[section]?.all?.includes(type))) ||
|
||||
permissions[section]?.all == "*" ||
|
||||
permissions[section]?.all?.includes(type)
|
||||
permissions[section]?.all?.includes(type) ||
|
||||
permissions[section]?.[module] == "*" ||
|
||||
permissions[section]?.[module]?.includes(type)
|
||||
)
|
||||
return true;
|
||||
if (module && (permissions[section]?.[module] == "*" || permissions[section]?.[module]?.includes(type)))
|
||||
return true;
|
||||
return false;
|
||||
},
|
||||
canSection:
|
||||
|
@ -33,16 +30,24 @@ export const useAbilityStore = defineStore("ability", {
|
|||
(type: PermissionType | "admin", section: PermissionSection): boolean => {
|
||||
const permissions = state.permissions;
|
||||
if (state.isOwner) return true;
|
||||
if (type == "admin") return permissions?.admin ?? false;
|
||||
if (permissions?.admin) return true;
|
||||
if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false;
|
||||
if (permissions?.admin || permissions?.adminByOwner) return true;
|
||||
if (
|
||||
permissions[section]?.all == "*" ||
|
||||
permissions[section]?.all?.includes(type) ||
|
||||
(permissions[section]?.all == "*" || permissions[section]?.all?.includes(type)) &&
|
||||
permissions[section] != undefined
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
},
|
||||
canAccessSection:
|
||||
(state) =>
|
||||
(section: PermissionSection): boolean => {
|
||||
const permissions = state.permissions;
|
||||
if (state.isOwner) return true;
|
||||
if (permissions?.admin || permissions?.adminByOwner) return true;
|
||||
if (permissions[section] != undefined) return true;
|
||||
return false;
|
||||
},
|
||||
isAdmin: (state) => (): boolean => {
|
||||
const permissions = state.permissions;
|
||||
if (state.isOwner) return true;
|
||||
|
@ -54,23 +59,41 @@ export const useAbilityStore = defineStore("ability", {
|
|||
permissions: PermissionObject,
|
||||
type: PermissionType | "admin",
|
||||
section: PermissionSection,
|
||||
module?: PermissionModule
|
||||
module: PermissionModule
|
||||
): boolean => {
|
||||
// ignores ownership
|
||||
if (type == "admin") return permissions?.admin ?? false;
|
||||
if (permissions?.admin) return true;
|
||||
if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false;
|
||||
if (permissions?.admin || permissions?.adminByOwner) return true;
|
||||
if (
|
||||
(!module &&
|
||||
permissions[section] != undefined &&
|
||||
(permissions[section]?.all == "*" || permissions[section]?.all?.includes(type))) ||
|
||||
permissions[section]?.all == "*" ||
|
||||
permissions[section]?.all?.includes(type)
|
||||
permissions[section]?.all?.includes(type) ||
|
||||
permissions[section]?.[module] == "*" ||
|
||||
permissions[section]?.[module]?.includes(type)
|
||||
)
|
||||
return true;
|
||||
if (module && (permissions[section]?.[module] == "*" || permissions[section]?.[module]?.includes(type)))
|
||||
return false;
|
||||
},
|
||||
_canSection:
|
||||
() =>
|
||||
(permissions: PermissionObject, type: PermissionType | "admin", section: PermissionSection): boolean => {
|
||||
// ignores ownership
|
||||
if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false;
|
||||
if (permissions?.admin || permissions?.adminByOwner) return true;
|
||||
if (
|
||||
(permissions[section]?.all == "*" || permissions[section]?.all?.includes(type)) &&
|
||||
permissions[section] != undefined
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
},
|
||||
_canAccessSection:
|
||||
() =>
|
||||
(permissions: PermissionObject, section: PermissionSection): boolean => {
|
||||
// ignores ownership
|
||||
if (permissions?.admin || permissions?.adminByOwner) return true;
|
||||
if (permissions[section] != undefined) return true;
|
||||
return false;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setAbility(permissions: PermissionObject, isOwner: boolean) {
|
||||
|
|
|
@ -106,6 +106,7 @@ export const useMemberStore = defineStore("member", {
|
|||
nameaffix: member.nameaffix,
|
||||
birthdate: member.birthdate,
|
||||
internalId: member.internalId,
|
||||
note: member.note,
|
||||
});
|
||||
this.fetchMembers();
|
||||
return result;
|
||||
|
@ -118,6 +119,7 @@ export const useMemberStore = defineStore("member", {
|
|||
nameaffix: member.nameaffix,
|
||||
birthdate: member.birthdate,
|
||||
internalId: member.internalId,
|
||||
note: member.note,
|
||||
});
|
||||
this.fetchMembers();
|
||||
return result;
|
||||
|
|
67
src/stores/admin/club/member/memberEducation.ts
Normal file
67
src/stores/admin/club/member/memberEducation.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http } from "@/serverCom";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { useMemberStore } from "./member";
|
||||
import type {
|
||||
CreateMemberEducationViewModel,
|
||||
MemberEducationViewModel,
|
||||
UpdateMemberEducationViewModel,
|
||||
} from "@/viewmodels/admin/club/member/memberEducation.models";
|
||||
|
||||
export const useMemberEducationStore = defineStore("memberEducation", {
|
||||
state: () => {
|
||||
return {
|
||||
memberEducations: [] as Array<MemberEducationViewModel>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
fetchMemberEducationsForMember() {
|
||||
const memberId = useMemberStore().activeMember;
|
||||
this.loading = "loading";
|
||||
http
|
||||
.get(`/admin/member/${memberId}/educations`)
|
||||
.then((result) => {
|
||||
this.memberEducations = result.data;
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
fetchMemberEducationById(id: number) {
|
||||
const memberId = useMemberStore().activeMember;
|
||||
return http.get(`/admin/member/${memberId}/education/${id}`);
|
||||
},
|
||||
async createMemberEducation(memberEducation: CreateMemberEducationViewModel): Promise<AxiosResponse<any, any>> {
|
||||
const memberId = useMemberStore().activeMember;
|
||||
const result = await http.post(`/admin/member/${memberId}/education`, {
|
||||
start: memberEducation.start,
|
||||
end: memberEducation.end,
|
||||
place: memberEducation.place,
|
||||
note: memberEducation.note,
|
||||
educationId: memberEducation.educationId,
|
||||
});
|
||||
this.fetchMemberEducationsForMember();
|
||||
return result;
|
||||
},
|
||||
async updateMemberEducation(memberEducation: UpdateMemberEducationViewModel): Promise<AxiosResponse<any, any>> {
|
||||
const memberId = useMemberStore().activeMember;
|
||||
const result = await http.patch(`/admin/member/${memberId}/education/${memberEducation.id}`, {
|
||||
start: memberEducation.start,
|
||||
end: memberEducation.end,
|
||||
place: memberEducation.place,
|
||||
note: memberEducation.note,
|
||||
educationId: memberEducation.educationId,
|
||||
});
|
||||
this.fetchMemberEducationsForMember();
|
||||
return result;
|
||||
},
|
||||
async deleteMemberEducation(memberEducation: number): Promise<AxiosResponse<any, any>> {
|
||||
const memberId = useMemberStore().activeMember;
|
||||
const result = await http.delete(`/admin/member/${memberId}/education/${memberEducation}`);
|
||||
this.fetchMemberEducationsForMember();
|
||||
return result;
|
||||
},
|
||||
},
|
||||
});
|
|
@ -7,6 +7,7 @@ import { useMemberStore } from "./member";
|
|||
import type {
|
||||
CreateMembershipViewModel,
|
||||
MembershipStatisticsViewModel,
|
||||
MembershipTotalStatisticsViewModel,
|
||||
MembershipViewModel,
|
||||
UpdateMembershipViewModel,
|
||||
} from "@/viewmodels/admin/club/member/membership.models";
|
||||
|
@ -16,6 +17,7 @@ export const useMembershipStore = defineStore("membership", {
|
|||
return {
|
||||
memberships: [] as Array<MembershipViewModel>,
|
||||
membershipStatistics: [] as Array<MembershipStatisticsViewModel>,
|
||||
totalMembershipStatistics: undefined as undefined | MembershipTotalStatisticsViewModel,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
};
|
||||
},
|
||||
|
@ -42,6 +44,15 @@ export const useMembershipStore = defineStore("membership", {
|
|||
})
|
||||
.catch((err) => {});
|
||||
},
|
||||
fetchMembershipTotalStatisticsForMember() {
|
||||
const memberId = useMemberStore().activeMember;
|
||||
http
|
||||
.get(`/admin/member/${memberId}/memberships/totalstatistics`)
|
||||
.then((result) => {
|
||||
this.totalMembershipStatistics = result.data;
|
||||
})
|
||||
.catch((err) => {});
|
||||
},
|
||||
fetchMembershipById(id: number) {
|
||||
const memberId = useMemberStore().activeMember;
|
||||
return http.get(`/admin/member/${memberId}/membership/${id}`);
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { defineStore } from "pinia";
|
||||
import type { CreateNewsletterViewModel, SyncNewsletterViewModel } from "@/viewmodels/admin/club/newsletter/newsletter.models";
|
||||
import type { CreateNewsletterViewModel } from "@/viewmodels/admin/club/newsletter/newsletter.models";
|
||||
import { http } from "@/serverCom";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type { NewsletterViewModel } from "@/viewmodels/admin/club/newsletter/newsletter.models";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isequal";
|
||||
import difference from "lodash.difference";
|
||||
|
||||
export const useNewsletterStore = defineStore("newsletter", {
|
||||
state: () => {
|
||||
|
@ -72,7 +71,6 @@ export const useNewsletterStore = defineStore("newsletter", {
|
|||
const result = await http.post(`/admin/newsletter`, {
|
||||
title: newsletter.title,
|
||||
});
|
||||
this.fetchNewsletters();
|
||||
return result;
|
||||
},
|
||||
async synchronizeActiveNewsletter(): Promise<void> {
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http, newEventSource, streamingFetch } from "@/serverCom";
|
||||
import { http, streamingFetch } from "@/serverCom";
|
||||
import { useNewsletterStore } from "./newsletter";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type { EventSourcePolyfill } from "event-source-polyfill";
|
||||
import { useNotificationStore, type NotificationType } from "../../../notification";
|
||||
import { useNotificationStore, type NotificationType } from "@/stores/notification";
|
||||
|
||||
export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
|
||||
state: () => {
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { defineStore } from "pinia";
|
||||
import type { CreateProtocolViewModel, SyncProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models";
|
||||
import type { CreateProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models";
|
||||
import { http } from "@/serverCom";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type { ProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isequal";
|
||||
import difference from "lodash.difference";
|
||||
|
||||
export const useProtocolStore = defineStore("protocol", {
|
||||
state: () => {
|
||||
|
@ -73,7 +72,6 @@ export const useProtocolStore = defineStore("protocol", {
|
|||
title: protocol.title,
|
||||
date: protocol.date,
|
||||
});
|
||||
this.fetchProtocols();
|
||||
return result;
|
||||
},
|
||||
async synchronizeActiveProtocol(): Promise<void> {
|
||||
|
|
|
@ -71,7 +71,7 @@ export const useProtocolAgendaStore = defineStore("protocolAgenda", {
|
|||
|
||||
await http
|
||||
.patch(`/admin/protocol/${protocolId}/synchronize/agenda`, {
|
||||
agenda: differenceWith(this.agenda, this.origin, isEqual),
|
||||
agenda: this.agenda,
|
||||
})
|
||||
.then((res) => {
|
||||
this.syncingProtocolAgenda = "synced";
|
||||
|
|
|
@ -72,7 +72,7 @@ export const useProtocolDecisionStore = defineStore("protocolDecision", {
|
|||
|
||||
await http
|
||||
.patch(`/admin/protocol/${protocolId}/synchronize/decisions`, {
|
||||
decisions: differenceWith(this.decision, this.origin, isEqual),
|
||||
decisions: this.decision,
|
||||
})
|
||||
.then((res) => {
|
||||
this.syncingProtocolDecision = "synced";
|
||||
|
|
|
@ -75,7 +75,7 @@ export const useProtocolVotingStore = defineStore("protocolVoting", {
|
|||
|
||||
await http
|
||||
.patch(`/admin/protocol/${protocolId}/synchronize/votings`, {
|
||||
votings: differenceWith(this.voting, this.origin, isEqual),
|
||||
votings: this.voting,
|
||||
})
|
||||
.then((res) => {
|
||||
this.syncingProtocolVoting = "synced";
|
||||
|
|
55
src/stores/admin/configuration/education.ts
Normal file
55
src/stores/admin/configuration/education.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { defineStore } from "pinia";
|
||||
import type {
|
||||
CreateEducationViewModel,
|
||||
UpdateEducationViewModel,
|
||||
EducationViewModel,
|
||||
} from "@/viewmodels/admin/configuration/education.models";
|
||||
import { http } from "@/serverCom";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const useEducationStore = defineStore("education", {
|
||||
state: () => {
|
||||
return {
|
||||
educations: [] as Array<EducationViewModel>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
fetchEducations() {
|
||||
this.loading = "loading";
|
||||
http
|
||||
.get("/admin/education")
|
||||
.then((result) => {
|
||||
this.educations = result.data;
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
fetchEducationById(id: number): Promise<AxiosResponse<any, any>> {
|
||||
return http.get(`/admin/education/${id}`);
|
||||
},
|
||||
async createEducation(education: CreateEducationViewModel): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.post(`/admin/education`, {
|
||||
education: education.education,
|
||||
description: education.description,
|
||||
});
|
||||
this.fetchEducations();
|
||||
return result;
|
||||
},
|
||||
async updateActiveEducation(education: UpdateEducationViewModel): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.patch(`/admin/education/${education.id}`, {
|
||||
education: education.education,
|
||||
description: education.description,
|
||||
});
|
||||
this.fetchEducations();
|
||||
return result;
|
||||
},
|
||||
async deleteEducation(education: number): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.delete(`/admin/education/${education}`);
|
||||
this.fetchEducations();
|
||||
return result;
|
||||
},
|
||||
},
|
||||
});
|
|
@ -6,10 +6,10 @@ import type {
|
|||
} from "@/viewmodels/admin/configuration/query.models";
|
||||
import { http } from "@/serverCom";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { useQueryBuilderStore } from "../club/queryBuilder";
|
||||
import { useModalStore } from "../../modal";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import { defineAsyncComponent, markRaw } from "vue";
|
||||
import { useAbilityStore } from "../../ability";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
|
||||
export const useQueryStoreStore = defineStore("queryStore", {
|
||||
state: () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http } from "@/serverCom";
|
||||
import type { AxiosResponse, AxiosProgressEvent } from "axios";
|
||||
import type { BackupRestoreViewModel } from "../../../viewmodels/admin/management/backup.models";
|
||||
import type { BackupRestoreViewModel } from "@/viewmodels/admin/management/backup.models";
|
||||
|
||||
export const useBackupStore = defineStore("backup", {
|
||||
state: () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import router from "@/router";
|
||||
import type { PermissionSection } from "../../types/permissionTypes";
|
||||
import type { PermissionSection } from "@/types/permissionTypes";
|
||||
|
||||
export type navigationModel = {
|
||||
[key in topLevelNavigationType]: navigationSplitModel;
|
||||
|
@ -48,7 +48,7 @@ export const useNavigationStore = defineStore("navigation", {
|
|||
updateTopLevel() {
|
||||
const abilityStore = useAbilityStore();
|
||||
this.topLevel = [
|
||||
...(abilityStore.canSection("read", "club")
|
||||
...(abilityStore.canAccessSection("club")
|
||||
? [
|
||||
{
|
||||
key: "club",
|
||||
|
@ -57,7 +57,7 @@ export const useNavigationStore = defineStore("navigation", {
|
|||
} as topLevelNavigationModel,
|
||||
]
|
||||
: []),
|
||||
...(abilityStore.canSection("read", "configuration")
|
||||
...(abilityStore.canAccessSection("configuration")
|
||||
? [
|
||||
{
|
||||
key: "configuration",
|
||||
|
@ -66,7 +66,7 @@ export const useNavigationStore = defineStore("navigation", {
|
|||
} as topLevelNavigationModel,
|
||||
]
|
||||
: []),
|
||||
...(abilityStore.canSection("read", "management")
|
||||
...(abilityStore.canAccessSection("management")
|
||||
? [
|
||||
{
|
||||
key: "management",
|
||||
|
@ -112,6 +112,9 @@ export const useNavigationStore = defineStore("navigation", {
|
|||
...(abilityStore.can("read", "configuration", "qualification")
|
||||
? [{ key: "qualification", title: "Qualifikationen" }]
|
||||
: []),
|
||||
...(abilityStore.can("read", "configuration", "education")
|
||||
? [{ key: "education", title: "Aus-/Fortbildungen" }]
|
||||
: []),
|
||||
...(abilityStore.can("read", "configuration", "executive_position")
|
||||
? [{ key: "executive_position", title: "Vereinsämter" }]
|
||||
: []),
|
||||
|
@ -147,7 +150,8 @@ export const useNavigationStore = defineStore("navigation", {
|
|||
this.activeNavigationObject.main.findIndex((e) => e.key == this.activeLink) == -1 ||
|
||||
this.activeLink == "default"
|
||||
) {
|
||||
let link = this.activeNavigationObject.main[0].key;
|
||||
let link = this.activeNavigationObject.main.filter((m) => !m.key.startsWith("divider"))[0].key;
|
||||
this.activeLink = link;
|
||||
router.push({ name: `admin-${this.activeNavigation}-${link}` });
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http } from "../serverCom";
|
||||
import { http } from "@/serverCom";
|
||||
|
||||
export const useConfigurationStore = defineStore("configuration", {
|
||||
state: () => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http } from "../serverCom";
|
||||
import { http } from "@/serverCom";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { useConfigurationStore } from "./configuration";
|
||||
|
||||
|
|
|
@ -16,10 +16,13 @@
|
|||
<div
|
||||
class="max-w-full w-full grow flex flex-col divide-y-2 divide-gray-300 bg-white rounded-lg justify-center overflow-hidden"
|
||||
>
|
||||
<slot name="topBar"></slot>
|
||||
<div v-if="topBar || title" class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 v-if="title" class="font-bold text-xl h-8 min-h-fit">{{ title }}</h1>
|
||||
<slot name="topBar"></slot>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 grow py-5 overflow-hidden">
|
||||
<slot name="diffMain"></slot>
|
||||
<div v-if="!diffMain" class="flex flex-col gap-2 grow px-7 overflow-y-scroll">
|
||||
<div v-if="!diffMain" class="flex flex-col gap-2 grow px-7 overflow-y-auto">
|
||||
<slot name="main"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -51,6 +54,10 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(useNavigationStore, ["activeLink", "activeNavigation"]),
|
||||
|
@ -60,6 +67,9 @@ export default defineComponent({
|
|||
rootRoute() {
|
||||
return ((this.$route?.name as string) ?? "").split("-")[0];
|
||||
},
|
||||
topBar() {
|
||||
return this.$slots.topBar;
|
||||
},
|
||||
diffMain() {
|
||||
return this.$slots.diffMain;
|
||||
},
|
||||
|
|
|
@ -13,6 +13,7 @@ export type PermissionModule =
|
|||
| "communication_type"
|
||||
| "membership_status"
|
||||
| "salutation"
|
||||
| "education"
|
||||
| "calendar_type"
|
||||
| "user"
|
||||
| "role"
|
||||
|
@ -31,6 +32,7 @@ export type PermissionString =
|
|||
| `${PermissionSection}.${PermissionModule}.*` // für alle Berechtigungen in einem Modul
|
||||
| `${PermissionSection}.${PermissionType}` // für spezifische Berechtigungen in einem Abschnitt
|
||||
| `${PermissionSection}.*` // für alle Berechtigungen in einem Abschnitt
|
||||
| `additional.${string}.${string}` // additional
|
||||
| "*"; // für Admin
|
||||
|
||||
export type PermissionObject = {
|
||||
|
@ -39,10 +41,20 @@ export type PermissionObject = {
|
|||
} & { all?: Array<PermissionType> | "*" };
|
||||
} & {
|
||||
admin?: boolean;
|
||||
adminByOwner?: boolean;
|
||||
} & {
|
||||
additional?: { [key: string]: string };
|
||||
};
|
||||
|
||||
export type SectionsAndModulesObject = {
|
||||
[section in PermissionSection]: Array<PermissionModule>;
|
||||
} & {
|
||||
additional?: Array<{
|
||||
key: string;
|
||||
name: string;
|
||||
type: "number" | "string";
|
||||
emptyIfAdmin: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
export const permissionSections: Array<PermissionSection> = ["club", "configuration", "management"];
|
||||
|
@ -59,6 +71,7 @@ export const permissionModules: Array<PermissionModule> = [
|
|||
"communication_type",
|
||||
"membership_status",
|
||||
"salutation",
|
||||
"education",
|
||||
"calendar_type",
|
||||
"user",
|
||||
"role",
|
||||
|
@ -80,6 +93,7 @@ export const sectionsAndModules: SectionsAndModulesObject = {
|
|||
"communication_type",
|
||||
"membership_status",
|
||||
"salutation",
|
||||
"education",
|
||||
"calendar_type",
|
||||
"query_store",
|
||||
"template",
|
||||
|
@ -87,4 +101,7 @@ export const sectionsAndModules: SectionsAndModulesObject = {
|
|||
"newsletter_config",
|
||||
],
|
||||
management: ["user", "role", "webapi", "backup", "setting"],
|
||||
additional: [
|
||||
//{ key: "val", name: "name", type: "number", emptyIfAdmin: true },
|
||||
],
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { CalendarTypeViewModel } from "../configuration/calendarType.models";
|
||||
import type { CalendarTypeViewModel } from "@/viewmodels/admin/configuration/calendarType.models";
|
||||
|
||||
export interface CalendarViewModel {
|
||||
id: string;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { CommunicationTypeViewModel } from "../../configuration/communicationType.models";
|
||||
import type { CommunicationTypeViewModel } from "@/viewmodels/admin/configuration/communicationType.models";
|
||||
|
||||
export interface CommunicationViewModel {
|
||||
id: number;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { CommunicationViewModel } from "./communication.models";
|
||||
import type { MembershipViewModel } from "./membership.models";
|
||||
import type { SalutationViewModel } from "../../configuration/salutation.models";
|
||||
import type { SalutationViewModel } from "@/viewmodels/admin/configuration/salutation.models";
|
||||
|
||||
export interface MemberViewModel {
|
||||
id: string;
|
||||
|
@ -15,6 +15,7 @@ export interface MemberViewModel {
|
|||
sendNewsletter?: CommunicationViewModel;
|
||||
smsAlarming?: Array<CommunicationViewModel>;
|
||||
preferredCommunication?: Array<CommunicationViewModel>;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface MemberStatisticsViewModel {
|
||||
|
@ -36,6 +37,7 @@ export interface CreateMemberViewModel {
|
|||
nameaffix: string;
|
||||
birthdate: Date;
|
||||
internalId?: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface UpdateMemberViewModel {
|
||||
|
@ -46,4 +48,5 @@ export interface UpdateMemberViewModel {
|
|||
nameaffix: string;
|
||||
birthdate: Date;
|
||||
internalId?: string;
|
||||
note?: string;
|
||||
}
|
||||
|
|
26
src/viewmodels/admin/club/member/memberEducation.models.ts
Normal file
26
src/viewmodels/admin/club/member/memberEducation.models.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
export interface MemberEducationViewModel {
|
||||
id: number;
|
||||
start: Date;
|
||||
end?: Date;
|
||||
place?: string;
|
||||
note?: string;
|
||||
education: string;
|
||||
educationId: number;
|
||||
}
|
||||
|
||||
export interface CreateMemberEducationViewModel {
|
||||
start: Date;
|
||||
end?: Date;
|
||||
place?: string;
|
||||
note?: string;
|
||||
educationId: number;
|
||||
}
|
||||
|
||||
export interface UpdateMemberEducationViewModel {
|
||||
id: number;
|
||||
start: Date;
|
||||
end?: Date;
|
||||
place?: string;
|
||||
note?: string;
|
||||
educationId: number;
|
||||
}
|
|
@ -21,6 +21,18 @@ export interface MembershipStatisticsViewModel {
|
|||
memberBirthdate: Date;
|
||||
}
|
||||
|
||||
export interface MembershipTotalStatisticsViewModel {
|
||||
durationInDays: number;
|
||||
durationInYears: number;
|
||||
exactDuration: string;
|
||||
memberId: string;
|
||||
memberSalutation: string;
|
||||
memberFirstname: string;
|
||||
memberLastname: string;
|
||||
memberNameaffix: string;
|
||||
memberBirthdate: Date;
|
||||
}
|
||||
|
||||
export interface CreateMembershipViewModel {
|
||||
start: Date;
|
||||
statusId: number;
|
||||
|
|
|
@ -7,6 +7,7 @@ export interface NewsletterViewModel {
|
|||
newsletterSignatur: string;
|
||||
isSent: boolean;
|
||||
recipientsByQueryId?: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface CreateNewsletterViewModel {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { CalendarViewModel } from "../calendar.models";
|
||||
import type { CalendarViewModel } from "@/viewmodels/admin/club/calendar.models";
|
||||
|
||||
export interface NewsletterDatesViewModel {
|
||||
newsletterId: number;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { MemberViewModel } from "../member/member.models";
|
||||
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||
|
||||
export interface NewsletterRecipientsViewModel {
|
||||
newsletterId: number;
|
||||
|
|
16
src/viewmodels/admin/configuration/education.models.ts
Normal file
16
src/viewmodels/admin/configuration/education.models.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
export interface EducationViewModel {
|
||||
id: number;
|
||||
education: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export interface CreateEducationViewModel {
|
||||
education: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export interface UpdateEducationViewModel {
|
||||
id: number;
|
||||
education: string;
|
||||
description: string | null;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import type { BackupSection } from "../../../types/backupTypes";
|
||||
import type { BackupSection } from "@/types/backupTypes";
|
||||
|
||||
export interface BackupRestoreViewModel {
|
||||
filename: string;
|
||||
|
|
|
@ -65,6 +65,11 @@
|
|||
</div>
|
||||
<p v-if="loginError" class="text-center">{{ loginError }}</p>
|
||||
</form>
|
||||
<div class="flex flex-col gap-2 empty:hidden">
|
||||
<RouterLink v-if="appShow_link_to_calendar" :to="{ name: 'public-calendar' }" button primary-outline>
|
||||
zum Kalender
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<FormBottomBar />
|
||||
</div>
|
||||
|
@ -76,12 +81,11 @@ import { defineComponent } from "vue";
|
|||
import Spinner from "@/components/Spinner.vue";
|
||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { resetAllPiniaStores } from "@/helpers/piniaReset";
|
||||
import FormBottomBar from "@/components/FormBottomBar.vue";
|
||||
import AppLogo from "@/components/AppLogo.vue";
|
||||
import { mapState } from "pinia";
|
||||
import { useConfigurationStore } from "@/stores/configuration";
|
||||
import { hashString } from "../helpers/crypto";
|
||||
import { hashString } from "@/helpers/crypto";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -96,10 +100,9 @@ export default defineComponent({
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useConfigurationStore, ["clubName"]),
|
||||
...mapState(useConfigurationStore, ["clubName", "appShow_link_to_calendar"]),
|
||||
},
|
||||
mounted() {
|
||||
resetAllPiniaStores();
|
||||
this.username = localStorage.getItem("username") ?? "";
|
||||
this.routine = localStorage.getItem("routine") ?? "";
|
||||
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
<template>
|
||||
<MainTemplate :useStagedOverviewLink="false">
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Administration übertragen</h1>
|
||||
</div>
|
||||
</template>
|
||||
<MainTemplate title="Administration übertragen" :useStagedOverviewLink="false">
|
||||
<template #main>
|
||||
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
<template>
|
||||
<MainTemplate :useStagedOverviewLink="false">
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Meine Anmeldedaten</h1>
|
||||
</div>
|
||||
</template>
|
||||
<MainTemplate title="Meine Anmeldedaten" :useStagedOverviewLink="false">
|
||||
<template #diffMain>
|
||||
<Spinner v-if="loading" class="mx-auto" />
|
||||
<div v-else class="flex flex-col w-full h-full gap-2 px-7 overflow-hidden">
|
||||
|
@ -54,10 +49,10 @@ import Spinner from "@/components/Spinner.vue";
|
|||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import TextCopy from "@/components/TextCopy.vue";
|
||||
import TotpCheckAndScan from "../../components/account/TotpCheckAndScan.vue";
|
||||
import PasswordChange from "../../components/account/PasswordChange.vue";
|
||||
import ChangeToPassword from "../../components/account/ChangeToPassword.vue";
|
||||
import ChangeToTOTP from "../../components/account/ChangeToTOTP.vue";
|
||||
import TotpCheckAndScan from "@/components/account/TotpCheckAndScan.vue";
|
||||
import PasswordChange from "@/components/account/PasswordChange.vue";
|
||||
import ChangeToPassword from "@/components/account/ChangeToPassword.vue";
|
||||
import ChangeToTOTP from "@/components/account/ChangeToTOTP.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
<template>
|
||||
<MainTemplate :useStagedOverviewLink="false">
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Mein Account</h1>
|
||||
</div>
|
||||
</template>
|
||||
<MainTemplate title="Mein Account" :useStagedOverviewLink="false">
|
||||
<template #main>
|
||||
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
<template>
|
||||
<MainTemplate :useStagedOverviewLink="false">
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Meine Berechtigungen</h1>
|
||||
</div>
|
||||
</template>
|
||||
<MainTemplate title="Meine Berechtigungen" :useStagedOverviewLink="false">
|
||||
<template #main>
|
||||
<Permission :permissions="permissions" :disableEdit="true" />
|
||||
<Permission :permissions="permissions" disableEdit />
|
||||
</template>
|
||||
</MainTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, markRaw, defineAsyncComponent } from "vue";
|
||||
import { defineComponent } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import MainTemplate from "@/templates/Main.vue";
|
||||
import Permission from "@/components/admin/Permission.vue";
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<MainTemplate title="Kalender">
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Kalender</h1>
|
||||
<div class="flex flex-row gap-2">
|
||||
<PlusIcon
|
||||
class="text-gray-500 h-5 w-5 cursor-pointer"
|
||||
@click="select({ start: '', end: '', allDay: false })"
|
||||
/>
|
||||
<LinkIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="openLinkModal" />
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<PlusIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="select({ start: '', end: '', allDay: false })" />
|
||||
<LinkIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="openLinkModal" />
|
||||
</div>
|
||||
</template>
|
||||
<template #diffMain>
|
||||
|
|
|
@ -1,84 +1,77 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Liste Drucken</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col w-full h-full gap-2 px-7 overflow-y-auto">
|
||||
<form
|
||||
class="flex flex-col h-fit w-full border border-primary rounded-md p-2 gap-2"
|
||||
@submit.prevent="sendPrintJob"
|
||||
>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<p class="min-w-16">Titel:</p>
|
||||
<input id="title" type="text" required />
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<p class="min-w-16">Query:</p>
|
||||
<select id="query" value="member">
|
||||
<option value="member">(system) alle Mitglieder</option>
|
||||
<option value="memberByRunningMembership">(system) alle Mitglieder mit laufender Mitgliedschaft</option>
|
||||
<option v-for="query in queries" :key="query.id" :value="query.id">
|
||||
{{ query.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-2 md:items-center">
|
||||
<div class="flex flex-row w-full gap-2 items-center">
|
||||
<p class="min-w-16">Kopfzeile:</p>
|
||||
<select id="header" value="def">
|
||||
<option value="def">Standard-Vorlage verwenden</option>
|
||||
<option v-for="template in templates" :key="template.id" :value="template.id">
|
||||
{{ template.template }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<p class="whitespace-nowrap">Höhe [mm]:</p>
|
||||
<input id="headerHeight" type="number" :min="15" class="w-24!" placeholder="15" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<p class="min-w-16">Hauptteil:</p>
|
||||
<select id="body" value="def">
|
||||
<MainTemplate title="Liste Drucken">
|
||||
<template #main>
|
||||
<form
|
||||
class="flex flex-col h-fit w-full border border-primary rounded-md p-2 gap-2"
|
||||
@submit.prevent="sendPrintJob"
|
||||
>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<p class="min-w-16">Titel:</p>
|
||||
<input id="title" type="text" required />
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<p class="min-w-16">Query:</p>
|
||||
<select id="query" value="member">
|
||||
<option value="member">(system) alle Mitglieder</option>
|
||||
<option value="memberByRunningMembership">(system) alle Mitglieder mit laufender Mitgliedschaft</option>
|
||||
<option v-for="query in queries" :key="query.id" :value="query.id">
|
||||
{{ query.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-2 md:items-center">
|
||||
<div class="flex flex-row w-full gap-2 items-center">
|
||||
<p class="min-w-16">Kopfzeile:</p>
|
||||
<select id="header" value="def">
|
||||
<option value="def">Standard-Vorlage verwenden</option>
|
||||
<option value="listprint.member">(system) Mitgliederliste</option>
|
||||
<option v-for="template in templates" :key="template.id" :value="template.id">
|
||||
{{ template.template }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-2 md:items-center">
|
||||
<div class="flex flex-row w-full gap-2 items-center">
|
||||
<p class="min-w-16">Fußzeile:</p>
|
||||
<select id="footer" value="def">
|
||||
<option value="def">Standard-Vorlage verwenden</option>
|
||||
<option v-for="template in templates" :key="template.id" :value="template.id">
|
||||
{{ template.template }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<p class="whitespace-nowrap">Höhe [mm]:</p>
|
||||
<input id="footerHeight" type="number" :min="15" class="w-24!" placeholder="15" />
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<p class="whitespace-nowrap">Höhe [mm]:</p>
|
||||
<input id="headerHeight" type="number" :min="15" class="w-24!" placeholder="15" />
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<button type="submit" primary class="w-fit!">Liste drucken</button>
|
||||
<button type="reset" primary-outline class="w-fit!">zurücksetzen</button>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<p class="min-w-16">Hauptteil:</p>
|
||||
<select id="body" value="def">
|
||||
<option value="def">Standard-Vorlage verwenden</option>
|
||||
<option value="listprint.member">(system) Mitgliederliste</option>
|
||||
<option v-for="template in templates" :key="template.id" :value="template.id">
|
||||
{{ template.template }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-2 md:items-center">
|
||||
<div class="flex flex-row w-full gap-2 items-center">
|
||||
<p class="min-w-16">Fußzeile:</p>
|
||||
<select id="footer" value="def">
|
||||
<option value="def">Standard-Vorlage verwenden</option>
|
||||
<option v-for="template in templates" :key="template.id" :value="template.id">
|
||||
{{ template.template }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
<div class="w-full grow min-h-[50%] flex flex-col gap-2">
|
||||
<Spinner v-if="status == 'loading'" />
|
||||
<div class="grow">
|
||||
<iframe v-show="status == 'success'" ref="viewer" class="w-full h-full" />
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<p class="whitespace-nowrap">Höhe [mm]:</p>
|
||||
<input id="footerHeight" type="number" :min="15" class="w-24!" placeholder="15" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<button type="submit" primary class="w-fit!">Liste drucken</button>
|
||||
<button type="reset" primary-outline class="w-fit!">zurücksetzen</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="w-full grow min-h-[50%] flex flex-col gap-2">
|
||||
<Spinner v-if="status == 'loading'" />
|
||||
<div class="grow">
|
||||
<iframe v-show="status == 'success'" ref="viewer" class="w-full h-full" />
|
||||
</div>
|
||||
|
||||
<div v-show="status == 'success'" class="flex flex-row gap-2 justify-end">
|
||||
<a ref="download" button primary class="w-fit!">download</a>
|
||||
</div>
|
||||
<div v-show="status == 'success'" class="flex flex-row gap-2 justify-end">
|
||||
<a ref="download" button primary class="w-fit!">download</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Mitglieder</h1>
|
||||
</div>
|
||||
</template>
|
||||
<MainTemplate title="Mitglieder">
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col w-full h-full gap-2 justify-center px-7">
|
||||
<Pagination
|
||||
|
|
|
@ -75,6 +75,10 @@
|
|||
<label for="internalId">Interne ID (optional)</label>
|
||||
<input type="text" id="internalId" v-model="member.internalId" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="note">Notiz (optional)</label>
|
||||
<textarea type="text" id="note" v-model="member.note" />
|
||||
</div>
|
||||
<div class="flex flex-row justify-end gap-2">
|
||||
<button primary-outline type="reset" class="w-fit!" :disabled="canSaveOrReset" @click="resetForm">
|
||||
verwerfen
|
||||
|
@ -93,7 +97,6 @@
|
|||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import MainTemplate from "@/templates/Main.vue";
|
||||
import { useMemberStore } from "@/stores/admin/club/member/member";
|
||||
import type { MemberViewModel, UpdateMemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
|
@ -103,7 +106,7 @@ import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } f
|
|||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isequal";
|
||||
import { useSalutationStore } from "../../../../stores/admin/configuration/salutation";
|
||||
import { useSalutationStore } from "@/stores/admin/configuration/salutation";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -163,6 +166,7 @@ export default defineComponent({
|
|||
nameaffix: formData.nameaffix.value,
|
||||
birthdate: formData.birthdate.value,
|
||||
internalId: formData.internalId.value,
|
||||
note: formData.note.value,
|
||||
};
|
||||
this.status = "loading";
|
||||
this.updateActiveMember(updateMember)
|
||||
|
|
51
src/views/admin/club/members/MemberEducations.vue
Normal file
51
src/views/admin/club/members/MemberEducations.vue
Normal file
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||
<div v-if="memberEducations != null" class="flex flex-col gap-2 w-full">
|
||||
<MemberEducationListItem v-for="education in memberEducations" :key="education.id" :education="education" />
|
||||
</div>
|
||||
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||
<p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">↺ laden fehlgeschlagen</p>
|
||||
</div>
|
||||
<div class="flex flex-row gap-4">
|
||||
<button v-if="can('create', 'club', 'member')" primary class="w-fit!" @click="openCreateModal">
|
||||
Aus-/Fortbildung hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import { useMemberEducationStore } from "@/stores/admin/club/member/memberEducation";
|
||||
import MemberEducationListItem from "@/components/admin/club/member/MemberEducationListItem.vue";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
memberId: String,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useMemberEducationStore, ["memberEducations", "loading"]),
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchItem();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useMemberEducationStore, ["fetchMemberEducationsForMember"]),
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
fetchItem() {
|
||||
this.fetchMemberEducationsForMember();
|
||||
},
|
||||
openCreateModal() {
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberEducationCreateModal.vue")))
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -25,9 +25,19 @@
|
|||
<label for="birthdate">Geburtsdatum</label>
|
||||
<input type="date" id="birthdate" :value="activeMemberObj.birthdate" readonly />
|
||||
</div>
|
||||
<div v-if="membershipStatistics.length != 0">
|
||||
<div>
|
||||
<label for="note">Notiz</label>
|
||||
<textarea type="text" id="note" v-model="activeMemberObj.note" readonly />
|
||||
</div>
|
||||
<div v-if="membershipStatistics.length != 0 || totalMembershipStatistics != undefined">
|
||||
<p>Statistiken zur Mitgliedschaft</p>
|
||||
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
|
||||
<div class="flex flex-col h-fit w-full rounded-md overflow-hidden divide-y divide-white">
|
||||
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
||||
<p>
|
||||
gesamt {{ totalMembershipStatistics?.durationInDays }} Tage
|
||||
<span class="whitespace-nowrap"> ~> {{ totalMembershipStatistics?.exactDuration }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-for="stat in membershipStatistics"
|
||||
class="bg-primary p-2 text-white flex flex-row justify-between items-center"
|
||||
|
@ -149,16 +159,20 @@ export default defineComponent({
|
|||
},
|
||||
computed: {
|
||||
...mapState(useMemberStore, ["activeMemberObj", "activeMemberStatistics", "loadingActive"]),
|
||||
...mapState(useMembershipStore, ["membershipStatistics"]),
|
||||
...mapState(useMembershipStore, ["membershipStatistics", "totalMembershipStatistics"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchMemberByActiveId();
|
||||
this.fetchMemberStatisticsByActiveId();
|
||||
this.fetchMembershipStatisticsForMember();
|
||||
this.fetchMembershipTotalStatisticsForMember();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useMemberStore, ["fetchMemberByActiveId", "fetchMemberStatisticsByActiveId"]),
|
||||
...mapActions(useMembershipStore, ["fetchMembershipStatisticsForMember"]),
|
||||
...mapActions(useMembershipStore, [
|
||||
"fetchMembershipStatisticsForMember",
|
||||
"fetchMembershipTotalStatisticsForMember",
|
||||
]),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -4,12 +4,11 @@
|
|||
<RouterLink to="../" class="text-primary">zurück zur Liste</RouterLink>
|
||||
</template>
|
||||
<template #topBar>
|
||||
<div class="flex flex-row gap-2 items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8 min-h-fit grow">
|
||||
{{ activeMemberObj?.lastname }}, {{ activeMemberObj?.firstname }}
|
||||
{{ activeMemberObj?.nameaffix ? `- ${activeMemberObj?.nameaffix}` : "" }}
|
||||
</h1>
|
||||
|
||||
<h1 class="font-bold text-xl h-8 min-h-fit">
|
||||
{{ activeMemberObj?.lastname }}, {{ activeMemberObj?.firstname }}
|
||||
{{ activeMemberObj?.nameaffix ? `- ${activeMemberObj?.nameaffix}` : "" }}
|
||||
</h1>
|
||||
<div class="flex flex-row gap-2">
|
||||
<div title="Mitgliederliste drucken" @click="openPrintModal">
|
||||
<DocumentTextIcon class="w-5 h-5 cursor-pointer" />
|
||||
</div>
|
||||
|
@ -22,7 +21,7 @@
|
|||
<template #diffMain>
|
||||
<div class="flex flex-col gap-2 grow px-7 overflow-hidden">
|
||||
<div class="flex flex-col grow gap-2 overflow-hidden">
|
||||
<div class="w-full flex flex-row max-lg:flex-wrap justify-center">
|
||||
<div class="w-full flex flex-row max-lg:flex-wrap justify-center items-stretch">
|
||||
<RouterLink
|
||||
v-for="tab in tabs"
|
||||
:key="tab.route"
|
||||
|
@ -32,7 +31,7 @@
|
|||
>
|
||||
<p
|
||||
:class="[
|
||||
'w-full rounded-lg py-2.5 text-sm text-center font-medium leading-5 focus:ring-0 outline-hidden',
|
||||
'flex w-full h-full items-center justify-center rounded-lg py-2.5 text-sm text-center font-medium leading-5 focus:ring-0 outline-hidden',
|
||||
isActive ? 'bg-red-200 shadow-sm border-b-2 border-primary rounded-b-none' : ' hover:bg-red-200',
|
||||
]"
|
||||
>
|
||||
|
@ -69,8 +68,9 @@ export default defineComponent({
|
|||
{ route: "admin-club-member-overview", title: "Übersicht" },
|
||||
{ route: "admin-club-member-membership", title: "Mitgliedschaft" },
|
||||
{ route: "admin-club-member-communication", title: "Kommunikation" },
|
||||
{ route: "admin-club-member-awards", title: "Auszeichnungen" },
|
||||
{ route: "admin-club-member-qualifications", title: "Qualifikationen" },
|
||||
{ route: "admin-club-member-awards", title: "Auszeichnungen / Ehrungen" },
|
||||
{ route: "admin-club-member-educations", title: "Aus- / Fortbildungen" },
|
||||
{ route: "admin-club-member-qualifications", title: "Qualifikationen / Funktionen" },
|
||||
{ route: "admin-club-member-positions", title: "Vereinsämter" },
|
||||
],
|
||||
};
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Übersicht</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col gap-2 justify-center items-center h-full"></div>
|
||||
</template>
|
||||
</MainTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapState } from "pinia";
|
||||
import MainTemplate from "@/templates/Main.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
computed: {},
|
||||
});
|
||||
</script>
|
|
@ -1,10 +1,5 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Newsletter</h1>
|
||||
</div>
|
||||
</template>
|
||||
<MainTemplate title="Newsletter">
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col w-full h-full gap-2 justify-center px-7">
|
||||
<Pagination
|
||||
|
@ -38,7 +33,7 @@ import Pagination from "@/components/Pagination.vue";
|
|||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter";
|
||||
import type { NewsletterViewModel } from "@/viewmodels/admin/club/newsletter/newsletter.models";
|
||||
import NewsletterListItem from "../../../../components/admin/club/newsletter/NewsletterListItem.vue";
|
||||
import NewsletterListItem from "@/components/admin/club/newsletter/NewsletterListItem.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -37,11 +37,15 @@
|
|||
})
|
||||
}}
|
||||
</p>
|
||||
<TrashIcon
|
||||
<DoubleConfirmClick
|
||||
v-if="can('create', 'club', 'newsletter')"
|
||||
class="w-5 h-5 p-1 box-content cursor-pointer text-white"
|
||||
@click.prevent="removeSelected(item.calendarId)"
|
||||
/>
|
||||
light
|
||||
v-slot="{ isSensitive }"
|
||||
@click:submit="removeSelected(item.calendarId)"
|
||||
>
|
||||
<TrashIcon v-if="!isSensitive" class="h-5 w-5" />
|
||||
<TrashIconSolid v-else class="h-5 w-5" />
|
||||
</DoubleConfirmClick>
|
||||
</summary>
|
||||
<div class="flex flex-col gap-2 px-1">
|
||||
<input
|
||||
|
@ -111,8 +115,10 @@ import { useAbilityStore } from "@/stores/ability";
|
|||
import { useCalendarStore } from "@/stores/admin/club/calendar";
|
||||
import type { CalendarViewModel } from "@/viewmodels/admin/club/calendar.models";
|
||||
import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||
import { TrashIcon as TrashIconSolid } from "@heroicons/vue/24/solid";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import type { NewsletterDatesViewModel } from "@/viewmodels/admin/club/newsletter/newsletterDates.models";
|
||||
import DoubleConfirmClick from "@/components/DoubleConfirmClick.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -81,7 +81,7 @@ import FailureXMark from "@/components/FailureXMark.vue";
|
|||
import { ArrowDownTrayIcon, ViewfinderCircleIcon } from "@heroicons/vue/24/outline";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { useNewsletterPrintoutStore } from "../../../../stores/admin/club/newsletter/newsletterPrintout";
|
||||
import { useNewsletterPrintoutStore } from "@/stores/admin/club/newsletter/newsletterPrintout";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -42,11 +42,15 @@
|
|||
<p>Newsletter senden an Typ: {{ member.sendNewsletter?.type.type ?? "---" }}</p>
|
||||
</div>
|
||||
|
||||
<TrashIcon
|
||||
<DoubleConfirmClick
|
||||
v-if="can('create', 'club', 'newsletter') && showMemberSelect"
|
||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||
@click="removeSelected(member.id)"
|
||||
/>
|
||||
light
|
||||
v-slot="{ isSensitive }"
|
||||
@click:submit="removeSelected(member.id)"
|
||||
>
|
||||
<TrashIcon v-if="!isSensitive" class="h-5 w-5" />
|
||||
<TrashIconSolid v-else class="h-5 w-5" />
|
||||
</DoubleConfirmClick>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -61,17 +65,8 @@
|
|||
import { defineComponent } from "vue";
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxLabel,
|
||||
ComboboxInput,
|
||||
ComboboxButton,
|
||||
ComboboxOptions,
|
||||
ComboboxOption,
|
||||
TransitionRoot,
|
||||
} from "@headlessui/vue";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import { ArchiveBoxIcon, ExclamationTriangleIcon, TrashIcon, UserPlusIcon } from "@heroicons/vue/24/outline";
|
||||
import { TrashIcon as TrashIconSolid } from "@heroicons/vue/24/solid";
|
||||
import { useMemberStore } from "@/stores/admin/club/member/member";
|
||||
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||
import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter";
|
||||
|
@ -79,9 +74,9 @@ import { useNewsletterRecipientsStore } from "@/stores/admin/club/newsletter/new
|
|||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { useQueryStoreStore } from "@/stores/admin/configuration/queryStore";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import MemberSearchSelect from "@/components/admin/MemberSearchSelect.vue";
|
||||
import type { FieldType } from "@/types/dynamicQueries";
|
||||
import DoubleConfirmClick from "@/components/DoubleConfirmClick.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<MainTemplate :title="origin?.title">
|
||||
<template #headerInsert>
|
||||
<RouterLink to="../" class="text-primary w-fit">zurück zur Liste</RouterLink>
|
||||
</template>
|
||||
<template #topBar>
|
||||
<div class="flex flex-row gap-2 items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8 min-h-fit grow">{{ origin?.title }}</h1>
|
||||
<NewsletterSyncing
|
||||
:executeSyncAll="executeSyncAll"
|
||||
@syncState="
|
||||
(state) => {
|
||||
syncState = state;
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<NewsletterSyncing
|
||||
:executeSyncAll="executeSyncAll"
|
||||
@syncState="
|
||||
(state) => {
|
||||
syncState = state;
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col gap-2 grow px-7 overflow-hidden">
|
||||
|
@ -53,8 +50,8 @@ import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter";
|
|||
import { useModalStore } from "@/stores/modal";
|
||||
import NewsletterSyncing from "@/components/admin/club/newsletter/NewsletterSyncing.vue";
|
||||
import { PrinterIcon } from "@heroicons/vue/24/outline";
|
||||
import { useNewsletterDatesStore } from "../../../../stores/admin/club/newsletter/newsletterDates";
|
||||
import { useNewsletterRecipientsStore } from "../../../../stores/admin/club/newsletter/newsletterRecipients";
|
||||
import { useNewsletterDatesStore } from "@/stores/admin/club/newsletter/newsletterDates";
|
||||
import { useNewsletterRecipientsStore } from "@/stores/admin/club/newsletter/newsletterRecipients";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Protokolle</h1>
|
||||
</div>
|
||||
</template>
|
||||
<MainTemplate title="Protokolle">
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col w-full h-full gap-2 justify-center px-7">
|
||||
<Pagination
|
||||
|
|
|
@ -31,6 +31,16 @@
|
|||
:disabled="!can('create', 'club', 'protocol')"
|
||||
/>
|
||||
|
||||
<DoubleConfirmClick
|
||||
v-if="can('create', 'club', 'protocol')"
|
||||
light
|
||||
v-slot="{ isSensitive }"
|
||||
@click:submit="removeFromArray(item.id)"
|
||||
>
|
||||
<TrashIcon v-if="!isSensitive" class="h-5 w-5" />
|
||||
<TrashIconSolid v-else class="h-5 w-5" />
|
||||
</DoubleConfirmClick>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<ChevronUpIcon
|
||||
v-if="index != 0"
|
||||
|
@ -73,7 +83,9 @@ import "@vueup/vue-quill/dist/vue-quill.snow.css";
|
|||
import { toolbarOptions } from "@/helpers/quillConfig";
|
||||
import { useProtocolAgendaStore } from "@/stores/admin/club/protocol/protocolAgenda";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/outline";
|
||||
import { ChevronDownIcon, ChevronUpIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||
import { TrashIcon as TrashIconSolid } from "@heroicons/vue/24/solid";
|
||||
import DoubleConfirmClick from "@/components/DoubleConfirmClick.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -109,6 +121,9 @@ export default defineComponent({
|
|||
});
|
||||
}
|
||||
},
|
||||
removeFromArray(thisId: number) {
|
||||
this.agenda = this.agenda.filter((item) => item.id !== thisId);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -31,6 +31,16 @@
|
|||
:disabled="!can('create', 'club', 'protocol')"
|
||||
/>
|
||||
|
||||
<DoubleConfirmClick
|
||||
v-if="can('create', 'club', 'protocol')"
|
||||
light
|
||||
v-slot="{ isSensitive }"
|
||||
@click:submit="removeFromArray(item.id)"
|
||||
>
|
||||
<TrashIcon v-if="!isSensitive" class="h-5 w-5" />
|
||||
<TrashIconSolid v-else class="h-5 w-5" />
|
||||
</DoubleConfirmClick>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<ChevronUpIcon
|
||||
v-if="index != 0"
|
||||
|
@ -73,7 +83,9 @@ import "@vueup/vue-quill/dist/vue-quill.snow.css";
|
|||
import { toolbarOptions } from "@/helpers/quillConfig";
|
||||
import { useProtocolDecisionStore } from "@/stores/admin/club/protocol/protocolDecision";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/outline";
|
||||
import { ChevronDownIcon, ChevronUpIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||
import { TrashIcon as TrashIconSolid } from "@heroicons/vue/24/solid";
|
||||
import DoubleConfirmClick from "@/components/DoubleConfirmClick.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -109,6 +121,9 @@ export default defineComponent({
|
|||
});
|
||||
}
|
||||
},
|
||||
removeFromArray(thisId: number) {
|
||||
this.decision = this.decision.filter((item) => item.id !== thisId);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -42,11 +42,15 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<TrashIcon
|
||||
<DoubleConfirmClick
|
||||
v-if="can('create', 'club', 'protocol')"
|
||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||
@click="removeSelected(member.memberId)"
|
||||
/>
|
||||
light
|
||||
v-slot="{ isSensitive }"
|
||||
@click:submit="removeSelected(member.memberId)"
|
||||
>
|
||||
<TrashIcon v-if="!isSensitive" class="h-5 w-5" />
|
||||
<TrashIconSolid v-else class="h-5 w-5" />
|
||||
</DoubleConfirmClick>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -57,10 +61,12 @@ import { defineComponent } from "vue";
|
|||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||
import { TrashIcon as TrashIconSolid } from "@heroicons/vue/24/solid";
|
||||
import { useProtocolPresenceStore } from "@/stores/admin/club/protocol/protocolPresence";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import MemberSearchSelect from "@/components/admin/MemberSearchSelect.vue";
|
||||
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||
import DoubleConfirmClick from "@/components/DoubleConfirmClick.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<MainTemplate :title="`${origin?.title}, ${origin?.date}`">
|
||||
<template #headerInsert>
|
||||
<RouterLink to="../" class="text-primary w-fit">zurück zur Liste</RouterLink>
|
||||
</template>
|
||||
<template #topBar>
|
||||
<div class="flex flex-row gap-2 items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8 min-h-fit grow">{{ origin?.title }}, {{ origin?.date }}</h1>
|
||||
<ProtocolSyncing
|
||||
:executeSyncAll="executeSyncAll"
|
||||
@syncState="
|
||||
(state) => {
|
||||
syncState = state;
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<ProtocolSyncing
|
||||
:executeSyncAll="executeSyncAll"
|
||||
@syncState="
|
||||
(state) => {
|
||||
syncState = state;
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col gap-2 grow px-7 overflow-hidden">
|
||||
|
@ -92,10 +89,10 @@ export default defineComponent({
|
|||
},
|
||||
mounted() {
|
||||
this.fetchProtocolByActiveId();
|
||||
this.fetchProtocolAgenda()
|
||||
this.fetchProtocolDecision()
|
||||
this.fetchProtocolPresence()
|
||||
this.fetchProtocolVoting()
|
||||
this.fetchProtocolAgenda();
|
||||
this.fetchProtocolDecision();
|
||||
this.fetchProtocolPresence();
|
||||
this.fetchProtocolVoting();
|
||||
},
|
||||
// this.syncState is undefined, so it will never work
|
||||
// beforeRouteLeave(to, from, next) {
|
||||
|
@ -118,8 +115,8 @@ export default defineComponent({
|
|||
...mapActions(useProtocolStore, ["fetchProtocolByActiveId"]),
|
||||
...mapActions(useProtocolAgendaStore, ["fetchProtocolAgenda"]),
|
||||
...mapActions(useProtocolDecisionStore, ["fetchProtocolDecision"]),
|
||||
...mapActions(useProtocolPresenceStore,["fetchProtocolPresence"]),
|
||||
...mapActions(useProtocolVotingStore,["fetchProtocolVoting"]),
|
||||
...mapActions(useProtocolPresenceStore, ["fetchProtocolPresence"]),
|
||||
...mapActions(useProtocolVotingStore, ["fetchProtocolVoting"]),
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
openInfoModal() {
|
||||
this.openModal(
|
||||
|
|
|
@ -31,6 +31,16 @@
|
|||
:disabled="!can('create', 'club', 'protocol')"
|
||||
/>
|
||||
|
||||
<DoubleConfirmClick
|
||||
v-if="can('create', 'club', 'protocol')"
|
||||
light
|
||||
v-slot="{ isSensitive }"
|
||||
@click:submit="removeFromArray(item.id)"
|
||||
>
|
||||
<TrashIcon v-if="!isSensitive" class="h-5 w-5" />
|
||||
<TrashIconSolid v-else class="h-5 w-5" />
|
||||
</DoubleConfirmClick>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<ChevronUpIcon
|
||||
v-if="index != 0"
|
||||
|
@ -83,14 +93,16 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import { QuillEditor } from "@vueup/vue-quill";
|
||||
import "@vueup/vue-quill/dist/vue-quill.snow.css";
|
||||
import { toolbarOptions } from "@/helpers/quillConfig";
|
||||
import { useProtocolVotingStore } from "@/stores/admin/club/protocol/protocolVoting";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/outline";
|
||||
import { ChevronDownIcon, ChevronUpIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||
import { TrashIcon as TrashIconSolid } from "@heroicons/vue/24/solid";
|
||||
import DoubleConfirmClick from "@/components/DoubleConfirmClick.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -99,7 +111,7 @@ export default defineComponent({
|
|||
protocolId: String,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useProtocolVotingStore, ["voting", "loading"]),
|
||||
...mapWritableState(useProtocolVotingStore, ["voting", "loading"]),
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
sortedVoting() {
|
||||
return this.voting.slice().sort((a, b) => a.sort - b.sort);
|
||||
|
@ -126,6 +138,9 @@ export default defineComponent({
|
|||
});
|
||||
}
|
||||
},
|
||||
removeFromArray(thisId: number) {
|
||||
this.voting = this.voting.filter((item) => item.id !== thisId);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,57 +1,50 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Query Builder</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col w-full h-full gap-2 px-7 overflow-y-auto">
|
||||
<BuilderHost
|
||||
v-model="query"
|
||||
allow-predefined-select
|
||||
@query:run="sendQuery"
|
||||
@query:save="triggerSave"
|
||||
@results:clear="clearResults"
|
||||
@results:export="exportData"
|
||||
/>
|
||||
<p>Ergebnisse:</p>
|
||||
<div
|
||||
v-if="loadingData == 'failed'"
|
||||
class="flex flex-col p-2 border border-red-600 bg-red-200 rounded-md select-none"
|
||||
>
|
||||
<p v-if="typeof queryError == 'string'">
|
||||
{{ queryError }}
|
||||
<MainTemplate title="Query Builder">
|
||||
<template #main>
|
||||
<BuilderHost
|
||||
v-model="query"
|
||||
allow-predefined-select
|
||||
@query:run="sendQuery"
|
||||
@query:save="triggerSave"
|
||||
@results:clear="clearResults"
|
||||
@results:export="exportData"
|
||||
/>
|
||||
<p>Ergebnisse:</p>
|
||||
<div
|
||||
v-if="loadingData == 'failed'"
|
||||
class="flex flex-col p-2 border border-red-600 bg-red-200 rounded-md select-none"
|
||||
>
|
||||
<p v-if="typeof queryError == 'string'">
|
||||
{{ queryError }}
|
||||
</p>
|
||||
<div v-else>
|
||||
<p>
|
||||
CODE: <br />
|
||||
{{ queryError.code }}
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
MSG: <br />
|
||||
{{ queryError.msg }}
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
RESULTING SQL: <br />
|
||||
{{ queryError.sql }}
|
||||
</p>
|
||||
<div v-else>
|
||||
<p>
|
||||
CODE: <br />
|
||||
{{ queryError.code }}
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
MSG: <br />
|
||||
{{ queryError.msg }}
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
RESULTING SQL: <br />
|
||||
{{ queryError.sql }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Pagination
|
||||
v-else
|
||||
:items="data"
|
||||
:totalCount="totalLength"
|
||||
:indicateLoading="loadingData == 'loading'"
|
||||
@load-data="(offset, count) => sendQuery(offset, count)"
|
||||
>
|
||||
<template #pageRow="{ row }: { row: { id: FieldType; [key: string]: FieldType } }">
|
||||
<p>{{ row }}</p>
|
||||
</template>
|
||||
</Pagination>
|
||||
</div>
|
||||
<Pagination
|
||||
v-else
|
||||
:items="data"
|
||||
:totalCount="totalLength"
|
||||
:indicateLoading="loadingData == 'loading'"
|
||||
@load-data="(offset, count) => sendQuery(offset, count)"
|
||||
>
|
||||
<template #pageRow="{ row }: { row: { id: FieldType; [key: string]: FieldType } }">
|
||||
<p>{{ row }}</p>
|
||||
</template>
|
||||
</Pagination>
|
||||
</template>
|
||||
</MainTemplate>
|
||||
</template>
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Auszeichnungen</h1>
|
||||
</div>
|
||||
</template>
|
||||
<MainTemplate title="Auszeichnungen">
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col gap-4 h-full pl-7">
|
||||
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue