Compare commits
8 commits
main
...
feature/#6
Author | SHA1 | Date | |
---|---|---|---|
9bd663f266 | |||
beaf6a5926 | |||
8880af2880 | |||
5d9007f517 | |||
20a2a3ccd0 | |||
916e61897a | |||
b19e8df561 | |||
fb78360946 |
48 changed files with 1000 additions and 187 deletions
|
@ -1,5 +1 @@
|
||||||
VITE_SERVER_ADDRESS = backend_url #ohne pfad
|
VITE_SERVER_ADDRESS = backend_url #ohne pfad
|
||||||
VITE_APP_NAME_OVERWRITE = Mitgliederverwaltung # overwrites FF Admin
|
|
||||||
VITE_IMPRINT_LINK = https://mywebsite-imprint-url
|
|
||||||
VITE_PRIVACY_LINK = https://mywebsite-privacy-url
|
|
||||||
VITE_CUSTOM_LOGIN_MESSAGE = betrieben von xy
|
|
|
@ -1,5 +1 @@
|
||||||
VITE_SERVER_ADDRESS = __SERVERADDRESS__
|
VITE_SERVER_ADDRESS = __SERVERADDRESS__
|
||||||
VITE_APP_NAME_OVERWRITE = __APPNAMEOVERWRITE__
|
|
||||||
VITE_IMPRINT_LINK = __IMPRINTLINK__
|
|
||||||
VITE_PRIVACY_LINK = __PRIVACYLINK__
|
|
||||||
VITE_CUSTOM_LOGIN_MESSAGE = __CUSTOMLOGINMESSAGE__
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
keys="SERVERADDRESS APPNAMEOVERWRITE IMPRINTLINK PRIVACYLINK CUSTOMLOGINMESSAGE"
|
keys="SERVERADDRESS"
|
||||||
files="/usr/share/nginx/html/assets/config-*.js /usr/share/nginx/html/manifest.webmanifest"
|
files="/usr/share/nginx/html/assets/config-*.js"
|
||||||
|
|
||||||
# Replace env vars in files served by NGINX
|
# Replace env vars in files served by NGINX
|
||||||
for file in $files
|
for file in $files
|
||||||
|
@ -12,11 +12,6 @@ do
|
||||||
# Get environment variable
|
# Get environment variable
|
||||||
value=$(eval echo "\$$key")
|
value=$(eval echo "\$$key")
|
||||||
|
|
||||||
# Set default value for APPNAMEOVERWRITE if empty
|
|
||||||
if [ "$key" = "APPNAMEOVERWRITE" ] && [ -z "$value" ]; then
|
|
||||||
value="FF Admin"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "replace $key by $value"
|
echo "replace $key by $value"
|
||||||
|
|
||||||
# replace __[variable_name]__ value with environment variable
|
# replace __[variable_name]__ value with environment variable
|
||||||
|
|
|
@ -2,11 +2,14 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<!-- icon and manifest are provided by App.vue -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
<script>
|
||||||
|
// screen.orientation.lock("portrait-primary").catch(() => {});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
7
package-lock.json
generated
7
package-lock.json
generated
|
@ -35,6 +35,7 @@
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pdf-dist": "^1.0.0",
|
"pdf-dist": "^1.0.0",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
|
"pwacompat": "^2.0.17",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
"socket.io-client": "^4.5.0",
|
"socket.io-client": "^4.5.0",
|
||||||
|
@ -8752,6 +8753,12 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pwacompat": {
|
||||||
|
"version": "2.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/pwacompat/-/pwacompat-2.0.17.tgz",
|
||||||
|
"integrity": "sha512-6Du7IZdIy7cHiv7AhtDy4X2QRM8IAD5DII69mt5qWibC2d15ZU8DmBG1WdZKekG11cChSu4zkSUGPF9sweOl6w==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/qrcode": {
|
"node_modules/qrcode": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pdf-dist": "^1.0.0",
|
"pdf-dist": "^1.0.0",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
|
"pwacompat": "^2.0.17",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
"socket.io-client": "^4.5.0",
|
"socket.io-client": "^4.5.0",
|
||||||
|
|
BIN
public/Logo.png
BIN
public/Logo.png
Binary file not shown.
Before Width: | Height: | Size: 34 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 29 KiB |
16
src/App.vue
16
src/App.vue
|
@ -8,6 +8,12 @@
|
||||||
</div>
|
</div>
|
||||||
<Footer @contextmenu.prevent />
|
<Footer @contextmenu.prevent />
|
||||||
<Notification />
|
<Notification />
|
||||||
|
|
||||||
|
<Teleport to="head">
|
||||||
|
<title>{{ clubName }}</title>
|
||||||
|
<link rel="icon" type="image/ico" :href="config.server_address + '/api/public/favicon.ico'" />
|
||||||
|
<link rel="manifest" :href="config.server_address + '/api/public/manifest.webmanifest'" />
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -15,20 +21,25 @@ import { defineComponent } from "vue";
|
||||||
import { RouterView } from "vue-router";
|
import { RouterView } from "vue-router";
|
||||||
import Header from "./components/Header.vue";
|
import Header from "./components/Header.vue";
|
||||||
import Footer from "./components/Footer.vue";
|
import Footer from "./components/Footer.vue";
|
||||||
import { mapState } from "pinia";
|
import { mapActions, mapState } from "pinia";
|
||||||
import { useAuthStore } from "./stores/auth";
|
import { useAuthStore } from "./stores/auth";
|
||||||
import { isAuthenticatedPromise } from "./router/authGuard";
|
import { isAuthenticatedPromise } from "./router/authGuard";
|
||||||
import ContextMenu from "./components/ContextMenu.vue";
|
import ContextMenu from "./components/ContextMenu.vue";
|
||||||
import Modal from "./components/Modal.vue";
|
import Modal from "./components/Modal.vue";
|
||||||
import Notification from "./components/Notification.vue";
|
import Notification from "./components/Notification.vue";
|
||||||
|
import { config } from "./config";
|
||||||
|
import { useConfigurationStore } from "@/stores/configuration";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useAuthStore, ["authCheck"]),
|
...mapState(useAuthStore, ["authCheck"]),
|
||||||
|
...mapState(useConfigurationStore, ["clubName"]),
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.configure();
|
||||||
|
|
||||||
if (!this.authCheck && localStorage.getItem("access_token")) {
|
if (!this.authCheck && localStorage.getItem("access_token")) {
|
||||||
isAuthenticatedPromise().catch(() => {
|
isAuthenticatedPromise().catch(() => {
|
||||||
localStorage.removeItem("access_token");
|
localStorage.removeItem("access_token");
|
||||||
|
@ -36,5 +47,8 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useConfigurationStore, ["configure"]),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
7
src/components/AppLogo.vue
Normal file
7
src/components/AppLogo.vue
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<img :src="url + '/api/public/applogo.png'" alt="LOGO" class="h-full w-auto" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { url } from "@/serverCom";
|
||||||
|
</script>
|
53
src/components/CheckProgressBar.vue
Normal file
53
src/components/CheckProgressBar.vue
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full flex flex-row items-center">
|
||||||
|
<div class="contents" v-for="(i, index) in total" :key="index">
|
||||||
|
<div
|
||||||
|
v-if="index <= successfull && index != step"
|
||||||
|
class="relative flex items-center justify-center h-8 w-8 border-4 border-success rounded-full"
|
||||||
|
>
|
||||||
|
<SuccessCheckmark class="h-8! asolute top-0 m-0!" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="index <= step"
|
||||||
|
class="flex items-center justify-center h-8 w-8 border-4 border-success rounded-full"
|
||||||
|
>
|
||||||
|
<div class="h-2 w-2 border-4 border-success bg-success rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="h-8 w-8 border-4 border-gray-400 rounded-full"></div>
|
||||||
|
<div v-if="i != total" class="grow border-2" :class="index < step ? ' border-success' : 'border-gray-400'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import SuccessCheckmark from "./SuccessCheckmark.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
step: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
validator(value: number) {
|
||||||
|
return value >= 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
successfull: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
validator(value: number) {
|
||||||
|
return value >= 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
validator(value: number) {
|
||||||
|
return value >= 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,10 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col text-gray-400 text-sm mt-4 items-center">
|
<div class="flex flex-col text-gray-400 text-sm mt-4 items-center">
|
||||||
<div class="flex flex-row gap-2 justify-center">
|
<p v-if="appCustom_login_message">{{ appCustom_login_message }}</p>
|
||||||
<a v-if="config.imprint_link" :href="config.imprint_link" target="_blank">Datenschutz</a>
|
<div class="flex flex-row gap-2 justify-center mb-3">
|
||||||
<a v-if="config.privacy_link" :href="config.privacy_link" target="_blank">Impressum</a>
|
<a v-if="clubWebsite" :href="clubWebsite" target="_blank">Webseite</a>
|
||||||
|
<a v-if="clubImprint" :href="clubImprint" target="_blank">Datenschutz</a>
|
||||||
|
<a v-if="clubPrivacy" :href="clubPrivacy" target="_blank">Impressum</a>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="config.custom_login_message">{{ config.custom_login_message }}</p>
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://ff-admin.de/admin" target="_blank">FF Admin</a>
|
<a href="https://ff-admin.de/admin" target="_blank">FF Admin</a>
|
||||||
entwickelt von
|
entwickelt von
|
||||||
|
@ -14,5 +15,21 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { config } from "@/config";
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import { useConfigurationStore } from "@/stores/configuration";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
computed: {
|
||||||
|
...mapState(useConfigurationStore, [
|
||||||
|
"appCustom_login_message",
|
||||||
|
"appShow_link_to_calendar",
|
||||||
|
"clubImprint",
|
||||||
|
"clubPrivacy",
|
||||||
|
"clubWebsite",
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<header class="flex flex-row h-16 min-h-16 justify-between p-3 md:px-5 bg-white shadow-xs">
|
<header class="flex flex-row h-16 min-h-16 justify-between p-3 md:px-5 bg-white shadow-xs">
|
||||||
<RouterLink to="/" class="flex flex-row gap-2 align-bottom w-fit h-full">
|
<RouterLink to="/" class="flex flex-row gap-2 align-bottom w-fit h-full">
|
||||||
<img src="/Logo.png" alt="LOGO" class="h-full w-auto" />
|
<AppLogo />
|
||||||
<h1 v-if="false" class="font-bold text-3xl w-fit whitespace-nowrap">
|
<h1 v-if="false" class="font-bold text-3xl w-fit whitespace-nowrap">
|
||||||
{{ config.app_name_overwrite || "FF Admin" }}
|
{{ clubName }}
|
||||||
</h1>
|
</h1>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<div class="flex flex-row gap-2 items-center">
|
<div class="flex flex-row gap-2 items-center">
|
||||||
|
@ -37,15 +37,17 @@ import { useAuthStore } from "@/stores/auth";
|
||||||
import { useNavigationStore } from "@/stores/admin/navigation";
|
import { useNavigationStore } from "@/stores/admin/navigation";
|
||||||
import TopLevelLink from "./admin/TopLevelLink.vue";
|
import TopLevelLink from "./admin/TopLevelLink.vue";
|
||||||
import UserMenu from "./UserMenu.vue";
|
import UserMenu from "./UserMenu.vue";
|
||||||
import { config } from "@/config";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
import AppLogo from "./AppLogo.vue";
|
||||||
|
import { useConfigurationStore } from "../stores/configuration";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useAuthStore, ["authCheck"]),
|
...mapState(useAuthStore, ["authCheck"]),
|
||||||
...mapState(useNavigationStore, ["topLevel"]),
|
...mapState(useNavigationStore, ["topLevel"]),
|
||||||
|
...mapState(useConfigurationStore, ["clubName"]),
|
||||||
routeName() {
|
routeName() {
|
||||||
return typeof this.$route.name == "string" ? this.$route.name : "";
|
return typeof this.$route.name == "string" ? this.$route.name : "";
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
<button button primary @click="close">Mein Account</button>
|
<button button primary @click="close">Mein Account</button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem v-slot="{ close }">
|
<MenuItem v-if="false" v-slot="{ close }">
|
||||||
<RouterLink to="/docs" target="_blank">
|
<RouterLink to="/docs" target="_blank">
|
||||||
<button button primary @click="close">Dokumentation</button>
|
<button button primary @click="close">Dokumentation</button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<Combobox v-model="selected" :disabled="disabled" multiple>
|
<Combobox v-model="selected" :disabled="disabled" multiple>
|
||||||
<ComboboxLabel>{{ title }}</ComboboxLabel>
|
<ComboboxLabel v-if="!showTitleAsPlaceholder">{{ title }}</ComboboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative" :class="{ 'mt-1': !showTitleAsPlaceholder }">
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
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"
|
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"
|
||||||
|
:placeholder="showTitleAsPlaceholder ? title : ''"
|
||||||
@input="query = $event.target.value"
|
@input="query = $event.target.value"
|
||||||
/>
|
/>
|
||||||
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
|
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
@ -101,6 +102,10 @@ export default defineComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
showTitleAsPlaceholder: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ["update:model-value", "add:difference", "remove:difference", "add:member", "add:memberByArray"],
|
emits: ["update:model-value", "add:difference", "remove:difference", "add:member", "add:memberByArray"],
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
@ -3,13 +3,13 @@
|
||||||
<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>Newsletter bei Type "{{ comType.type }}" versenden/exportieren als</p>
|
<p>Newsletter bei Type "{{ comType.type }}" versenden/exportieren als</p>
|
||||||
<div v-if="can('create', 'configuration', 'newsletter_config')" class="flex flex-row justify-end w-16">
|
<div v-if="can('create', 'configuration', 'newsletter_config')" class="flex flex-row justify-end w-16">
|
||||||
<button v-if="status == null" type="submit" class="p-0! h-fit! w-fit!" title="speichern">
|
<button v-if="status == null" type="submit" class="p-0! h-fit! w-fit!" title="Änderung speichern">
|
||||||
<ArchiveBoxArrowDownIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
<ArchiveBoxArrowDownIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
||||||
</button>
|
</button>
|
||||||
<Spinner v-else-if="status == 'loading'" class="my-auto" />
|
<Spinner v-else-if="status == 'loading'" class="my-auto" />
|
||||||
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||||
<FailureXMark v-else-if="status?.status == 'failed'" />
|
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||||
<button type="button" class="p-0! h-fit! w-fit!" title="zurücksetzen" @click="resetForm">
|
<button type="button" class="p-0! h-fit! w-fit!" title="Änderung zurücksetzen" @click="resetForm">
|
||||||
<ArchiveBoxXMarkIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
<ArchiveBoxXMarkIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,7 +36,7 @@ import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
import { NewsletterConfigType } from "@/enums/newsletterConfigType";
|
import { NewsletterConfigEnum } from "@/enums/newsletterConfigEnum";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import type { CommunicationTypeViewModel } from "@/viewmodels/admin/configuration/communicationType.models";
|
import type { CommunicationTypeViewModel } from "@/viewmodels/admin/configuration/communicationType.models";
|
||||||
import { useAbilityStore } from "@/stores/ability";
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
@ -62,7 +62,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.configs = Object.values(NewsletterConfigType);
|
this.configs = Object.values(NewsletterConfigEnum);
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
try {
|
try {
|
||||||
|
|
70
src/components/setup/Account.vue
Normal file
70
src/components/setup/Account.vue
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-2" @submit.prevent="setup">
|
||||||
|
<p class="text-center">Admin Account</p>
|
||||||
|
<div class="-space-y-px">
|
||||||
|
<div>
|
||||||
|
<input id="username" name="username" type="text" required placeholder="Benutzer" class="rounded-b-none!" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input id="mail" name="mail" type="email" required placeholder="Mailadresse" class="rounded-none!" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input id="firstname" name="firstname" type="text" required placeholder="Vorname" class="rounded-none!" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input id="lastname" name="lastname" type="text" required placeholder="Nachname" class="rounded-t-none!" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button type="submit" primary :disabled="setupStatus == 'loading' || setupStatus == 'success'">
|
||||||
|
Admin-Account anlegen
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="setupStatus == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="setupStatus == 'success'" />
|
||||||
|
<FailureXMark v-else-if="setupStatus == 'failed'" />
|
||||||
|
</div>
|
||||||
|
<p v-if="setupMessage" class="text-center">{{ setupMessage }}</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
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";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
setupStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
setupMessage: "" as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSetupStore, ["createAdmin"]),
|
||||||
|
setup(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
this.setupStatus = "loading";
|
||||||
|
this.setupMessage = "";
|
||||||
|
this.createAdmin({
|
||||||
|
username: formData.username.value,
|
||||||
|
mail: formData.mail.value,
|
||||||
|
firstname: formData.firstname.value,
|
||||||
|
lastname: formData.lastname.value,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
// this.setupStatus = "success";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.setupStatus = "failed";
|
||||||
|
this.setupMessage = err.response.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
66
src/components/setup/App.vue
Normal file
66
src/components/setup/App.vue
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-2" @submit.prevent="setup">
|
||||||
|
<p class="text-center">App Konfiguration</p>
|
||||||
|
<div class="-space-y-px">
|
||||||
|
<div>
|
||||||
|
<input id="login_message" name="login_message" type="text" placeholder="Nachricht unter Login (optional)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row items-center gap-2 pt-1">
|
||||||
|
<input type="checkbox" id="show_cal_link" checked />
|
||||||
|
<label for="show_cal_link">Link zum Kalender anzeigen (optional)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-primary cursor-pointer ml-auto" @click="skip('app')">überspringen</p>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button type="submit" primary :disabled="setupStatus == 'loading' || setupStatus == 'success'">
|
||||||
|
Anwendungsdaten speichern
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="setupStatus == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="setupStatus == 'success'" />
|
||||||
|
<FailureXMark v-else-if="setupStatus == 'failed'" />
|
||||||
|
</div>
|
||||||
|
<p v-if="setupMessage" class="text-center">{{ setupMessage }}</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
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";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
setupStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
setupMessage: "" as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSetupStore, ["setApp", "skip"]),
|
||||||
|
setup(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
this.setupStatus = "loading";
|
||||||
|
this.setupMessage = "";
|
||||||
|
this.setApp({
|
||||||
|
login_message: formData.login_message.value,
|
||||||
|
show_cal_link: formData.show_cal_link.checked,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
// this.setupStatus = "success";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.setupStatus = "failed";
|
||||||
|
this.setupMessage = err.response.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
99
src/components/setup/Club.vue
Normal file
99
src/components/setup/Club.vue
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-2" @submit.prevent="setup">
|
||||||
|
<p class="text-center">Feuerwehr-/Vereinsdaten</p>
|
||||||
|
<div class="-space-y-px">
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Feuerwehr-/Vereinsname (optional)"
|
||||||
|
class="rounded-b-none!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="imprint"
|
||||||
|
name="imprint"
|
||||||
|
type="url"
|
||||||
|
placeholder="Link zum Impressum (optional)"
|
||||||
|
class="rounded-none!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="privacy"
|
||||||
|
name="privacy"
|
||||||
|
type="url"
|
||||||
|
placeholder="Link zur Datenschutzerklärung (optional)"
|
||||||
|
class="rounded-none!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="website"
|
||||||
|
name="website"
|
||||||
|
type="url"
|
||||||
|
placeholder="Link zur Webseite (optional)"
|
||||||
|
class="rounded-t-none!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-primary cursor-pointer ml-auto" @click="skip('club')">überspringen</p>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button type="submit" primary :disabled="setupStatus == 'loading' || setupStatus == 'success'">
|
||||||
|
Vereinsdaten speichern
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="setupStatus == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="setupStatus == 'success'" />
|
||||||
|
<FailureXMark v-else-if="setupStatus == 'failed'" />
|
||||||
|
</div>
|
||||||
|
<p v-if="setupMessage" class="text-center">{{ setupMessage }}</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
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";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
setupStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
setupMessage: "" as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSetupStore, ["setClub", "skip"]),
|
||||||
|
setup(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
this.setupStatus = "loading";
|
||||||
|
this.setupMessage = "";
|
||||||
|
this.setClub({
|
||||||
|
name: formData.name.value,
|
||||||
|
imprint: formData.imprint.value,
|
||||||
|
privacy: formData.privacy.value,
|
||||||
|
website: formData.website.value,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
// this.setupStatus = "success";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.setupStatus = "failed";
|
||||||
|
this.setupMessage = err.response.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
3
src/components/setup/Finished.vue
Normal file
3
src/components/setup/Finished.vue
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<p class="text-center">Sie haben einen Verifizierungslink per Mail erhalten.</p>
|
||||||
|
</template>
|
87
src/components/setup/Images.vue
Normal file
87
src/components/setup/Images.vue
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-2" @submit.prevent="setup">
|
||||||
|
<p class="text-center">Feuerwehr-/Vereins-Auftritt</p>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<p>quadratisches Icon für App (optional)</p>
|
||||||
|
<img ref="icon_img" class="hidden w-full h-20 object-contain" />
|
||||||
|
<input class="hidden!" type="file" ref="icon" accept="image/*" @change="previewImage('icon')" />
|
||||||
|
<button type="button" primary-outline @click="($refs.icon as HTMLInputElement).click()">Icon auswählen</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<p>Logo (optional)</p>
|
||||||
|
<img ref="logo_img" class="hidden w-full h-20 object-contain" />
|
||||||
|
<input class="hidden!" type="file" ref="logo" accept="image/*" @change="previewImage('logo')" />
|
||||||
|
<button type="button" primary-outline @click="($refs.logo as HTMLInputElement).click()">Logo auswählen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p class="text-primary cursor-pointer ml-auto" @click="skip('appImages')">überspringen</p>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button type="submit" primary :disabled="setupStatus == 'loading' || setupStatus == 'success'">
|
||||||
|
Bilder speichern
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="setupStatus == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="setupStatus == 'success'" />
|
||||||
|
<FailureXMark v-else-if="setupStatus == 'failed'" />
|
||||||
|
</div>
|
||||||
|
<p v-if="setupMessage" class="text-center">{{ setupMessage }}</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
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";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
setupStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
setupMessage: "" as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSetupStore, ["setClubImages", "skip"]),
|
||||||
|
previewImage(inputname: "icon" | "logo") {
|
||||||
|
let input = this.$refs[inputname] as HTMLInputElement;
|
||||||
|
let previewElement = this.$refs[inputname + "_img"] as HTMLImageElement;
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function (e) {
|
||||||
|
previewElement.src = e.target?.result as string;
|
||||||
|
previewElement.style.display = "block";
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(input.files[0]);
|
||||||
|
} else {
|
||||||
|
previewElement.src = "";
|
||||||
|
previewElement.style.display = "none";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(e: any) {
|
||||||
|
this.setupStatus = "loading";
|
||||||
|
this.setupMessage = "";
|
||||||
|
this.setClubImages({
|
||||||
|
icon: (this.$refs.icon as HTMLInputElement).files?.[0],
|
||||||
|
logo: (this.$refs.logo as HTMLInputElement).files?.[0],
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
// this.setupStatus = "success";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.setupStatus = "failed";
|
||||||
|
this.setupMessage = err.response.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
95
src/components/setup/Mail.vue
Normal file
95
src/components/setup/Mail.vue
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-2" @submit.prevent="setup">
|
||||||
|
<p class="text-center">Mailversand</p>
|
||||||
|
<div class="-space-y-px">
|
||||||
|
<div class="mb-2">
|
||||||
|
<input id="mail" name="mail" type="email" placeholder="Mailadresse" required autocomplete="email" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="user"
|
||||||
|
name="user"
|
||||||
|
type="text"
|
||||||
|
placeholder="Benutzername (kann auch Mail sein)"
|
||||||
|
required
|
||||||
|
autocomplete="username"
|
||||||
|
class="rounded-b-none!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Passwort"
|
||||||
|
required
|
||||||
|
autocomplete="new-password"
|
||||||
|
class="rounded-none!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input id="host" name="host" type="text" placeholder="Server-Host" required class="rounded-none!" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input id="port" name="port" type="number" placeholder="Port (25, 465, 587)" required class="rounded-t-none!" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-2 pt-1">
|
||||||
|
<input type="checkbox" id="secure" />
|
||||||
|
<label for="secure">SSL-Verbindung (setzen bei Port 465)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button type="submit" primary :disabled="setupStatus == 'loading' || setupStatus == 'success'">
|
||||||
|
Mailversand speichern
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="setupStatus == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="setupStatus == 'success'" />
|
||||||
|
<FailureXMark v-else-if="setupStatus == 'failed'" />
|
||||||
|
</div>
|
||||||
|
<p v-if="setupMessage" class="text-center">{{ setupMessage }}</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
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";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
setupStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
setupMessage: "" as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSetupStore, ["setMailConfig", "skip"]),
|
||||||
|
setup(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
this.setupStatus = "loading";
|
||||||
|
this.setupMessage = "";
|
||||||
|
this.setMailConfig({
|
||||||
|
host: formData.host.value,
|
||||||
|
port: formData.port.value,
|
||||||
|
secure: formData.secure.checked,
|
||||||
|
mail: formData.mail.value,
|
||||||
|
username: formData.user.value,
|
||||||
|
password: formData.password.value,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
// this.setupStatus = "success";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.setupStatus = "failed";
|
||||||
|
this.setupMessage = err.response.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,15 +1,7 @@
|
||||||
export interface Config {
|
export interface Config {
|
||||||
server_address: string;
|
server_address: string;
|
||||||
app_name_overwrite: string;
|
|
||||||
imprint_link: string;
|
|
||||||
privacy_link: string;
|
|
||||||
custom_login_message: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config: Config = {
|
export const config: Config = {
|
||||||
server_address: import.meta.env.VITE_SERVER_ADDRESS,
|
server_address: import.meta.env.VITE_SERVER_ADDRESS,
|
||||||
app_name_overwrite: import.meta.env.VITE_APP_NAME_OVERWRITE,
|
|
||||||
imprint_link: import.meta.env.VITE_IMPRINT_LINK,
|
|
||||||
privacy_link: import.meta.env.VITE_PRIVACY_LINK,
|
|
||||||
custom_login_message: import.meta.env.VITE_CUSTOM_LOGIN_MESSAGE,
|
|
||||||
};
|
};
|
||||||
|
|
5
src/enums/newsletterConfigEnum.ts
Normal file
5
src/enums/newsletterConfigEnum.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export enum NewsletterConfigEnum {
|
||||||
|
pdf = "pdf",
|
||||||
|
mail = "mail",
|
||||||
|
none = "none",
|
||||||
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
export enum NewsletterConfigType {
|
|
||||||
pdf = "pdf",
|
|
||||||
mail = "mail",
|
|
||||||
}
|
|
|
@ -18,7 +18,7 @@
|
||||||
--error: #9a0d55;
|
--error: #9a0d55;
|
||||||
--warning: #bb6210;
|
--warning: #bb6210;
|
||||||
--info: #388994;
|
--info: #388994;
|
||||||
--success: #73ad0f;
|
--success: #7ac142;
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
--primary: #ff0d00;
|
--primary: #ff0d00;
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
--error: #9a0d55;
|
--error: #9a0d55;
|
||||||
--warning: #bb6210;
|
--warning: #bb6210;
|
||||||
--info: #4ccbda;
|
--info: #4ccbda;
|
||||||
--success: #73ad0f;
|
--success: #7ac142;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,9 @@ import "../node_modules/nprogress/nprogress.css";
|
||||||
import { http } from "./serverCom";
|
import { http } from "./serverCom";
|
||||||
import "./main.css";
|
import "./main.css";
|
||||||
|
|
||||||
|
// auto generates splash screen for iOS
|
||||||
|
import "pwacompat";
|
||||||
|
|
||||||
NProgress.configure({ showSpinner: false });
|
NProgress.configure({ showSpinner: false });
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
|
@ -2,14 +2,12 @@ import { createRouter, createWebHistory } from "vue-router";
|
||||||
import Login from "@/views/Login.vue";
|
import Login from "@/views/Login.vue";
|
||||||
|
|
||||||
import { isAuthenticated } from "./authGuard";
|
import { isAuthenticated } from "./authGuard";
|
||||||
import { loadAccountData } from "./accountGuard";
|
|
||||||
import { isSetup } from "./setupGuard";
|
import { isSetup } from "./setupGuard";
|
||||||
import { abilityAndNavUpdate } from "./adminGuard";
|
import { abilityAndNavUpdate } from "./adminGuard";
|
||||||
import type { PermissionType, PermissionSection, PermissionModule } from "@/types/permissionTypes";
|
import type { PermissionType, PermissionSection, PermissionModule } from "@/types/permissionTypes";
|
||||||
import { resetMemberStores, setMemberId } from "./memberGuard";
|
import { resetMemberStores, setMemberId } from "./memberGuard";
|
||||||
import { resetProtocolStores, setProtocolId } from "./protocolGuard";
|
import { resetProtocolStores, setProtocolId } from "./protocolGuard";
|
||||||
import { resetNewsletterStores, setNewsletterId } from "./newsletterGuard";
|
import { resetNewsletterStores, setNewsletterId } from "./newsletterGuard";
|
||||||
import { config } from "../config";
|
|
||||||
import { setBackupPage } from "./backupGuard";
|
import { setBackupPage } from "./backupGuard";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
@ -642,6 +640,13 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "settings",
|
||||||
|
name: "admin-management-setting",
|
||||||
|
component: () => import("@/views/admin/management/setting/Setting.vue"),
|
||||||
|
meta: { type: "read", section: "management", module: "setting" },
|
||||||
|
beforeEnter: [abilityAndNavUpdate],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "backup",
|
path: "backup",
|
||||||
name: "admin-management-backup-route",
|
name: "admin-management-backup-route",
|
||||||
|
@ -777,10 +782,6 @@ const router = createRouter({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
router.afterEach((to, from) => {
|
|
||||||
document.title = config.app_name_overwrite || "FF Admin";
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
||||||
declare module "vue-router" {
|
declare module "vue-router" {
|
||||||
|
|
|
@ -135,4 +135,4 @@ async function* streamingFetch(path: string, abort?: AbortController) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { http, newEventSource, streamingFetch, host };
|
export { http, newEventSource, streamingFetch, host, url };
|
||||||
|
|
|
@ -6,7 +6,7 @@ import type {
|
||||||
import { http } from "@/serverCom";
|
import { http } from "@/serverCom";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
|
|
||||||
export const useNewsletterConfigStore = defineStore("newsletterConfi", {
|
export const useNewsletterConfigStore = defineStore("newsletterConfig", {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
config: [] as Array<NewsletterConfigViewModel>,
|
config: [] as Array<NewsletterConfigViewModel>,
|
||||||
|
|
53
src/stores/admin/management/setting.ts
Normal file
53
src/stores/admin/management/setting.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { http } from "@/serverCom";
|
||||||
|
import { type SettingString, type SettingValueMapping } from "@/types/settingTypes";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
export const useSettingStore = defineStore("setting", {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
settings: {} as { [key in SettingString]: SettingValueMapping[key] },
|
||||||
|
loading: "loading" as "loading" | "fetched" | "failed",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
readSetting:
|
||||||
|
(state) =>
|
||||||
|
<K extends SettingString>(key: K): SettingValueMapping[K] => {
|
||||||
|
return state.settings[key];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
fetchSettings() {
|
||||||
|
this.loading = "loading";
|
||||||
|
http
|
||||||
|
.get("/admin/setting")
|
||||||
|
.then((result) => {
|
||||||
|
this.settings = result.data;
|
||||||
|
this.loading = "fetched";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.loading = "failed";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async getSetting(key: SettingString): Promise<AxiosResponse<any, any>> {
|
||||||
|
return await http.get(`/admin/setting/${key}`).then((res) => {
|
||||||
|
//@ts-expect-error
|
||||||
|
this.settings[key] = res.data;
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async updateSetting<K extends SettingString>(
|
||||||
|
key: K,
|
||||||
|
val: SettingValueMapping[K]
|
||||||
|
): Promise<AxiosResponse<any, any>> {
|
||||||
|
return await http.put("/admin/setting", {
|
||||||
|
setting: key,
|
||||||
|
value: val,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async resetSetting(key: SettingString): Promise<AxiosResponse<any, any>> {
|
||||||
|
return await http.delete(`/admin/setting/${key}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -137,6 +137,7 @@ export const useNavigationStore = defineStore("navigation", {
|
||||||
...(abilityStore.can("read", "management", "user") ? [{ key: "user", title: "Benutzer" }] : []),
|
...(abilityStore.can("read", "management", "user") ? [{ key: "user", title: "Benutzer" }] : []),
|
||||||
...(abilityStore.can("read", "management", "role") ? [{ key: "role", title: "Rollen" }] : []),
|
...(abilityStore.can("read", "management", "role") ? [{ key: "role", title: "Rollen" }] : []),
|
||||||
...(abilityStore.can("read", "management", "webapi") ? [{ key: "webapi", title: "Webapi-Token" }] : []),
|
...(abilityStore.can("read", "management", "webapi") ? [{ key: "webapi", title: "Webapi-Token" }] : []),
|
||||||
|
...(abilityStore.can("read", "management", "setting") ? [{ key: "setting", title: "Einstellungen" }] : []),
|
||||||
...(abilityStore.can("read", "management", "backup") ? [{ key: "backup", title: "Backups" }] : []),
|
...(abilityStore.can("read", "management", "backup") ? [{ key: "backup", title: "Backups" }] : []),
|
||||||
...(abilityStore.isAdmin() ? [{ key: "version", title: "Version" }] : []),
|
...(abilityStore.isAdmin() ? [{ key: "version", title: "Version" }] : []),
|
||||||
],
|
],
|
||||||
|
|
30
src/stores/configuration.ts
Normal file
30
src/stores/configuration.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { http } from "../serverCom";
|
||||||
|
|
||||||
|
export const useConfigurationStore = defineStore("configuration", {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
clubName: "",
|
||||||
|
clubImprint: "",
|
||||||
|
clubPrivacy: "",
|
||||||
|
clubWebsite: "",
|
||||||
|
appCustom_login_message: "",
|
||||||
|
appShow_link_to_calendar: false as boolean,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
configure() {
|
||||||
|
http
|
||||||
|
.get("/public/configuration")
|
||||||
|
.then((res) => {
|
||||||
|
this.clubName = res.data["club.name"];
|
||||||
|
this.clubImprint = res.data["club.imprint"];
|
||||||
|
this.clubPrivacy = res.data["club.privacy"];
|
||||||
|
this.clubWebsite = res.data["club.website"];
|
||||||
|
this.appCustom_login_message = res.data["app.custom_login_message"];
|
||||||
|
this.appShow_link_to_calendar = res.data["app.show_link_to_calendar"];
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
130
src/stores/setup.ts
Normal file
130
src/stores/setup.ts
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { http } from "../serverCom";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
import { useConfigurationStore } from "./configuration";
|
||||||
|
|
||||||
|
export const useSetupStore = defineStore("setup", {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
dictionary: ["club", "clubImages", "app", "mail", "account", "finished"],
|
||||||
|
step: 0 as number,
|
||||||
|
successfull: 0 as number,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
stepIndex: (state) => (dict: string) => state.dictionary.findIndex((d) => d == dict),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
skip(dict: string) {
|
||||||
|
let myIndex = this.stepIndex(dict);
|
||||||
|
this.step += 1;
|
||||||
|
if (this.successfull <= myIndex) {
|
||||||
|
this.successfull = myIndex + 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setClub(data: {
|
||||||
|
name?: string;
|
||||||
|
imprint?: string;
|
||||||
|
privacy?: string;
|
||||||
|
website?: string;
|
||||||
|
}): Promise<AxiosResponse<any, any>> {
|
||||||
|
let configStore = useConfigurationStore();
|
||||||
|
|
||||||
|
let myIndex = this.stepIndex("club");
|
||||||
|
const res = await http.post(`/setup/club`, {
|
||||||
|
name: data.name,
|
||||||
|
imprint: data.imprint,
|
||||||
|
privacy: data.privacy,
|
||||||
|
website: data.website,
|
||||||
|
});
|
||||||
|
configStore.configure();
|
||||||
|
|
||||||
|
this.step += 1;
|
||||||
|
if (this.successfull <= myIndex) {
|
||||||
|
this.successfull = myIndex;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
async setClubImages(data: { icon?: File; logo?: File }): Promise<AxiosResponse<any, any>> {
|
||||||
|
let configStore = useConfigurationStore();
|
||||||
|
|
||||||
|
let myIndex = this.stepIndex("clubImages");
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
if (data.icon) {
|
||||||
|
formData.append("icon", data.icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.logo) {
|
||||||
|
formData.append("logo", data.logo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await http.post(`/setup/club/images`, formData, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
configStore.configure();
|
||||||
|
|
||||||
|
this.step += 1;
|
||||||
|
if (this.successfull <= myIndex) {
|
||||||
|
this.successfull = myIndex;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
async setApp(data: { login_message: string; show_cal_link: boolean }): Promise<AxiosResponse<any, any>> {
|
||||||
|
let myIndex = this.stepIndex("app");
|
||||||
|
const res = await http.post(`/setup/app`, {
|
||||||
|
custom_login_message: data.login_message,
|
||||||
|
show_link_to_calendar: data.show_cal_link,
|
||||||
|
});
|
||||||
|
this.step += 1;
|
||||||
|
if (this.successfull <= myIndex) {
|
||||||
|
this.successfull = myIndex;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
async setMailConfig(data: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
secure: boolean;
|
||||||
|
mail: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}): Promise<AxiosResponse<any, any>> {
|
||||||
|
let myIndex = this.stepIndex("mail");
|
||||||
|
const res = await http.post(`/setup/mail`, {
|
||||||
|
host: data.host,
|
||||||
|
port: data.port,
|
||||||
|
secure: data.secure,
|
||||||
|
mail: data.mail,
|
||||||
|
username: data.username,
|
||||||
|
password: data.password,
|
||||||
|
});
|
||||||
|
this.step += 1;
|
||||||
|
if (this.successfull <= myIndex) {
|
||||||
|
this.successfull = myIndex;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
async createAdmin(credentials: {
|
||||||
|
username: string;
|
||||||
|
mail: string;
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
}): Promise<AxiosResponse<any, any>> {
|
||||||
|
let myIndex = this.stepIndex("account");
|
||||||
|
const res = await http.post(`/setup/me`, {
|
||||||
|
username: credentials.username,
|
||||||
|
mail: credentials.mail,
|
||||||
|
firstname: credentials.firstname,
|
||||||
|
lastname: credentials.lastname,
|
||||||
|
});
|
||||||
|
this.step += 1;
|
||||||
|
if (this.successfull < myIndex) {
|
||||||
|
this.successfull = myIndex;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -21,7 +21,8 @@ export type PermissionModule =
|
||||||
| "query_store"
|
| "query_store"
|
||||||
| "template"
|
| "template"
|
||||||
| "template_usage"
|
| "template_usage"
|
||||||
| "backup";
|
| "backup"
|
||||||
|
| "setting";
|
||||||
|
|
||||||
export type PermissionType = "read" | "create" | "update" | "delete";
|
export type PermissionType = "read" | "create" | "update" | "delete";
|
||||||
|
|
||||||
|
@ -67,6 +68,7 @@ export const permissionModules: Array<PermissionModule> = [
|
||||||
"template",
|
"template",
|
||||||
"template_usage",
|
"template_usage",
|
||||||
"backup",
|
"backup",
|
||||||
|
"setting",
|
||||||
];
|
];
|
||||||
export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"];
|
export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"];
|
||||||
export const sectionsAndModules: SectionsAndModulesObject = {
|
export const sectionsAndModules: SectionsAndModulesObject = {
|
||||||
|
@ -84,5 +86,5 @@ export const sectionsAndModules: SectionsAndModulesObject = {
|
||||||
"template_usage",
|
"template_usage",
|
||||||
"newsletter_config",
|
"newsletter_config",
|
||||||
],
|
],
|
||||||
management: ["user", "role", "webapi", "backup"],
|
management: ["user", "role", "webapi", "backup", "setting"],
|
||||||
};
|
};
|
||||||
|
|
80
src/types/settingTypes.ts
Normal file
80
src/types/settingTypes.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
export type SettingTopic = "club" | "app" | "session" | "mail" | "backup" | "security";
|
||||||
|
export type SettingString =
|
||||||
|
| "club.icon"
|
||||||
|
| "club.logo"
|
||||||
|
| "club.name"
|
||||||
|
| "club.imprint"
|
||||||
|
| "club.privacy"
|
||||||
|
| "club.website"
|
||||||
|
| "app.custom_login_message"
|
||||||
|
| "app.show_link_to_calendar"
|
||||||
|
| "session.jwt_expiration"
|
||||||
|
| "session.refresh_expiration"
|
||||||
|
| "session.pwa_refresh_expiration"
|
||||||
|
| "mail.email"
|
||||||
|
| "mail.username"
|
||||||
|
| "mail.password"
|
||||||
|
| "mail.host"
|
||||||
|
| "mail.port"
|
||||||
|
| "mail.secure"
|
||||||
|
| "backup.interval"
|
||||||
|
| "backup.copies";
|
||||||
|
|
||||||
|
export type SettingTypeAtom = "longstring" | "string" | "ms" | "number" | "boolean" | "url" | "email";
|
||||||
|
export type SettingType = SettingTypeAtom | `${SettingTypeAtom}/crypt` | `${SettingTypeAtom}/rand`;
|
||||||
|
|
||||||
|
export type SettingValueMapping = {
|
||||||
|
"club.icon": string;
|
||||||
|
"club.logo": string;
|
||||||
|
"club.name": string;
|
||||||
|
"club.imprint": string;
|
||||||
|
"club.privacy": string;
|
||||||
|
"club.website": string;
|
||||||
|
"app.custom_login_message": string;
|
||||||
|
"app.show_link_to_calendar": boolean;
|
||||||
|
"session.jwt_expiration": string;
|
||||||
|
"session.refresh_expiration": string;
|
||||||
|
"session.pwa_refresh_expiration": string;
|
||||||
|
"mail.email": string;
|
||||||
|
"mail.username": string;
|
||||||
|
"mail.password": string;
|
||||||
|
"mail.host": string;
|
||||||
|
"mail.port": number;
|
||||||
|
"mail.secure": boolean;
|
||||||
|
"backup.interval": number;
|
||||||
|
"backup.copies": number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Typsicherer Zugriff auf Settings
|
||||||
|
export type SettingDefinition<T extends SettingType | SettingTypeAtom[]> = {
|
||||||
|
type: T;
|
||||||
|
default?: string | number | boolean;
|
||||||
|
optional?: boolean;
|
||||||
|
min?: T extends "number" | `number/crypt` | `number/rand` ? number : never;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SettingsSchema = {
|
||||||
|
[key in SettingString]: SettingDefinition<SettingType | SettingTypeAtom[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const settingsType: SettingsSchema = {
|
||||||
|
"club.icon": { type: "string", optional: true },
|
||||||
|
"club.logo": { type: "string", optional: true },
|
||||||
|
"club.name": { type: "string", default: "FF Admin" },
|
||||||
|
"club.imprint": { type: "url", optional: true },
|
||||||
|
"club.privacy": { type: "url", optional: true },
|
||||||
|
"club.website": { type: "url", optional: true },
|
||||||
|
"app.custom_login_message": { type: "string", optional: true },
|
||||||
|
"app.show_link_to_calendar": { type: "boolean", default: true },
|
||||||
|
"session.jwt_expiration": { type: "ms", default: "15m" },
|
||||||
|
"session.refresh_expiration": { type: "ms", default: "1d" },
|
||||||
|
"session.pwa_refresh_expiration": { type: "ms", default: "5d" },
|
||||||
|
"mail.email": { type: "email", optional: false },
|
||||||
|
"mail.username": { type: "string", optional: false },
|
||||||
|
"mail.password": { type: "string/crypt", optional: false },
|
||||||
|
"mail.host": { type: "url", optional: false },
|
||||||
|
"mail.port": { type: "number", default: 587 },
|
||||||
|
"mail.secure": { type: "boolean", default: false },
|
||||||
|
"backup.interval": { type: "number", default: 1, min: 1 },
|
||||||
|
"backup.copies": { type: "number", default: 7, min: 1 },
|
||||||
|
};
|
|
@ -1,13 +1,13 @@
|
||||||
import type { NewsletterConfigType } from "@/enums/newsletterConfigType";
|
import type { NewsletterConfigEnum } from "@/enums/newsletterConfigEnum";
|
||||||
import type { CommunicationTypeViewModel } from "./communicationType.models";
|
import type { CommunicationTypeViewModel } from "./communicationType.models";
|
||||||
|
|
||||||
export interface NewsletterConfigViewModel {
|
export interface NewsletterConfigViewModel {
|
||||||
comTypeId: number;
|
comTypeId: number;
|
||||||
config: NewsletterConfigType;
|
config: NewsletterConfigEnum;
|
||||||
comType: CommunicationTypeViewModel;
|
comType: CommunicationTypeViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetNewsletterConfigViewModel {
|
export interface SetNewsletterConfigViewModel {
|
||||||
comTypeId: number;
|
comTypeId: number;
|
||||||
config: NewsletterConfigType;
|
config: NewsletterConfigEnum;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div class="max-w-md w-full space-y-8 pb-20">
|
<div class="max-w-md w-full space-y-8 pb-20">
|
||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col items-center gap-4">
|
||||||
<img src="/Logo.png" alt="LOGO" class="h-auto w-full" />
|
<AppLogo />
|
||||||
<h2 class="text-center text-4xl font-extrabold text-gray-900">
|
<h2 class="text-center text-4xl font-extrabold text-gray-900">
|
||||||
{{ config.app_name_overwrite || "FF Admin" }}
|
{{ clubName }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -50,7 +50,9 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { resetAllPiniaStores } from "@/helpers/piniaReset";
|
import { resetAllPiniaStores } from "@/helpers/piniaReset";
|
||||||
import FormBottomBar from "@/components/FormBottomBar.vue";
|
import FormBottomBar from "@/components/FormBottomBar.vue";
|
||||||
import { config } from "@/config";
|
import AppLogo from "@/components/AppLogo.vue";
|
||||||
|
import { mapState } from "pinia";
|
||||||
|
import { useConfigurationStore } from "@/stores/configuration";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -61,6 +63,9 @@ export default defineComponent({
|
||||||
loginError: "" as string,
|
loginError: "" as string,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useConfigurationStore, ["clubName"]),
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
resetAllPiniaStores();
|
resetAllPiniaStores();
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<SidebarLayout>
|
<SidebarLayout>
|
||||||
<template #sidebar>
|
<template #sidebar>
|
||||||
<SidebarTemplate
|
<SidebarTemplate mainTitle="Mein Account" :topTitle="clubName" :showTopList="isOwner">
|
||||||
mainTitle="Mein Account"
|
|
||||||
:topTitle="config.app_name_overwrite || 'FF Admin'"
|
|
||||||
:showTopList="isOwner"
|
|
||||||
>
|
|
||||||
<template v-if="isOwner" #topList>
|
<template v-if="isOwner" #topList>
|
||||||
<RoutingLink
|
<RoutingLink
|
||||||
title="Administration"
|
title="Administration"
|
||||||
|
@ -42,13 +38,14 @@ import SidebarTemplate from "@/templates/Sidebar.vue";
|
||||||
import RoutingLink from "@/components/admin/RoutingLink.vue";
|
import RoutingLink from "@/components/admin/RoutingLink.vue";
|
||||||
import { RouterView } from "vue-router";
|
import { RouterView } from "vue-router";
|
||||||
import { useAbilityStore } from "@/stores/ability";
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
import { config } from "@/config";
|
import { useConfigurationStore } from "@/stores/configuration";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useAbilityStore, ["isOwner"]),
|
...mapState(useAbilityStore, ["isOwner"]),
|
||||||
|
...mapState(useConfigurationStore, ["clubName"]),
|
||||||
activeRouteName() {
|
activeRouteName() {
|
||||||
return this.$route.name;
|
return this.$route.name;
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,52 +4,56 @@
|
||||||
<p v-else-if="loading == 'failed'" @click="fetchNewsletterRecipients" class="cursor-pointer">
|
<p v-else-if="loading == 'failed'" @click="fetchNewsletterRecipients" class="cursor-pointer">
|
||||||
↺ laden fehlgeschlagen
|
↺ laden fehlgeschlagen
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col gap-2 h-1/2">
|
|
||||||
|
<div v-if="!showMemberSelect" class="flex flex-row gap-2 items-center">
|
||||||
<select v-model="recipientsByQueryId">
|
<select v-model="recipientsByQueryId">
|
||||||
<option value="def">Optional</option>
|
<option value="def">Optional</option>
|
||||||
<option v-for="query in queries" :key="query.id" :value="query.id">{{ query.title }}</option>
|
<option v-for="query in queries" :key="query.id" :value="query.id">{{ query.title }}</option>
|
||||||
</select>
|
</select>
|
||||||
<p>Empfänger durch gespeicherte Abfrage</p>
|
<div title="Empfänger manuell hinzufügen" @click="showMemberSelect = true">
|
||||||
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
<UserPlusIcon class="w-7 h-7 cursor-pointer" />
|
||||||
<div
|
|
||||||
v-for="member in queried"
|
|
||||||
:key="member.id"
|
|
||||||
class="flex flex-row h-fit w-full border border-primary rounded-md bg-primary p-2 text-white justify-between items-center"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
|
||||||
<p>Newsletter senden an Typ: {{ member.sendNewsletter?.type.type }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 h-1/2">
|
<div v-else class="flex flex-row gap-2 items-center">
|
||||||
<MemberSearchSelect
|
<MemberSearchSelect
|
||||||
title="weitere Empfänger suchen"
|
title="weitere Empfänger suchen"
|
||||||
|
showTitleAsPlaceholder
|
||||||
v-model="recipients"
|
v-model="recipients"
|
||||||
:disabled="!can('create', 'club', 'newsletter')"
|
:disabled="!can('create', 'club', 'newsletter')"
|
||||||
/>
|
/>
|
||||||
|
<div title="Empfänger über Query hinzufügen" @click="showMemberSelect = false">
|
||||||
<p>Ausgewählte Empfänger</p>
|
<ArchiveBoxIcon class="w-7 h-7 cursor-pointer" />
|
||||||
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
|
||||||
<div
|
|
||||||
v-for="member in selected"
|
|
||||||
:key="member.id"
|
|
||||||
class="flex flex-row h-fit w-full border border-primary rounded-md bg-primary p-2 text-white justify-between items-center"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
|
||||||
<p>Newsletter senden an Typ: {{ member.sendNewsletter?.type.type }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TrashIcon
|
|
||||||
v-if="can('create', 'club', 'newsletter')"
|
|
||||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
|
||||||
@click="removeSelected(member.id)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p v-if="!showMemberSelect">Empfänger durch gespeicherte Abfrage</p>
|
||||||
|
<p v-else>Ausgewählte Empfänger</p>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
||||||
|
<div
|
||||||
|
v-for="member in showRecipientsByMode"
|
||||||
|
:key="member.id"
|
||||||
|
class="flex flex-row gap-2 h-fit w-full border border-primary rounded-md bg-primary p-2 text-white items-center"
|
||||||
|
>
|
||||||
|
<ExclamationTriangleIcon v-if="member.sendNewsletter == null" class="w-7 h-7" />
|
||||||
|
|
||||||
|
<div class="grow">
|
||||||
|
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
||||||
|
<p>Newsletter senden an Typ: {{ member.sendNewsletter?.type.type ?? "---" }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TrashIcon
|
||||||
|
v-if="can('create', 'club', 'newsletter') && showMemberSelect"
|
||||||
|
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||||
|
@click="removeSelected(member.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="countOfNoConfig != 0" class="flex flex-row items-center gap-2 pt-3">
|
||||||
|
<ExclamationTriangleIcon class="text-red-500 w-5 h-5" />
|
||||||
|
<p>{{ countOfNoConfig }} Mitglieder der Auswahl haben keinen Newsletter-Versand konfiguriert!</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -67,7 +71,7 @@ import {
|
||||||
TransitionRoot,
|
TransitionRoot,
|
||||||
} from "@headlessui/vue";
|
} from "@headlessui/vue";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { TrashIcon } from "@heroicons/vue/24/outline";
|
import { ArchiveBoxIcon, ExclamationTriangleIcon, TrashIcon, UserPlusIcon } from "@heroicons/vue/24/outline";
|
||||||
import { useMemberStore } from "@/stores/admin/club/member/member";
|
import { useMemberStore } from "@/stores/admin/club/member/member";
|
||||||
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||||
import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter";
|
import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter";
|
||||||
|
@ -93,6 +97,7 @@ export default defineComponent({
|
||||||
return {
|
return {
|
||||||
query: "" as String,
|
query: "" as String,
|
||||||
members: [] as Array<MemberViewModel>,
|
members: [] as Array<MemberViewModel>,
|
||||||
|
showMemberSelect: false as boolean,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -125,6 +130,17 @@ export default defineComponent({
|
||||||
.some((d) => (d.memberId ?? d.id) == m.id)
|
.some((d) => (d.memberId ?? d.id) == m.id)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
showRecipientsByMode() {
|
||||||
|
return (this.showMemberSelect ? this.selected : this.queried).sort((a, b) => {
|
||||||
|
const aHasConfig = a.sendNewsletter != null;
|
||||||
|
const bHasConfig = b.sendNewsletter != null;
|
||||||
|
if (aHasConfig === bHasConfig) return 0;
|
||||||
|
return aHasConfig ? -1 : 1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
countOfNoConfig() {
|
||||||
|
return this.showRecipientsByMode.filter((member) => member.sendNewsletter == null).length;
|
||||||
|
},
|
||||||
recipientsByQueryId: {
|
recipientsByQueryId: {
|
||||||
get() {
|
get() {
|
||||||
return this.activeNewsletterObj?.recipientsByQueryId ?? "def";
|
return this.activeNewsletterObj?.recipientsByQueryId ?? "def";
|
||||||
|
|
36
src/views/admin/management/setting/Setting.vue
Normal file
36
src/views/admin/management/setting/Setting.vue
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<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">Einstellungen</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<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">Einstellungen</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MainTemplate>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import MainTemplate from "@/templates/Main.vue";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
import { useSettingStore } from "@/stores/admin/management/setting";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
computed: {
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchSettings();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSettingStore, ["fetchSettings"]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div class="max-w-md w-full space-y-8 pb-20">
|
<div class="max-w-md w-full space-y-8 pb-20">
|
||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col items-center gap-4">
|
||||||
<img src="/Logo.png" alt="LOGO" class="h-auto w-full" />
|
<AppLogo />
|
||||||
<h2 class="text-center text-4xl font-extrabold text-gray-900">Einrichtung</h2>
|
<h2 class="text-center text-4xl font-extrabold text-gray-900">Einrichtung</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import FormBottomBar from "@/components/FormBottomBar.vue";
|
import FormBottomBar from "@/components/FormBottomBar.vue";
|
||||||
import TextCopy from "@/components/TextCopy.vue";
|
import TextCopy from "@/components/TextCopy.vue";
|
||||||
|
import AppLogo from "@/components/AppLogo.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div class="max-w-md w-full space-y-8 pb-20">
|
<div class="max-w-md w-full space-y-8 pb-20">
|
||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col items-center gap-4">
|
||||||
<img src="/Logo.png" alt="LOGO" class="h-auto w-full" />
|
<AppLogo />
|
||||||
<h2 class="text-center text-4xl font-extrabold text-gray-900">TOTP zurücksetzen</h2>
|
<h2 class="text-center text-4xl font-extrabold text-gray-900">TOTP zurücksetzen</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { RouterLink } from "vue-router";
|
import { RouterLink } from "vue-router";
|
||||||
import FormBottomBar from "@/components/FormBottomBar.vue";
|
import FormBottomBar from "@/components/FormBottomBar.vue";
|
||||||
import TextCopy from "@/components/TextCopy.vue";
|
import TextCopy from "@/components/TextCopy.vue";
|
||||||
|
import AppLogo from "@/components/AppLogo.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div class="max-w-md w-full space-y-8 pb-20">
|
<div class="max-w-md w-full space-y-8 pb-20">
|
||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col items-center gap-4">
|
||||||
<img src="/Logo.png" alt="LOGO" class="h-auto w-full" />
|
<AppLogo />
|
||||||
<h2 class="text-center text-4xl font-extrabold text-gray-900">TOTP zurücksetzen</h2>
|
<h2 class="text-center text-4xl font-extrabold text-gray-900">TOTP zurücksetzen</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import FormBottomBar from "@/components/FormBottomBar.vue";
|
import FormBottomBar from "@/components/FormBottomBar.vue";
|
||||||
|
import AppLogo from "@/components/AppLogo.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -2,36 +2,18 @@
|
||||||
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div class="max-w-md w-full space-y-8 pb-20">
|
<div class="max-w-md w-full space-y-8 pb-20">
|
||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col items-center gap-4">
|
||||||
<img src="/Logo.png" alt="LOGO" class="h-auto w-full" />
|
<AppLogo />
|
||||||
<h2 class="text-center text-4xl font-extrabold text-gray-900">Einrichtung</h2>
|
<h2 class="text-center text-4xl font-extrabold text-gray-900">Einrichtung</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<CheckProgressBar :total="dictionary.length" :step="step" :successfull="successfull" />
|
||||||
|
|
||||||
<form class="flex flex-col gap-2" @submit.prevent="setup">
|
<Club v-if="step == stepIndex('club')" />
|
||||||
<div class="-space-y-px">
|
<Images v-else-if="step == stepIndex('clubImages')" />
|
||||||
<div>
|
<App v-else-if="step == stepIndex('app')" />
|
||||||
<input id="username" name="username" type="text" required placeholder="Benutzer" class="rounded-b-none!" />
|
<Mail v-else-if="step == stepIndex('mail')" />
|
||||||
</div>
|
<Account v-else-if="step == stepIndex('account')" />
|
||||||
<div>
|
<Finished v-else-if="step == stepIndex('finished')" />
|
||||||
<input id="mail" name="mail" type="email" required placeholder="Mailadresse" class="rounded-none!" />
|
<p v-else class="text-center">UI-Fehler - versuchen Sie einen Reload der Seite</p>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input id="firstname" name="firstname" type="text" required placeholder="Vorname" class="rounded-none!" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input id="lastname" name="lastname" type="text" required placeholder="Nachname" class="rounded-t-none!" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-row gap-2">
|
|
||||||
<button type="submit" primary :disabled="setupStatus == 'loading' || setupStatus == 'success'">
|
|
||||||
Admin-Account anlegen
|
|
||||||
</button>
|
|
||||||
<Spinner v-if="setupStatus == 'loading'" class="my-auto" />
|
|
||||||
<SuccessCheckmark v-else-if="setupStatus == 'success'" />
|
|
||||||
<FailureXMark v-else-if="setupStatus == 'failed'" />
|
|
||||||
</div>
|
|
||||||
<p v-if="setupMessage" class="text-center">{{ setupMessage }}</p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<FormBottomBar />
|
<FormBottomBar />
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,41 +22,23 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
|
||||||
import FormBottomBar from "@/components/FormBottomBar.vue";
|
import FormBottomBar from "@/components/FormBottomBar.vue";
|
||||||
|
import AppLogo from "@/components/AppLogo.vue";
|
||||||
|
import App from "@/components/setup/App.vue";
|
||||||
|
import Account from "@/components/setup/Account.vue";
|
||||||
|
import CheckProgressBar from "@/components/CheckProgressBar.vue";
|
||||||
|
import { mapState } from "pinia";
|
||||||
|
import { useSetupStore } from "@/stores/setup";
|
||||||
|
import Club from "@/components/setup/Club.vue";
|
||||||
|
import Images from "@/components/setup/Images.vue";
|
||||||
|
import Finished from "@/components/setup/Finished.vue";
|
||||||
|
import Mail from "../../components/setup/Mail.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
data() {
|
computed: {
|
||||||
return {
|
...mapState(useSetupStore, ["step", "stepIndex", "successfull", "dictionary"]),
|
||||||
setupStatus: undefined as undefined | "loading" | "success" | "failed",
|
|
||||||
setupMessage: "" as string,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setup(e: any) {
|
|
||||||
let formData = e.target.elements;
|
|
||||||
this.setupStatus = "loading";
|
|
||||||
this.setupMessage = "";
|
|
||||||
this.$http
|
|
||||||
.post(`/setup`, {
|
|
||||||
username: formData.username.value,
|
|
||||||
mail: formData.mail.value,
|
|
||||||
firstname: formData.firstname.value,
|
|
||||||
lastname: formData.lastname.value,
|
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
this.setupStatus = "success";
|
|
||||||
this.setupMessage = "Sie haben einen Verifizierungslink per Mail erhalten.";
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
this.setupStatus = "failed";
|
|
||||||
this.setupMessage = err.response.data;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div class="max-w-md w-full space-y-8 pb-20">
|
<div class="max-w-md w-full space-y-8 pb-20">
|
||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col items-center gap-4">
|
||||||
<img src="/Logo.png" alt="LOGO" class="h-auto w-full" />
|
<AppLogo />
|
||||||
<h2 class="text-center text-4xl font-extrabold text-gray-900">Einrichtung</h2>
|
<h2 class="text-center text-4xl font-extrabold text-gray-900">Einrichtung</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { RouterLink } from "vue-router";
|
import { RouterLink } from "vue-router";
|
||||||
import FormBottomBar from "@/components/FormBottomBar.vue";
|
import FormBottomBar from "@/components/FormBottomBar.vue";
|
||||||
import TextCopy from "@/components/TextCopy.vue";
|
import TextCopy from "@/components/TextCopy.vue";
|
||||||
|
import AppLogo from "@/components/AppLogo.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -92,7 +93,7 @@ export default defineComponent({
|
||||||
this.setupStatus = "loading";
|
this.setupStatus = "loading";
|
||||||
this.setupError = "";
|
this.setupError = "";
|
||||||
this.$http
|
this.$http
|
||||||
.put(`/setup`, {
|
.post(`/setup/finish`, {
|
||||||
token: this.token,
|
token: this.token,
|
||||||
mail: this.mail,
|
mail: this.mail,
|
||||||
totp: formData.totp.value,
|
totp: formData.totp.value,
|
||||||
|
|
|
@ -39,27 +39,9 @@ export default defineConfig({
|
||||||
VitePWA({
|
VitePWA({
|
||||||
registerType: "autoUpdate",
|
registerType: "autoUpdate",
|
||||||
injectRegister: "auto",
|
injectRegister: "auto",
|
||||||
includeAssets: ["favicon.png", "favicon.ico"],
|
manifest: false,
|
||||||
manifest: {
|
|
||||||
name: "__APPNAMEOVERWRITE__",
|
|
||||||
short_name: "__APPNAMEOVERWRITE__",
|
|
||||||
theme_color: "#990b00",
|
|
||||||
display: "standalone",
|
|
||||||
start_url: "/",
|
|
||||||
icons: [
|
|
||||||
{
|
|
||||||
src: "favicon.ico",
|
|
||||||
sizes: "48x48",
|
|
||||||
type: "image/ico",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "favicon.png",
|
|
||||||
sizes: "512x512",
|
|
||||||
type: "image/png",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
workbox: {
|
workbox: {
|
||||||
|
navigateFallbackDenylist: [/^\/api*/],
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
urlPattern: /^\/api\//,
|
urlPattern: /^\/api\//,
|
||||||
|
|
Loading…
Add table
Reference in a new issue