Compare commits

...

8 commits

79 changed files with 545 additions and 228 deletions

View file

@ -1,4 +1,5 @@
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

View file

@ -1,4 +1,5 @@
VITE_SERVER_ADDRESS = __SERVERADDRESS__
VITE_APP_NAME_OVERWRITE = __APPNAMEOVERWRITE__
VITE_IMPRINT_LINK = __IMPRINTLINK__
VITE_PRIVACY_LINK = __PRIVACYLINK__
VITE_CUSTOM_LOGIN_MESSAGE = __CUSTOMLOGINMESSAGE__

View file

@ -31,12 +31,14 @@ services:
restart: unless-stopped
#environment:
# - SERVERADDRESS=<backend_url (https://... | http://...)> # wichtig: ohne pfad
# - SERVERADDRESS=<backend_url (https://... | http://...)> # wichtig: ohne Pfad
# - APPNAMEOVERWRITE=Mitgliederverwaltung # ersetzt den Namen FF-Admin auf der Login-Seite und sonstigen Positionen in der Oberfläche
# - IMPRINTLINK=https://mywebsite-imprint-url
# - PRIVACYLINK=https://mywebsite-privacy-url
# - CUSTOMLOGINMESSAGE=betrieben von xy
#volumes:
# - <volume|local path>/myfavicon.png:/usr/share/nginx/html/favicon.png
# - <volume|local path>/myfavicon.ico:/usr/share/nginx/html/favicon.ico # 48x48 px Auflösung
# - <volume|local path>/myfavicon.png:/usr/share/nginx/html/favicon.png # 512x512 px Auflösung - wird als pwa Icon genutzt
# - <volume|local path>/mylogo.png:/usr/share/nginx/html/Logo.png
```
@ -62,7 +64,7 @@ npm run start
### Konfiguration
Ein eigenes favicon und Logo kann über ein volume ausgetauscht werden.
Ein eigenes Favicon und Logo kann über das verwenden Volume ausgetauscht werden. Es dürfen jedoch nur einzelne Dateien ausgetauscht werden.
## Einrichtung

View file

@ -1,6 +1,6 @@
# FF Admin
## FF Admin ist eine Verwaltungsoberfläche für die Feuerwehr:
## FF Admin ist eine Verwaltungsoberfläche für die Feuerwehr oder andere Vereine:
FF Admin bietet folgende Module:
- Mitgliederverwaltung
@ -23,3 +23,6 @@ FF Admin ist in Verein, Wehr, Einstellungen und Nutzerverwaltung getrennt.
Die den Modulen zugrunde liegenden Daten können in den Einstellungen gesetzt werden.
Fast alle Daten lassen sich einstellen, damit es keine Einschränkungen in der Auswahl von Werten... gibt. Diese Modularität muss allerdings bei einigen Modulen gesondert eingestellt werden.
## Verwendung
Damit FF Admin auch für andere Vereine genutzt werden kann, muss keine erweiterte Konfiguration vorgenommen werden. Am besten ist es alle nicht benötigten Module in der Berechtigungsverwaltung zu deaktivieren. So wird normalerweise der Abschnitt Wehr nicht außerhalb der Feuerwehr benötigt. So müssen hier lediglich keine Berechtigungen vergeben werden und das Modul ist außer für Administratoren oder Owner nicht sichtbar.

View file

@ -1,9 +1,10 @@
#!/bin/sh
keys="SERVERADDRESS IMPRINTLINK PRIVACYLINK CUSTOMLOGINMESSAGE"
keys="SERVERADDRESS APPNAMEOVERWRITE IMPRINTLINK PRIVACYLINK CUSTOMLOGINMESSAGE"
files="/usr/share/nginx/html/assets/config-*.js /usr/share/nginx/html/manifest.webmanifest"
# Replace env vars in files served by NGINX
for file in /usr/share/nginx/html/assets/config-*.js
for file in $files
do
echo "Processing $file ...";
for key in $keys

View file

@ -4,7 +4,6 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mitgliederverwaltung</title>
</head>
<body>
<div id="app"></div>

View file

@ -12,7 +12,7 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/",
"bnp": "npm run build-only && npm run preview",
"generate-pwa-assets": "pwa-assets-generator --preset minimal-2023 public/CM.svg"
"generate-pwa-assets": "pwa-assets-generator --preset minimal-2023 public/fw-wappen.png"
},
"repository": {
"type": "git",

View file

@ -1,8 +1,8 @@
{
"id": "0",
"name": "administration-db",
"createdAt": "2025-01-08T15:50:20.331Z",
"updatedAt": "2025-01-08T15:50:20.331Z",
"createdAt": "2025-01-12T13:30:56.612Z",
"updatedAt": "2025-01-12T13:30:56.612Z",
"databaseType": "mariadb",
"tables": [
{
@ -1291,6 +1291,18 @@
},
{
"id": "101",
"name": "postalCode",
"type": {
"name": "varchar",
"id": "varchar"
},
"unique": false,
"nullable": true,
"primaryKey": false,
"createdAt": 1736688552836
},
{
"id": "102",
"name": "city",
"type": {
"id": "varchar",
@ -1305,7 +1317,7 @@
"createdAt": 1734524896260
},
{
"id": "102",
"id": "103",
"name": "street",
"type": {
"id": "varchar",
@ -1320,7 +1332,7 @@
"createdAt": 1734524896260
},
{
"id": "103",
"id": "104",
"name": "streetNumber",
"type": {
"id": "int",
@ -1333,7 +1345,7 @@
"createdAt": 1734524896260
},
{
"id": "104",
"id": "105",
"name": "streetNumberAddition",
"type": {
"id": "varchar",
@ -1348,7 +1360,7 @@
"createdAt": 1734524896260
},
{
"id": "105",
"id": "106",
"name": "typeId",
"type": {
"id": "int",
@ -1360,7 +1372,7 @@
"createdAt": 1734524896260
},
{
"id": "106",
"id": "107",
"name": "memberId",
"type": {
"id": "int",
@ -1372,7 +1384,7 @@
"createdAt": 1734524896260
},
{
"id": "107",
"id": "108",
"name": "isSMSAlarming",
"type": {
"id": "tinyint",
@ -1387,7 +1399,7 @@
],
"indexes": [
{
"id": "108",
"id": "109",
"name": "PRIMARY",
"unique": true,
"fieldIds": [
@ -1396,20 +1408,20 @@
"createdAt": 1734524896260
},
{
"id": "109",
"id": "110",
"name": "FK_21994db635b47e07f45b2686a51",
"unique": false,
"fieldIds": [
"105"
"106"
],
"createdAt": 1734524896260
},
{
"id": "110",
"id": "111",
"name": "FK_fc5f59e5c9aafdedd25ed8ed36e",
"unique": false,
"fieldIds": [
"106"
"107"
],
"createdAt": 1734524896260
}
@ -1421,14 +1433,14 @@
"diagramId": "7gb18czobyir"
},
{
"id": "111",
"id": "112",
"name": "member_qualifications",
"schema": "administration",
"x": -250.37357560579426,
"y": 82.72883357238302,
"fields": [
{
"id": "112",
"id": "113",
"name": "id",
"type": {
"id": "int",
@ -1440,7 +1452,7 @@
"createdAt": 1734524896259
},
{
"id": "113",
"id": "114",
"name": "note",
"type": {
"id": "varchar",
@ -1455,7 +1467,7 @@
"createdAt": 1734524896259
},
{
"id": "114",
"id": "115",
"name": "start",
"type": {
"id": "date",
@ -1467,7 +1479,7 @@
"createdAt": 1734524896259
},
{
"id": "115",
"id": "116",
"name": "end",
"type": {
"id": "date",
@ -1480,7 +1492,7 @@
"createdAt": 1734524896259
},
{
"id": "116",
"id": "117",
"name": "terminationReason",
"type": {
"id": "varchar",
@ -1495,7 +1507,7 @@
"createdAt": 1734524896259
},
{
"id": "117",
"id": "118",
"name": "memberId",
"type": {
"id": "int",
@ -1507,7 +1519,7 @@
"createdAt": 1734524896259
},
{
"id": "118",
"id": "119",
"name": "qualificationId",
"type": {
"id": "int",
@ -1521,31 +1533,31 @@
],
"indexes": [
{
"id": "119",
"id": "120",
"name": "PRIMARY",
"unique": true,
"fieldIds": [
"112"
],
"createdAt": 1734524896259
},
{
"id": "120",
"name": "FK_98b70e687c35709d2f01b3d7d74",
"unique": false,
"fieldIds": [
"117"
"113"
],
"createdAt": 1734524896259
},
{
"id": "121",
"name": "FK_dbebe53df1caa0b6715a220b0ea",
"name": "FK_98b70e687c35709d2f01b3d7d74",
"unique": false,
"fieldIds": [
"118"
],
"createdAt": 1734524896259
},
{
"id": "122",
"name": "FK_dbebe53df1caa0b6715a220b0ea",
"unique": false,
"fieldIds": [
"119"
],
"createdAt": 1734524896259
}
],
"color": "#ff6b8a",
@ -1555,14 +1567,14 @@
"diagramId": "7gb18czobyir"
},
{
"id": "122",
"id": "123",
"name": "executive_position",
"schema": "administration",
"x": -542.0601569820527,
"y": 474.7348899814151,
"fields": [
{
"id": "123",
"id": "124",
"name": "id",
"type": {
"id": "int",
@ -1574,7 +1586,7 @@
"createdAt": 1734524896260
},
{
"id": "124",
"id": "125",
"name": "position",
"type": {
"id": "varchar",
@ -1590,11 +1602,11 @@
],
"indexes": [
{
"id": "125",
"id": "126",
"name": "PRIMARY",
"unique": true,
"fieldIds": [
"123"
"124"
],
"createdAt": 1734524896260
}
@ -1606,14 +1618,14 @@
"diagramId": "7gb18czobyir"
},
{
"id": "126",
"id": "127",
"name": "qualification",
"schema": "administration",
"x": -568.0578068648438,
"y": 192.56221408776412,
"fields": [
{
"id": "127",
"id": "128",
"name": "id",
"type": {
"id": "int",
@ -1625,7 +1637,7 @@
"createdAt": 1734524896260
},
{
"id": "128",
"id": "129",
"name": "qualification",
"type": {
"id": "varchar",
@ -1639,7 +1651,7 @@
"createdAt": 1734524896260
},
{
"id": "129",
"id": "130",
"name": "description",
"type": {
"id": "varchar",
@ -1656,11 +1668,11 @@
],
"indexes": [
{
"id": "130",
"id": "131",
"name": "PRIMARY",
"unique": true,
"fieldIds": [
"127"
"128"
],
"createdAt": 1734524896260
}
@ -1674,28 +1686,14 @@
],
"relationships": [
{
"id": "131",
"id": "132",
"name": "FK_1fd52c8f109123e5a2c67dc2c83",
"sourceSchema": "administration",
"targetSchema": "administration",
"sourceTableId": "1",
"targetTableId": "122",
"targetTableId": "123",
"sourceFieldId": "7",
"targetFieldId": "123",
"sourceCardinality": "many",
"targetCardinality": "one",
"createdAt": 1734524896262,
"diagramId": "7gb18czobyir"
},
{
"id": "132",
"name": "FK_21994db635b47e07f45b2686a51",
"sourceSchema": "administration",
"targetSchema": "administration",
"sourceTableId": "96",
"targetTableId": "71",
"sourceFieldId": "105",
"targetFieldId": "72",
"targetFieldId": "124",
"sourceCardinality": "many",
"targetCardinality": "one",
"createdAt": 1734524896262,
@ -1703,6 +1701,20 @@
},
{
"id": "133",
"name": "FK_21994db635b47e07f45b2686a51",
"sourceSchema": "administration",
"targetSchema": "administration",
"sourceTableId": "96",
"targetTableId": "71",
"sourceFieldId": "106",
"targetFieldId": "72",
"sourceCardinality": "many",
"targetCardinality": "one",
"createdAt": 1734524896262,
"diagramId": "7gb18czobyir"
},
{
"id": "134",
"name": "FK_2912b056a5d0b7977360a986164",
"sourceSchema": "administration",
"targetSchema": "administration",
@ -1716,7 +1728,7 @@
"diagramId": "7gb18czobyir"
},
{
"id": "134",
"id": "135",
"name": "FK_3b4b41597707b13086e71727422",
"sourceSchema": "administration",
"targetSchema": "administration",
@ -1730,13 +1742,13 @@
"diagramId": "7gb18czobyir"
},
{
"id": "135",
"id": "136",
"name": "FK_98b70e687c35709d2f01b3d7d74",
"sourceSchema": "administration",
"targetSchema": "administration",
"sourceTableId": "111",
"sourceTableId": "112",
"targetTableId": "60",
"sourceFieldId": "117",
"sourceFieldId": "118",
"targetFieldId": "61",
"sourceCardinality": "many",
"targetCardinality": "one",
@ -1744,7 +1756,7 @@
"diagramId": "7gb18czobyir"
},
{
"id": "136",
"id": "137",
"name": "FK_a47e04bfd3671d8a375d1896d25",
"sourceSchema": "administration",
"targetSchema": "administration",
@ -1758,7 +1770,7 @@
"diagramId": "7gb18czobyir"
},
{
"id": "137",
"id": "138",
"name": "FK_ba47b44c2ddf34c1bcc75df6675",
"sourceSchema": "administration",
"targetSchema": "administration",
@ -1772,21 +1784,21 @@
"diagramId": "7gb18czobyir"
},
{
"id": "138",
"id": "139",
"name": "FK_dbebe53df1caa0b6715a220b0ea",
"sourceSchema": "administration",
"targetSchema": "administration",
"sourceTableId": "111",
"targetTableId": "126",
"sourceFieldId": "118",
"targetFieldId": "127",
"sourceTableId": "112",
"targetTableId": "127",
"sourceFieldId": "119",
"targetFieldId": "128",
"sourceCardinality": "many",
"targetCardinality": "one",
"createdAt": 1734524896262,
"diagramId": "7gb18czobyir"
},
{
"id": "139",
"id": "140",
"name": "FK_e9fd4d37c4ac0fb08bd6eeeda3c",
"sourceSchema": "administration",
"targetSchema": "administration",
@ -1800,13 +1812,13 @@
"diagramId": "7gb18czobyir"
},
{
"id": "140",
"id": "141",
"name": "FK_fc5f59e5c9aafdedd25ed8ed36e",
"sourceSchema": "administration",
"targetSchema": "administration",
"sourceTableId": "96",
"targetTableId": "60",
"sourceFieldId": "106",
"sourceFieldId": "107",
"targetFieldId": "61",
"sourceCardinality": "many",
"targetCardinality": "one",
@ -1816,7 +1828,7 @@
],
"dependencies": [
{
"id": "141",
"id": "142",
"schema": "administration",
"tableId": "60",
"dependentSchema": "administration",
@ -1825,36 +1837,27 @@
"diagramId": "7gb18czobyir"
},
{
"id": "142",
"id": "143",
"schema": "administration",
"tableId": "126",
"tableId": "127",
"dependentSchema": "administration",
"dependentTableId": "86",
"createdAt": 1734524897266,
"diagramId": "7gb18czobyir"
},
{
"id": "143",
"id": "144",
"schema": "administration",
"tableId": "111",
"tableId": "112",
"dependentSchema": "administration",
"dependentTableId": "86",
"createdAt": 1734524897267,
"diagramId": "7gb18czobyir"
},
{
"id": "144",
"schema": "administration",
"tableId": "60",
"dependentSchema": "administration",
"dependentTableId": "11",
"createdAt": 1734524897283,
"diagramId": "7gb18czobyir"
},
{
"id": "145",
"schema": "administration",
"tableId": "1",
"tableId": "60",
"dependentSchema": "administration",
"dependentTableId": "11",
"createdAt": 1734524897283,
@ -1863,6 +1866,15 @@
{
"id": "146",
"schema": "administration",
"tableId": "1",
"dependentSchema": "administration",
"dependentTableId": "11",
"createdAt": 1734524897283,
"diagramId": "7gb18czobyir"
},
{
"id": "147",
"schema": "administration",
"tableId": "60",
"dependentSchema": "administration",
"dependentTableId": "21",
@ -1870,7 +1882,7 @@
"diagramId": "7gb18czobyir"
},
{
"id": "147",
"id": "148",
"schema": "administration",
"tableId": "56",
"dependentSchema": "administration",
@ -1879,7 +1891,7 @@
"diagramId": "7gb18czobyir"
},
{
"id": "148",
"id": "149",
"schema": "administration",
"tableId": "35",
"dependentSchema": "administration",
@ -1888,16 +1900,16 @@
"diagramId": "7gb18czobyir"
},
{
"id": "149",
"id": "150",
"schema": "administration",
"tableId": "122",
"tableId": "123",
"dependentSchema": "administration",
"dependentTableId": "11",
"createdAt": 1734524897283,
"diagramId": "7gb18czobyir"
},
{
"id": "150",
"id": "151",
"schema": "administration",
"tableId": "60",
"dependentSchema": "administration",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 KiB

After

Width:  |  Height:  |  Size: 650 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 MiB

After

Width:  |  Height:  |  Size: 11 MiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 KiB

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/fw-wappen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

View file

@ -5,10 +5,16 @@
<a v-if="config.privacy_link" :href="config.privacy_link" target="_blank">Impressum</a>
</div>
<p v-if="config.custom_login_message">{{ config.custom_login_message }}</p>
<a href="https://jk-effects.com" target="_blank"> &copy; Admin-Portal by JK Effects </a>
<p>
&copy;
<a href="https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin" target="_blank">Admin-Portal</a>
by
<a href="https://jk-effects.com" target="_blank">JK Effects</a>
</p>
</div>
</template>
<script setup lang="ts">
import { config } from '../config'
import { config } from '@/config'
</script>

View file

@ -2,7 +2,7 @@
<header class="flex flex-row h-16 min-h-16 justify-between p-3 md:px-5 bg-white shadow-sm">
<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" />
<h1 v-if="false" class="font-bold text-3xl w-fit whitespace-nowrap">FF Admin</h1>
<h1 v-if="false" class="font-bold text-3xl w-fit whitespace-nowrap">{{config.app_name_overwrite || "FF Admin"}}</h1>
</RouterLink>
<div class="flex flex-row gap-2 items-center">
<div v-if="authCheck" class="hidden md:flex flex-row gap-2 h-full align-middle">
@ -30,6 +30,7 @@ import { useAuthStore } from "@/stores/auth";
import { useNavigationStore } from "@/stores/admin/navigation";
import TopLevelLink from "./admin/TopLevelLink.vue";
import UserMenu from "./UserMenu.vue";
import { config } from "@/config"
</script>
<script lang="ts">

View file

@ -113,10 +113,10 @@ export default defineComponent({
},
methods: {
...mapActions(useNotificationStore, ["revoke"]),
close(id: number) {
close(id: string) {
this.revoke(id);
},
hovering(id: number, value: boolean, timeout?: number) {
hovering(id: string, value: boolean, timeout?: number) {
if (value) {
clearTimeout(this.timeouts[id]);
} else {

View file

@ -159,7 +159,7 @@ const loadPage = (newPage: number | ".") => {
if (pageEnd > entryCount.value) pageEnd = entryCount.value;
let loadedElementCount = filterData(props.items, searchString.value, pageStart, pageEnd).length;
console.log(loadedElementCount, props.maxEntriesPerPage, pageStart, pageEnd)
if (loadedElementCount < props.maxEntriesPerPage && (pageEnd != props.totalCount || loadedElementCount == 0))
emit("loadData", pageStart, props.maxEntriesPerPage, searchString.value);

View file

@ -24,7 +24,7 @@ export default defineComponent({
default: "LINK",
},
link: {
type: Object as PropType<string | { name: string }>,
type: Object as PropType<string | { name: string, params?:{[key:string]:string} }>,
default: "/",
},
active: {

View file

@ -124,11 +124,7 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button
primary-outline
@click="closeModal"
:disabled="status != null && status != 'loading' && status?.status != 'failed'"
>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
@ -194,6 +190,7 @@ export default defineComponent({
location: formData.location.value,
allDay: this.allDay,
};
this.status = "loading";
this.createCalendar(createCalendar)
.then(() => {
this.status = { status: "success" };

View file

@ -38,7 +38,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -78,6 +80,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useCalendarStore, ["deleteCalendar"]),
triggerDelete() {
this.status = "loading";
this.deleteCalendar(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -166,11 +166,7 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button
primary-outline
@click="closeModal"
:disabled="status != null && status != 'loading' && status?.status != 'failed'"
>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen / schließen
</button>
</div>
@ -264,6 +260,7 @@ export default defineComponent({
location: formData.location.value,
allDay: this.calendar.allDay,
};
this.status = "loading";
this.updateCalendar(updateCalendar)
.then(() => {
this.fetchItem();

View file

@ -133,6 +133,7 @@ export default defineComponent({
birthdate: formData.birthdate.value,
internalId: formData.internalId.value,
};
this.status = "loading";
this.createMember(createMember)
.then(() => {
this.status = { status: "success" };

View file

@ -72,6 +72,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberStore, ["deleteMember"]),
triggerDelete() {
this.status = "loading";
this.deleteMember(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -140,6 +140,7 @@ export default defineComponent({
given: formData.given.checked,
awardId: this.selectedAward.id,
};
this.status = "loading";
this.createMemberAward(createMemberAward)
.then(() => {
this.status = { status: "success" };

View file

@ -65,6 +65,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberAwardStore, ["deleteMemberAward"]),
triggerDelete() {
this.status = "loading";
this.deleteMemberAward(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -83,7 +83,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">schließen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
schließen
</button>
</div>
</div>
</div>
@ -167,6 +169,7 @@ export default defineComponent({
given: formData.given.checked,
awardId: this.memberAward.awardId,
};
this.status = "loading";
this.updateMemberAward(updateMemberAward)
.then(() => {
this.fetchItem();

View file

@ -166,7 +166,7 @@ export default defineComponent({
preferred: formData.preferred.checked,
mobile: formData.mobile?.value,
email: formData.email?.value,
postalCode: formData.postalCode.value,
postalCode: formData.postalCode?.value,
city: formData.city?.value,
street: formData.street?.value,
streetNumber: formData.streetNumber?.value,
@ -175,6 +175,7 @@ export default defineComponent({
isSMSAlarming: formData.isSMSAlarming?.checked,
typeId: this.selectedCommunicationType.id,
};
this.status = "loading";
this.createCommunication(createCommunication)
.then(() => {
this.status = { status: "success" };

View file

@ -68,6 +68,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useCommunicationStore, ["deleteCommunication"]),
triggerDelete() {
this.status = "loading";
this.deleteCommunication(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -62,7 +62,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">schließen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
schließen
</button>
</div>
</div>
</div>
@ -144,6 +146,7 @@ export default defineComponent({
isNewsletterMain: formData.isNewsletterMain.checked,
isSMSAlarming: formData.isSMSAlarming?.checked,
};
this.status = "loading";
this.updateCommunication(updateCommunication)
.then(() => {
this.fetchItem();

View file

@ -141,6 +141,7 @@ export default defineComponent({
note: formData.note.value,
executivePositionId: this.selectedExecutivePosition.id,
};
this.status = "loading";
this.createMemberExecutivePosition(createMemberExecutivePosition)
.then(() => {
this.status = { status: "success" };

View file

@ -65,6 +65,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberExecutivePositionStore, ["deleteMemberExecutivePosition"]),
triggerDelete() {
this.status = "loading";
this.deleteMemberExecutivePosition(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -89,7 +89,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">schließen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
schließen
</button>
</div>
</div>
</div>
@ -176,6 +178,7 @@ export default defineComponent({
note: formData.note.value,
executivePositionId: this.memberExecutivePosition.executivePositionId,
};
this.status = "loading";
this.updateMemberExecutivePosition(updateMemberExecutivePosition)
.then(() => {
this.fetchItem();

View file

@ -148,6 +148,7 @@ export default defineComponent({
note: formData.note.value,
qualificationId: this.selectedQualification.id,
};
this.status = "loading";
this.createMemberQualification(createMemberQualification)
.then(() => {
this.status = { status: "success" };

View file

@ -69,6 +69,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberQualificationStore, ["deleteMemberQualification"]),
triggerDelete() {
this.status = "loading";
this.deleteMemberQualification(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -90,7 +90,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">schließen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
schließen
</button>
</div>
</div>
</div>
@ -175,6 +177,7 @@ export default defineComponent({
terminationReason: formData.terminationReason.value,
qualificationId: this.memberQualification.qualificationId,
};
this.status = "loading";
this.updateMemberQualification(updateMemberQualification)
.then(() => {
this.fetchItem();

View file

@ -131,6 +131,7 @@ export default defineComponent({
start: formData.start.value,
statusId: this.selectedStatus.id,
};
this.status = "loading";
this.createMembership(createMember)
.then(() => {
this.status = { status: "success" };

View file

@ -67,6 +67,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMembershipStore, ["deleteMembership"]),
triggerDelete() {
this.status = "loading";
this.deleteMembership(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -86,7 +86,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">schließen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
schließen
</button>
</div>
</div>
</div>
@ -170,6 +172,7 @@ export default defineComponent({
terminationReason: formData.terminationReason.value,
statusId: this.membership.statusId,
};
this.status = "loading";
this.updateMembership(updateMembership)
.then(() => {
this.fetchItem();

View file

@ -61,6 +61,7 @@ export default defineComponent({
let createNewsletter: CreateNewsletterViewModel = {
title: formData.title.value,
};
this.status = "loading";
this.createNewsletter(createNewsletter)
.then(() => {
this.status = { status: "success" };

View file

@ -0,0 +1,44 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Newsletter Mail-Versand Logs</p>
</div>
<br />
<div class="h-96 overflow-y-scroll">
<p v-for="entry in mailSourceMessages">
{{ entry }}
</p>
</div>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal">
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 { useNewsletterPrintoutStore } from "@/stores/admin/club/newsletter/newsletterPrintout";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
};
},
computed:{
...mapState(useNewsletterPrintoutStore, ["mailSourceMessages"])
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
},
});
</script>

View file

@ -0,0 +1,50 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Newsletter Druck-Prozess Logs</p>
</div>
<br />
<div class="flex flex-col gap-2 h-96 overflow-y-scroll">
<div
v-for="entry in pdfSourceMessages"
class="flex flex-row gap-2 border border-gray-200 rounded-md p-1 items-center"
>
<SuccessCheckmark v-if="entry.factor == 'success'" class="w-5 h-5" />
<InformationCircleIcon v-else-if="entry.factor == 'info'" class="w-5 h-5 min-h-5 min-w-5 text-gray-500" />
<FailureXMark v-else-if="entry.factor == 'failed'" class="w-5 h-5" />
<p>{{ entry.iteration }}/{{ entry.total }}: {{ entry.msg }}</p>
</div>
</div>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal">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 { useNewsletterPrintoutStore } from "@/stores/admin/club/newsletter/newsletterPrintout";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import { InformationCircleIcon } from "@heroicons/vue/24/solid";
import FailureXMark from "@/components/FailureXMark.vue";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {};
},
computed: {
...mapState(useNewsletterPrintoutStore, ["pdfSourceMessages"]),
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
},
});
</script>

View file

@ -64,6 +64,7 @@ export default defineComponent({
title: formData.title.value,
date: formData.date.value,
};
this.status = "loading";
this.createProtocol(createProtocol)
.then(() => {
this.status = { status: "success" };

View file

@ -19,7 +19,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -57,6 +59,7 @@ export default defineComponent({
let createAward: CreateAwardViewModel = {
award: formData.award.value,
};
this.status = "loading";
this.createAward(createAward)
.then(() => {
this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -57,6 +59,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useAwardStore, ["deleteAward"]),
triggerDelete() {
this.status = "loading";
this.deleteAward(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -32,11 +32,7 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button
primary-outline
@click="closeModal"
:disabled="status != null && status != 'loading' && status?.status != 'failed'"
>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
@ -82,6 +78,7 @@ export default defineComponent({
nscdr: formData.nscdr.checked,
passphrase: formData.passphrase.value,
};
this.status = "loading";
this.createCalendarType(createCalendarType)
.then(() => {
this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -56,6 +58,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useCalendarTypeStore, ["deleteCalendarType"]),
triggerDelete() {
this.status = "loading";
this.deleteCalendarType(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -65,7 +65,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -114,6 +116,7 @@ export default defineComponent({
type: formData.communicationType.value,
fields: this.selectedFields,
};
this.status = "loading";
this.createCommunicationType(createCommunicationType)
.then(() => {
this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -56,6 +58,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useCommunicationTypeStore, ["deleteCommunicationType"]),
triggerDelete() {
this.status = "loading";
this.deleteCommunicationType(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -19,7 +19,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -57,6 +59,7 @@ export default defineComponent({
let createExecutivePosition: CreateExecutivePositionViewModel = {
position: formData.executivePosition.value,
};
this.status = "loading";
this.createExecutivePosition(createExecutivePosition)
.then(() => {
this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -57,6 +59,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useExecutivePositionStore, ["deleteExecutivePosition"]),
triggerDelete() {
this.status = "loading";
this.deleteExecutivePosition(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -19,7 +19,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -57,6 +59,7 @@ export default defineComponent({
let createMembershipStatus: CreateMembershipStatusViewModel = {
status: formData.membershipStatus.value,
};
this.status = "loading";
this.createMembershipStatus(createMembershipStatus)
.then(() => {
this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -57,6 +59,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMembershipStatusStore, ["deleteMembershipStatus"]),
triggerDelete() {
this.status = "loading";
this.deleteMembershipStatus(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -23,7 +23,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -62,6 +64,7 @@ export default defineComponent({
qualification: formData.qualification.value,
description: formData.description.value,
};
this.status = "loading";
this.createQualification(createQualification)
.then(() => {
this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -57,6 +59,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useQualificationStore, ["deleteQualification"]),
triggerDelete() {
this.status = "loading";
this.deleteQualification(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -19,7 +19,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -62,6 +64,7 @@ export default defineComponent({
title: formData.title.value,
query: this.query ?? "",
};
this.status = "loading";
this.createQueryStore(createAward)
.then(() => {
this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -56,6 +58,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useQueryStoreStore, ["deleteQueryStore"]),
triggerDelete() {
this.status = "loading";
this.deleteQueryStore(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -21,7 +21,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -73,6 +75,7 @@ export default defineComponent({
id: this.data,
query: this.query ?? "",
};
this.status = "loading";
this.updateActiveQueryStore(updateQuery)
.then(() => {
this.status = { status: "success" };

View file

@ -23,7 +23,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -62,6 +64,7 @@ export default defineComponent({
template: formData.template.value,
description: formData.description.value,
};
this.status = "loading";
this.createTemplate(createTemplate)
.then((res) => {
this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -57,6 +59,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useTemplateStore, ["deleteTemplate"]),
triggerDelete() {
this.status = "loading";
this.deleteTemplate(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -19,7 +19,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -53,6 +55,7 @@ export default defineComponent({
...mapActions(useRoleStore, ["createRole"]),
triggerCreateRole(e: any) {
let formData = e.target.elements;
this.status = "loading";
this.createRole(formData.role.value)
.then(() => {
this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -56,6 +58,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useRoleStore, ["deleteRole"]),
triggerDeleteRole() {
this.status = "loading";
this.deleteRole(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -56,6 +58,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useUserStore, ["deleteUser"]),
triggerDeleteUser() {
this.status = "loading";
this.deleteUser(this.data)
.then(() => {
this.status = { status: "success" };

View file

@ -32,7 +32,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
@ -70,13 +72,13 @@ export default defineComponent({
...mapActions(useInviteStore, ["createInvite"]),
invite(e: any) {
let formData = e.target.elements;
this.status = "loading";
let createInvite: CreateInviteViewModel = {
username: formData.username.value,
mail: formData.mail.value,
firstname: formData.firstname.value,
lastname: formData.lastname.value,
};
this.status = "loading";
this.createInvite(createInvite)
.then((result) => {
this.status = { status: "success" };
@ -86,7 +88,7 @@ export default defineComponent({
})
.catch((err) => {
this.status = { status: "failed", reason: err.response.data };
})
});
},
},
});

View file

@ -1,5 +1,6 @@
export interface Config {
server_address: string;
app_name_overwrite: string;
imprint_link: string;
privacy_link: string;
custom_login_message: string;
@ -7,6 +8,7 @@ export interface Config {
export const config: Config = {
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,

View file

@ -14,7 +14,6 @@ export function flattenQueryResult(result: Array<QueryResult>): Array<{ [key: st
const newKey = prefix ? `${prefix}_${key}` : key;
if (Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null)) {
console.log(value, newKey);
const arrayResults: Array<{ [key: string]: FieldType }> = [];
value.forEach((item) => {
const flattenedItems = flatten(item, newKey);
@ -29,7 +28,6 @@ export function flattenQueryResult(result: Array<QueryResult>): Array<{ [key: st
});
results = tempResults;
} else if (value && typeof value === "object" && !Array.isArray(value)) {
console.log(value, newKey);
const objResults = flatten(value as QueryResult, newKey);
const tempResults: Array<{ [key: string]: FieldType }> = [];
results.forEach((res) => {

View file

@ -9,6 +9,7 @@ import type { PermissionType, PermissionSection, PermissionModule } from "@/type
import { resetMemberStores, setMemberId } from "./memberGuard";
import { resetProtocolStores, setProtocolId } from "./protocolGuard";
import { resetNewsletterStores, setNewsletterId } from "./newsletterGuard";
import { config } from "../config";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -683,6 +684,10 @@ const router = createRouter({
],
});
router.afterEach((to, from) => {
document.title = config.app_name_overwrite || "FF Admin";
});
export default router;
declare module "vue-router" {

View file

@ -9,8 +9,6 @@ export async function setNewsletterId(to: any, from: any, next: any) {
useNewsletterDatesStore().$reset();
useNewsletterRecipientsStore().$reset();
useNewsletterPrintoutStore().unsubscribePdfPrintingProgress();
useNewsletterPrintoutStore().unsubscribeMailSendingProgress();
useNewsletterPrintoutStore().$reset();
next();
@ -23,8 +21,6 @@ export async function resetNewsletterStores(to: any, from: any, next: any) {
useNewsletterDatesStore().$reset();
useNewsletterRecipientsStore().$reset();
useNewsletterPrintoutStore().unsubscribePdfPrintingProgress();
useNewsletterPrintoutStore().unsubscribeMailSendingProgress();
useNewsletterPrintoutStore().$reset();
next();

View file

@ -28,6 +28,15 @@ http.interceptors.request.use(
}
}
const isPWA =
window.matchMedia("(display-mode: standalone)").matches ||
window.matchMedia("(display-mode: fullscreen)").matches;
if (isPWA) {
if (config.headers) {
config.headers["X-PWA-Client"] = isPWA ? "true" : "false";
}
}
return config;
},
(error) => {
@ -53,11 +62,15 @@ http.interceptors.response.use(
.then(() => {
return http(originalRequest);
})
.catch();
.catch(() => {});
}
const notificationStore = useNotificationStore();
notificationStore.push("Fehler", error.response.data, "error");
if (error.toString().includes("Network Error")) {
notificationStore.push("Netzwerkfehler", "Server nicht erreichbar!", "error");
} else {
notificationStore.push("Fehler", error.response.data, "error");
}
return Promise.reject(error);
}
@ -99,4 +112,25 @@ function newEventSource(path: string) {
});
}
export { http, newEventSource, host };
async function* streamingFetch(path: string, abort?: AbortController) {
await refreshToken()
.then(() => {})
.catch(() => {});
const token = localStorage.getItem("accessToken");
const response = await fetch(url + "/api" + path, {
signal: abort?.signal,
headers: {
Authorization: `Bearer ${token}`,
},
});
const reader = response.body?.getReader();
while (true && reader) {
const { done, value } = await reader.read();
if (done) break;
yield new TextDecoder().decode(value);
}
}
export { http, newEventSource, streamingFetch, host };

View file

@ -1,8 +1,9 @@
import { defineStore } from "pinia";
import { http, newEventSource } from "@/serverCom";
import { http, newEventSource, 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";
export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
state: () => {
@ -12,10 +13,10 @@ export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
printing: undefined as undefined | "loading" | "success" | "failed",
sending: undefined as undefined | "loading" | "success" | "failed",
sendingPreview: undefined as undefined | "loading" | "success" | "failed",
pdfProgessSource: undefined as undefined | EventSourcePolyfill,
mailProgessSource: undefined as undefined | EventSourcePolyfill,
pdfSourceMessages: [] as Array<Object>,
mailSourceMessages: [] as Array<Object>,
pdfSourceMessages: [] as Array<{ kind: string; factor: string; [key: string]: string }>,
mailSourceMessages: [] as Array<{ kind: string; factor: string; [key: string]: string }>,
pdfPrintingAbort: undefined as undefined | AbortController,
mailSendingAbort: undefined as undefined | AbortController,
};
},
actions: {
@ -63,6 +64,7 @@ export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
});
},
createNewsletterPrintout() {
this.subscribePdfPrintingProgress();
this.printing = "loading";
const newsletterId = useNewsletterStore().activeNewsletter;
if (newsletterId == null) return;
@ -78,10 +80,12 @@ export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
.finally(() => {
setTimeout(() => {
this.printing = undefined;
this.pdfPrintingAbort?.abort();
}, 1500);
});
},
createNewsletterSend() {
this.subscribeMailSendingProgress();
this.sending = "loading";
const newsletterId = useNewsletterStore().activeNewsletter;
if (newsletterId == null) return;
@ -96,32 +100,55 @@ export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
.finally(() => {
setTimeout(() => {
this.sending = undefined;
this.mailSendingAbort?.abort();
}, 1500);
});
},
subscribePdfPrintingProgress() {
// const newsletterId = useNewsletterStore().activeNewsletter;
// if (this.pdfProgessSource != undefined) return;
// this.pdfProgessSource = newEventSource(`/admin/newsletter/${newsletterId}/printoutprogress`);
// this.pdfProgessSource.onmessage = (event) => {
// console.log("pdf", event);
// };
async subscribePdfPrintingProgress() {
this.pdfSourceMessages = [];
const newsletterId = useNewsletterStore().activeNewsletter;
const notificationStore = useNotificationStore();
this.pdfPrintingAbort = new AbortController();
for await (let chunk of streamingFetch(
`/admin/newsletter/${newsletterId}/printoutprogress`,
this.pdfPrintingAbort
)) {
chunk.split("//").forEach((r) => {
if (r.trim() != "") {
let data = JSON.parse(r);
this.pdfSourceMessages.push(data);
let type: NotificationType = "info";
let timeout = undefined;
if (data.factor == "failed") {
type = "error";
timeout = 0;
}
notificationStore.push(`Druck: ${data.iteration}/${data.total}`, `${data.msg}`, type, timeout);
}
});
this.fetchNewsletterPrintout();
}
},
subscribeMailSendingProgress() {
// const newsletterId = useNewsletterStore().activeNewsletter;
// if (this.mailProgessSource != undefined) return;
// this.mailProgessSource = newEventSource(`/admin/newsletter/${newsletterId}/sendprogress`);
// this.mailProgessSource.onmessage = (event) => {
// console.log("mail", event);
// };
},
unsubscribePdfPrintingProgress() {
this.pdfProgessSource?.close();
this.pdfProgessSource = undefined;
},
unsubscribeMailSendingProgress() {
this.mailProgessSource?.close();
this.mailProgessSource = undefined;
async subscribeMailSendingProgress() {
this.mailSourceMessages = [];
const newsletterId = useNewsletterStore().activeNewsletter;
const notificationStore = useNotificationStore();
this.mailSendingAbort = new AbortController();
for await (let chunk of streamingFetch(`/admin/newsletter/${newsletterId}/sendprogress`, this.mailSendingAbort)) {
chunk.split("//").forEach((r) => {
if (r.trim() != "") {
let data = JSON.parse(r);
this.mailSourceMessages.push(data);
let type: NotificationType = "info";
let timeout = undefined;
if (data.factor == "failed") {
type = "error";
timeout = 0;
}
notificationStore.push(`Mailversand: ${data.iteration}/${data.total}`, `${data.msg}`, type, timeout);
}
});
}
},
},
});

View file

@ -38,7 +38,6 @@ export const useProtocolDecisionStore = defineStore("protocolDecision", {
this.loading = "fetched";
})
.catch((err) => {
console.log(err);
this.loading = "failed";
});
},

View file

@ -98,30 +98,32 @@ export const useNavigationStore = defineStore("navigation", {
settings: {
mainTitle: "Einstellungen",
main: [
...(abilityStore.can("read", "settings", "qualification")
? [{ key: "qualification", title: "Qualifikationen" }]
: []),
{ key: "divider1", title: "Mitgliederdaten" },
...(abilityStore.can("read", "settings", "award") ? [{ key: "award", title: "Auszeichnungen" }] : []),
...(abilityStore.can("read", "settings", "executive_position")
? [{ key: "executive_position", title: "Vereinsämter" }]
: []),
...(abilityStore.can("read", "settings", "communication_type")
? [{ key: "communication_type", title: "Kommunikationsarten" }]
: []),
...(abilityStore.can("read", "settings", "membership_status")
? [{ key: "membership_status", title: "Mitgliedsstatus" }]
: []),
...(abilityStore.can("read", "settings", "calendar_type")
? [{ key: "calendar_type", title: "Terminarten" }]
...(abilityStore.can("read", "settings", "qualification")
? [{ key: "qualification", title: "Qualifikationen" }]
: []),
...(abilityStore.can("read", "settings", "executive_position")
? [{ key: "executive_position", title: "Vereinsämter" }]
: []),
{ key: "divider2", title: "Einstellungen" },
...(abilityStore.can("read", "settings", "newsletter_config")
? [{ key: "newsletter_config", title: "Newsletter Konfiguration" }]
: []),
...(abilityStore.can("read", "settings", "query") ? [{ key: "query_store", title: "Query Store" }] : []),
...(abilityStore.can("read", "settings", "template") ? [{ key: "template", title: "Templates" }] : []),
...(abilityStore.can("read", "settings", "template_usage")
? [{ key: "template_usage", title: "Template-Verwendung" }]
: []),
...(abilityStore.can("read", "settings", "newsletter_config")
? [{ key: "newsletter_config", title: "Newsletter Konfiguration" }]
...(abilityStore.can("read", "settings", "calendar_type")
? [{ key: "calendar_type", title: "Terminarten" }]
: []),
...(abilityStore.can("read", "settings", "query") ? [{ key: "query_store", title: "Query Store" }] : []),
],
},
user: {

View file

@ -1,7 +1,7 @@
import { defineStore } from "pinia";
export interface Notification {
id: number;
id: string;
title: string;
text: string;
type: NotificationType;
@ -19,7 +19,7 @@ export const useNotificationStore = defineStore("notification", {
},
actions: {
push(title: string, text: string, type: NotificationType, timeout: number = 5000) {
let id = Date.now();
let id = `${Date.now()}_${Math.random()}`;
this.notifications.push({
id,
title,
@ -27,14 +27,16 @@ export const useNotificationStore = defineStore("notification", {
type,
indicator: false,
});
setTimeout(() => {
this.notifications[this.notifications.findIndex((n) => n.id === id)].indicator = true;
}, 100);
this.timeouts[id] = setTimeout(() => {
this.revoke(id);
}, timeout);
if (timeout != 0) {
setTimeout(() => {
this.notifications[this.notifications.findIndex((n) => n.id === id)].indicator = true;
}, 100);
this.timeouts[id] = setTimeout(() => {
this.revoke(id);
}, timeout);
}
},
revoke(id: number) {
revoke(id: string) {
this.notifications.splice(
this.notifications.findIndex((n) => n.id === id),
1

View file

@ -3,7 +3,7 @@
<div class="max-w-md w-full space-y-8 pb-20">
<div class="flex flex-col items-center gap-4">
<img src="/Logo.png" alt="LOGO" class="h-36" />
<h2 class="text-center text-4xl font-extrabold text-gray-900">FF Admin</h2>
<h2 class="text-center text-4xl font-extrabold text-gray-900">{{config.app_name_overwrite || "FF Admin"}}</h2>
</div>
<form class="flex flex-col gap-2" @submit.prevent="login">
@ -48,6 +48,7 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { resetAllPiniaStores } from "@/helpers/piniaReset";
import FormBottomBar from "@/components/FormBottomBar.vue";
import { config } from "@/config"
</script>
<script lang="ts">

View file

@ -1,7 +1,7 @@
<template>
<SidebarLayout>
<template #sidebar>
<SidebarTemplate mainTitle="Mein Account" topTitle="FF Admin" :showTopList="isOwner">
<SidebarTemplate mainTitle="Mein Account" :topTitle="config.app_name_overwrite || 'FF Admin'" :showTopList="isOwner">
<template v-if="isOwner" #topList>
<RoutingLink
title="Administration"
@ -38,6 +38,7 @@ import SidebarTemplate from "@/templates/Sidebar.vue";
import RoutingLink from "@/components/admin/RoutingLink.vue";
import { RouterView } from "vue-router";
import { useAbilityStore } from "@/stores/ability";
import { config } from "@/config"
</script>
<script lang="ts">

View file

@ -16,13 +16,15 @@
/>
</template>
<template #list>
<RoutingLink
v-for="item in activeNavigationObject.main"
:key="item.key"
:title="item.title"
:link="{ name: `admin-${activeNavigation}-${item.key}` }"
:active="activeLink == item.key"
/>
<div v-for="item in activeNavigationObject.main" :key="item.key">
<RoutingLink
v-if="!item.key.includes('divider')"
:title="item.title"
:link="{ name: `admin-${activeNavigation}-${item.key}` }"
:active="activeLink == item.key"
/>
<p v-else class="pt-4 border-b border-gray-300">{{ item.title }}</p>
</div>
</template>
</SidebarTemplate>
</template>

View file

@ -61,6 +61,12 @@
<SuccessCheckmark v-else-if="sendingPreview == 'success'" />
<FailureXMark v-else-if="sendingPreview == 'failed'" />
</button>
<button v-if="pdfSourceMessages.length != 0" primary-outline class="!w-fit" @click="openPdfLogs">
Druck Logs
</button>
<button v-if="mailSourceMessages.length != 0" primary-outline class="!w-fit" @click="openMailLogs">
Versand Logs
</button>
</div>
</div>
</template>
@ -84,13 +90,19 @@ export default defineComponent({
newsletterId: String,
},
computed: {
...mapState(useNewsletterPrintoutStore, ["printout", "loading", "printing", "sending", "sendingPreview"]),
...mapState(useNewsletterPrintoutStore, [
"printout",
"loading",
"printing",
"sending",
"sendingPreview",
"mailSourceMessages",
"pdfSourceMessages",
]),
...mapState(useAbilityStore, ["can"]),
},
mounted() {
this.fetchNewsletterPrintout();
this.subscribeMailSendingProgress();
this.subscribePdfPrintingProgress();
},
methods: {
...mapActions(useModalStore, ["openModal"]),
@ -100,8 +112,6 @@ export default defineComponent({
"fetchNewsletterPrintoutById",
"createNewsletterMailPreview",
"createNewsletterSend",
"subscribeMailSendingProgress",
"subscribePdfPrintingProgress",
]),
openPdfShow(filename?: string) {
this.openModal(
@ -122,6 +132,20 @@ export default defineComponent({
})
.catch(() => {});
},
openPdfLogs() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/club/newsletter/NewsletterPrintingProgressModal.vue"))
)
);
},
openMailLogs() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/club/newsletter/NewsletterMailProgressModal.vue"))
)
);
},
},
});
</script>

View file

@ -139,7 +139,6 @@ export default defineComponent({
if (!fromSave) this.loadDesign();
})
.catch((err) => {
console.log(err);
this.loading = "failed";
});
},

View file

@ -3,7 +3,7 @@
<template #sidebar>
<SidebarTemplate mainTitle="Dokumentation">
<template #list>
<RoutingLink title="FF Admin" :link="{ name: 'docs-page', params: { page: 'ff-admin' } }" :active="page == 'ff-admin'" />
<RoutingLink title="Admin" :link="{ name: 'docs-page', params: { page: 'ff-admin' } }" :active="page == 'ff-admin'" />
<RoutingLink title="Mitgliederverwaltung" :link="{ name: 'docs-page', params: { page: 'member' } }" :active="page == 'member'" />
<RoutingLink title="Kalendar" :link="{ name: 'docs-page', params: { page: 'calendar' } }" :active="page == 'calendar'" />
<RoutingLink title="Newsletter-Versand" :link="{ name: 'docs-page', params: { page: 'newsletter' } }" :active="page == 'newsletter'" />

View file

@ -5,6 +5,7 @@ import vue from "@vitejs/plugin-vue";
import vueDevTools from "vite-plugin-vue-devtools";
import Markdown from "unplugin-vue-markdown/vite";
import hljs from "highlight.js";
import { VitePWA } from "vite-plugin-pwa";
// https://vitejs.dev/config/
export default defineConfig({
@ -34,6 +35,28 @@ export default defineConfig({
});
},
}),
VitePWA({
registerType: "autoUpdate",
injectRegister: "auto",
includeAssets: ["favicon.png", "favicon.ico"],
manifest: {
name: "__APPNAMEOVERWRITE__",
short_name: "__APPNAMEOVERWRITE__",
theme_color: "#990b00",
icons: [
{
src: "favicon.ico",
sizes: "48x48",
type: "image/png",
},
{
src: "favicon.png",
sizes: "512x512",
type: "image/png",
},
],
},
}),
],
resolve: {
alias: {