Merge pull request 'patches v1.0.2' (#37) from develop into main

Reviewed-on: #37
This commit is contained in:
Julian Krauser 2025-01-13 10:22:30 +00:00
commit 557ee051ab
79 changed files with 545 additions and 228 deletions

View file

@ -1,4 +1,5 @@
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_IMPRINT_LINK = https://mywebsite-imprint-url
VITE_PRIVACY_LINK = https://mywebsite-privacy-url VITE_PRIVACY_LINK = https://mywebsite-privacy-url
VITE_CUSTOM_LOGIN_MESSAGE = betrieben von xy VITE_CUSTOM_LOGIN_MESSAGE = betrieben von xy

View file

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

View file

@ -31,12 +31,14 @@ services:
restart: unless-stopped restart: unless-stopped
#environment: #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 # - IMPRINTLINK=https://mywebsite-imprint-url
# - PRIVACYLINK=https://mywebsite-privacy-url # - PRIVACYLINK=https://mywebsite-privacy-url
# - CUSTOMLOGINMESSAGE=betrieben von xy # - CUSTOMLOGINMESSAGE=betrieben von xy
#volumes: #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 # - <volume|local path>/mylogo.png:/usr/share/nginx/html/Logo.png
``` ```
@ -62,7 +64,7 @@ npm run start
### Konfiguration ### 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 ## Einrichtung

View file

@ -1,6 +1,6 @@
# FF Admin # 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: FF Admin bietet folgende Module:
- Mitgliederverwaltung - 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. 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. 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 #!/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 # Replace env vars in files served by NGINX
for file in /usr/share/nginx/html/assets/config-*.js for file in $files
do do
echo "Processing $file ..."; echo "Processing $file ...";
for key in $keys for key in $keys

View file

@ -4,7 +4,6 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" /> <link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mitgliederverwaltung</title>
</head> </head>
<body> <body>
<div id="app"></div> <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", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/", "format": "prettier --write src/",
"bnp": "npm run build-only && npm run preview", "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": { "repository": {
"type": "git", "type": "git",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -38,7 +38,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -78,6 +80,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]), ...mapActions(useModalStore, ["closeModal"]),
...mapActions(useCalendarStore, ["deleteCalendar"]), ...mapActions(useCalendarStore, ["deleteCalendar"]),
triggerDelete() { triggerDelete() {
this.status = "loading";
this.deleteCalendar(this.data) this.deleteCalendar(this.data)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -83,7 +83,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -167,6 +169,7 @@ export default defineComponent({
given: formData.given.checked, given: formData.given.checked,
awardId: this.memberAward.awardId, awardId: this.memberAward.awardId,
}; };
this.status = "loading";
this.updateMemberAward(updateMemberAward) this.updateMemberAward(updateMemberAward)
.then(() => { .then(() => {
this.fetchItem(); this.fetchItem();

View file

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

View file

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

View file

@ -62,7 +62,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -144,6 +146,7 @@ export default defineComponent({
isNewsletterMain: formData.isNewsletterMain.checked, isNewsletterMain: formData.isNewsletterMain.checked,
isSMSAlarming: formData.isSMSAlarming?.checked, isSMSAlarming: formData.isSMSAlarming?.checked,
}; };
this.status = "loading";
this.updateCommunication(updateCommunication) this.updateCommunication(updateCommunication)
.then(() => { .then(() => {
this.fetchItem(); this.fetchItem();

View file

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

View file

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

View file

@ -89,7 +89,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -176,6 +178,7 @@ export default defineComponent({
note: formData.note.value, note: formData.note.value,
executivePositionId: this.memberExecutivePosition.executivePositionId, executivePositionId: this.memberExecutivePosition.executivePositionId,
}; };
this.status = "loading";
this.updateMemberExecutivePosition(updateMemberExecutivePosition) this.updateMemberExecutivePosition(updateMemberExecutivePosition)
.then(() => { .then(() => {
this.fetchItem(); this.fetchItem();

View file

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

View file

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

View file

@ -90,7 +90,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -175,6 +177,7 @@ export default defineComponent({
terminationReason: formData.terminationReason.value, terminationReason: formData.terminationReason.value,
qualificationId: this.memberQualification.qualificationId, qualificationId: this.memberQualification.qualificationId,
}; };
this.status = "loading";
this.updateMemberQualification(updateMemberQualification) this.updateMemberQualification(updateMemberQualification)
.then(() => { .then(() => {
this.fetchItem(); this.fetchItem();

View file

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

View file

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

View file

@ -86,7 +86,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -170,6 +172,7 @@ export default defineComponent({
terminationReason: formData.terminationReason.value, terminationReason: formData.terminationReason.value,
statusId: this.membership.statusId, statusId: this.membership.statusId,
}; };
this.status = "loading";
this.updateMembership(updateMembership) this.updateMembership(updateMembership)
.then(() => { .then(() => {
this.fetchItem(); this.fetchItem();

View file

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

View file

@ -19,7 +19,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -57,6 +59,7 @@ export default defineComponent({
let createAward: CreateAwardViewModel = { let createAward: CreateAwardViewModel = {
award: formData.award.value, award: formData.award.value,
}; };
this.status = "loading";
this.createAward(createAward) this.createAward(createAward)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -57,6 +59,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]), ...mapActions(useModalStore, ["closeModal"]),
...mapActions(useAwardStore, ["deleteAward"]), ...mapActions(useAwardStore, ["deleteAward"]),
triggerDelete() { triggerDelete() {
this.status = "loading";
this.deleteAward(this.data) this.deleteAward(this.data)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

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

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -56,6 +58,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]), ...mapActions(useModalStore, ["closeModal"]),
...mapActions(useCalendarTypeStore, ["deleteCalendarType"]), ...mapActions(useCalendarTypeStore, ["deleteCalendarType"]),
triggerDelete() { triggerDelete() {
this.status = "loading";
this.deleteCalendarType(this.data) this.deleteCalendarType(this.data)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -65,7 +65,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -114,6 +116,7 @@ export default defineComponent({
type: formData.communicationType.value, type: formData.communicationType.value,
fields: this.selectedFields, fields: this.selectedFields,
}; };
this.status = "loading";
this.createCommunicationType(createCommunicationType) this.createCommunicationType(createCommunicationType)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -56,6 +58,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]), ...mapActions(useModalStore, ["closeModal"]),
...mapActions(useCommunicationTypeStore, ["deleteCommunicationType"]), ...mapActions(useCommunicationTypeStore, ["deleteCommunicationType"]),
triggerDelete() { triggerDelete() {
this.status = "loading";
this.deleteCommunicationType(this.data) this.deleteCommunicationType(this.data)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -19,7 +19,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -57,6 +59,7 @@ export default defineComponent({
let createExecutivePosition: CreateExecutivePositionViewModel = { let createExecutivePosition: CreateExecutivePositionViewModel = {
position: formData.executivePosition.value, position: formData.executivePosition.value,
}; };
this.status = "loading";
this.createExecutivePosition(createExecutivePosition) this.createExecutivePosition(createExecutivePosition)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -57,6 +59,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]), ...mapActions(useModalStore, ["closeModal"]),
...mapActions(useExecutivePositionStore, ["deleteExecutivePosition"]), ...mapActions(useExecutivePositionStore, ["deleteExecutivePosition"]),
triggerDelete() { triggerDelete() {
this.status = "loading";
this.deleteExecutivePosition(this.data) this.deleteExecutivePosition(this.data)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -19,7 +19,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -57,6 +59,7 @@ export default defineComponent({
let createMembershipStatus: CreateMembershipStatusViewModel = { let createMembershipStatus: CreateMembershipStatusViewModel = {
status: formData.membershipStatus.value, status: formData.membershipStatus.value,
}; };
this.status = "loading";
this.createMembershipStatus(createMembershipStatus) this.createMembershipStatus(createMembershipStatus)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -57,6 +59,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]), ...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMembershipStatusStore, ["deleteMembershipStatus"]), ...mapActions(useMembershipStatusStore, ["deleteMembershipStatus"]),
triggerDelete() { triggerDelete() {
this.status = "loading";
this.deleteMembershipStatus(this.data) this.deleteMembershipStatus(this.data)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -23,7 +23,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -62,6 +64,7 @@ export default defineComponent({
qualification: formData.qualification.value, qualification: formData.qualification.value,
description: formData.description.value, description: formData.description.value,
}; };
this.status = "loading";
this.createQualification(createQualification) this.createQualification(createQualification)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -57,6 +59,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]), ...mapActions(useModalStore, ["closeModal"]),
...mapActions(useQualificationStore, ["deleteQualification"]), ...mapActions(useQualificationStore, ["deleteQualification"]),
triggerDelete() { triggerDelete() {
this.status = "loading";
this.deleteQualification(this.data) this.deleteQualification(this.data)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -19,7 +19,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -62,6 +64,7 @@ export default defineComponent({
title: formData.title.value, title: formData.title.value,
query: this.query ?? "", query: this.query ?? "",
}; };
this.status = "loading";
this.createQueryStore(createAward) this.createQueryStore(createAward)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -56,6 +58,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]), ...mapActions(useModalStore, ["closeModal"]),
...mapActions(useQueryStoreStore, ["deleteQueryStore"]), ...mapActions(useQueryStoreStore, ["deleteQueryStore"]),
triggerDelete() { triggerDelete() {
this.status = "loading";
this.deleteQueryStore(this.data) this.deleteQueryStore(this.data)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -21,7 +21,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -73,6 +75,7 @@ export default defineComponent({
id: this.data, id: this.data,
query: this.query ?? "", query: this.query ?? "",
}; };
this.status = "loading";
this.updateActiveQueryStore(updateQuery) this.updateActiveQueryStore(updateQuery)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -23,7 +23,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -62,6 +64,7 @@ export default defineComponent({
template: formData.template.value, template: formData.template.value,
description: formData.description.value, description: formData.description.value,
}; };
this.status = "loading";
this.createTemplate(createTemplate) this.createTemplate(createTemplate)
.then((res) => { .then((res) => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -57,6 +59,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]), ...mapActions(useModalStore, ["closeModal"]),
...mapActions(useTemplateStore, ["deleteTemplate"]), ...mapActions(useTemplateStore, ["deleteTemplate"]),
triggerDelete() { triggerDelete() {
this.status = "loading";
this.deleteTemplate(this.data) this.deleteTemplate(this.data)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -19,7 +19,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -53,6 +55,7 @@ export default defineComponent({
...mapActions(useRoleStore, ["createRole"]), ...mapActions(useRoleStore, ["createRole"]),
triggerCreateRole(e: any) { triggerCreateRole(e: any) {
let formData = e.target.elements; let formData = e.target.elements;
this.status = "loading";
this.createRole(formData.role.value) this.createRole(formData.role.value)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -56,6 +58,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]), ...mapActions(useModalStore, ["closeModal"]),
...mapActions(useRoleStore, ["deleteRole"]), ...mapActions(useRoleStore, ["deleteRole"]),
triggerDeleteRole() { triggerDeleteRole() {
this.status = "loading";
this.deleteRole(this.data) this.deleteRole(this.data)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

@ -16,7 +16,9 @@
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2"> <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> </div>
</div> </div>
@ -56,6 +58,7 @@ export default defineComponent({
...mapActions(useModalStore, ["closeModal"]), ...mapActions(useModalStore, ["closeModal"]),
...mapActions(useUserStore, ["deleteUser"]), ...mapActions(useUserStore, ["deleteUser"]),
triggerDeleteUser() { triggerDeleteUser() {
this.status = "loading";
this.deleteUser(this.data) this.deleteUser(this.data)
.then(() => { .then(() => {
this.status = { status: "success" }; this.status = { status: "success" };

View file

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

View file

@ -1,5 +1,6 @@
export interface Config { export interface Config {
server_address: string; server_address: string;
app_name_overwrite: string;
imprint_link: string; imprint_link: string;
privacy_link: string; privacy_link: string;
custom_login_message: string; custom_login_message: string;
@ -7,6 +8,7 @@ export interface Config {
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, imprint_link: import.meta.env.VITE_IMPRINT_LINK,
privacy_link: import.meta.env.VITE_PRIVACY_LINK, privacy_link: import.meta.env.VITE_PRIVACY_LINK,
custom_login_message: import.meta.env.VITE_CUSTOM_LOGIN_MESSAGE, 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; const newKey = prefix ? `${prefix}_${key}` : key;
if (Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null)) { if (Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null)) {
console.log(value, newKey);
const arrayResults: Array<{ [key: string]: FieldType }> = []; const arrayResults: Array<{ [key: string]: FieldType }> = [];
value.forEach((item) => { value.forEach((item) => {
const flattenedItems = flatten(item, newKey); const flattenedItems = flatten(item, newKey);
@ -29,7 +28,6 @@ export function flattenQueryResult(result: Array<QueryResult>): Array<{ [key: st
}); });
results = tempResults; results = tempResults;
} else if (value && typeof value === "object" && !Array.isArray(value)) { } else if (value && typeof value === "object" && !Array.isArray(value)) {
console.log(value, newKey);
const objResults = flatten(value as QueryResult, newKey); const objResults = flatten(value as QueryResult, newKey);
const tempResults: Array<{ [key: string]: FieldType }> = []; const tempResults: Array<{ [key: string]: FieldType }> = [];
results.forEach((res) => { results.forEach((res) => {

View file

@ -9,6 +9,7 @@ import type { PermissionType, PermissionSection, PermissionModule } from "@/type
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";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), 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; export default router;
declare module "vue-router" { declare module "vue-router" {

View file

@ -9,8 +9,6 @@ export async function setNewsletterId(to: any, from: any, next: any) {
useNewsletterDatesStore().$reset(); useNewsletterDatesStore().$reset();
useNewsletterRecipientsStore().$reset(); useNewsletterRecipientsStore().$reset();
useNewsletterPrintoutStore().unsubscribePdfPrintingProgress();
useNewsletterPrintoutStore().unsubscribeMailSendingProgress();
useNewsletterPrintoutStore().$reset(); useNewsletterPrintoutStore().$reset();
next(); next();
@ -23,8 +21,6 @@ export async function resetNewsletterStores(to: any, from: any, next: any) {
useNewsletterDatesStore().$reset(); useNewsletterDatesStore().$reset();
useNewsletterRecipientsStore().$reset(); useNewsletterRecipientsStore().$reset();
useNewsletterPrintoutStore().unsubscribePdfPrintingProgress();
useNewsletterPrintoutStore().unsubscribeMailSendingProgress();
useNewsletterPrintoutStore().$reset(); useNewsletterPrintoutStore().$reset();
next(); 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; return config;
}, },
(error) => { (error) => {
@ -53,11 +62,15 @@ http.interceptors.response.use(
.then(() => { .then(() => {
return http(originalRequest); return http(originalRequest);
}) })
.catch(); .catch(() => {});
} }
const notificationStore = useNotificationStore(); 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); 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 { defineStore } from "pinia";
import { http, newEventSource } from "@/serverCom"; import { http, newEventSource, streamingFetch } from "@/serverCom";
import { useNewsletterStore } from "./newsletter"; import { useNewsletterStore } from "./newsletter";
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
import type { EventSourcePolyfill } from "event-source-polyfill"; import type { EventSourcePolyfill } from "event-source-polyfill";
import { useNotificationStore, type NotificationType } from "../../../notification";
export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", { export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
state: () => { state: () => {
@ -12,10 +13,10 @@ export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
printing: undefined as undefined | "loading" | "success" | "failed", printing: undefined as undefined | "loading" | "success" | "failed",
sending: undefined as undefined | "loading" | "success" | "failed", sending: undefined as undefined | "loading" | "success" | "failed",
sendingPreview: undefined as undefined | "loading" | "success" | "failed", sendingPreview: undefined as undefined | "loading" | "success" | "failed",
pdfProgessSource: undefined as undefined | EventSourcePolyfill, pdfSourceMessages: [] as Array<{ kind: string; factor: string; [key: string]: string }>,
mailProgessSource: undefined as undefined | EventSourcePolyfill, mailSourceMessages: [] as Array<{ kind: string; factor: string; [key: string]: string }>,
pdfSourceMessages: [] as Array<Object>, pdfPrintingAbort: undefined as undefined | AbortController,
mailSourceMessages: [] as Array<Object>, mailSendingAbort: undefined as undefined | AbortController,
}; };
}, },
actions: { actions: {
@ -63,6 +64,7 @@ export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
}); });
}, },
createNewsletterPrintout() { createNewsletterPrintout() {
this.subscribePdfPrintingProgress();
this.printing = "loading"; this.printing = "loading";
const newsletterId = useNewsletterStore().activeNewsletter; const newsletterId = useNewsletterStore().activeNewsletter;
if (newsletterId == null) return; if (newsletterId == null) return;
@ -78,10 +80,12 @@ export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
.finally(() => { .finally(() => {
setTimeout(() => { setTimeout(() => {
this.printing = undefined; this.printing = undefined;
this.pdfPrintingAbort?.abort();
}, 1500); }, 1500);
}); });
}, },
createNewsletterSend() { createNewsletterSend() {
this.subscribeMailSendingProgress();
this.sending = "loading"; this.sending = "loading";
const newsletterId = useNewsletterStore().activeNewsletter; const newsletterId = useNewsletterStore().activeNewsletter;
if (newsletterId == null) return; if (newsletterId == null) return;
@ -96,32 +100,55 @@ export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
.finally(() => { .finally(() => {
setTimeout(() => { setTimeout(() => {
this.sending = undefined; this.sending = undefined;
this.mailSendingAbort?.abort();
}, 1500); }, 1500);
}); });
}, },
subscribePdfPrintingProgress() { async subscribePdfPrintingProgress() {
// const newsletterId = useNewsletterStore().activeNewsletter; this.pdfSourceMessages = [];
// if (this.pdfProgessSource != undefined) return; const newsletterId = useNewsletterStore().activeNewsletter;
// this.pdfProgessSource = newEventSource(`/admin/newsletter/${newsletterId}/printoutprogress`); const notificationStore = useNotificationStore();
// this.pdfProgessSource.onmessage = (event) => { this.pdfPrintingAbort = new AbortController();
// console.log("pdf", event); 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() { async subscribeMailSendingProgress() {
// const newsletterId = useNewsletterStore().activeNewsletter; this.mailSourceMessages = [];
// if (this.mailProgessSource != undefined) return; const newsletterId = useNewsletterStore().activeNewsletter;
// this.mailProgessSource = newEventSource(`/admin/newsletter/${newsletterId}/sendprogress`); const notificationStore = useNotificationStore();
// this.mailProgessSource.onmessage = (event) => { this.mailSendingAbort = new AbortController();
// console.log("mail", event); for await (let chunk of streamingFetch(`/admin/newsletter/${newsletterId}/sendprogress`, this.mailSendingAbort)) {
// }; chunk.split("//").forEach((r) => {
}, if (r.trim() != "") {
unsubscribePdfPrintingProgress() { let data = JSON.parse(r);
this.pdfProgessSource?.close(); this.mailSourceMessages.push(data);
this.pdfProgessSource = undefined; let type: NotificationType = "info";
}, let timeout = undefined;
unsubscribeMailSendingProgress() { if (data.factor == "failed") {
this.mailProgessSource?.close(); type = "error";
this.mailProgessSource = undefined; 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"; this.loading = "fetched";
}) })
.catch((err) => { .catch((err) => {
console.log(err);
this.loading = "failed"; this.loading = "failed";
}); });
}, },

View file

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

View file

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

View file

@ -3,7 +3,7 @@
<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-36" /> <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> </div>
<form class="flex flex-col gap-2" @submit.prevent="login"> <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 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"
</script> </script>
<script lang="ts"> <script lang="ts">

View file

@ -1,7 +1,7 @@
<template> <template>
<SidebarLayout> <SidebarLayout>
<template #sidebar> <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> <template v-if="isOwner" #topList>
<RoutingLink <RoutingLink
title="Administration" title="Administration"
@ -38,6 +38,7 @@ 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"
</script> </script>
<script lang="ts"> <script lang="ts">

View file

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

View file

@ -61,6 +61,12 @@
<SuccessCheckmark v-else-if="sendingPreview == 'success'" /> <SuccessCheckmark v-else-if="sendingPreview == 'success'" />
<FailureXMark v-else-if="sendingPreview == 'failed'" /> <FailureXMark v-else-if="sendingPreview == 'failed'" />
</button> </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>
</div> </div>
</template> </template>
@ -84,13 +90,19 @@ export default defineComponent({
newsletterId: String, newsletterId: String,
}, },
computed: { computed: {
...mapState(useNewsletterPrintoutStore, ["printout", "loading", "printing", "sending", "sendingPreview"]), ...mapState(useNewsletterPrintoutStore, [
"printout",
"loading",
"printing",
"sending",
"sendingPreview",
"mailSourceMessages",
"pdfSourceMessages",
]),
...mapState(useAbilityStore, ["can"]), ...mapState(useAbilityStore, ["can"]),
}, },
mounted() { mounted() {
this.fetchNewsletterPrintout(); this.fetchNewsletterPrintout();
this.subscribeMailSendingProgress();
this.subscribePdfPrintingProgress();
}, },
methods: { methods: {
...mapActions(useModalStore, ["openModal"]), ...mapActions(useModalStore, ["openModal"]),
@ -100,8 +112,6 @@ export default defineComponent({
"fetchNewsletterPrintoutById", "fetchNewsletterPrintoutById",
"createNewsletterMailPreview", "createNewsletterMailPreview",
"createNewsletterSend", "createNewsletterSend",
"subscribeMailSendingProgress",
"subscribePdfPrintingProgress",
]), ]),
openPdfShow(filename?: string) { openPdfShow(filename?: string) {
this.openModal( this.openModal(
@ -122,6 +132,20 @@ export default defineComponent({
}) })
.catch(() => {}); .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> </script>

View file

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

View file

@ -3,7 +3,7 @@
<template #sidebar> <template #sidebar>
<SidebarTemplate mainTitle="Dokumentation"> <SidebarTemplate mainTitle="Dokumentation">
<template #list> <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="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="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'" /> <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 vueDevTools from "vite-plugin-vue-devtools";
import Markdown from "unplugin-vue-markdown/vite"; import Markdown from "unplugin-vue-markdown/vite";
import hljs from "highlight.js"; import hljs from "highlight.js";
import { VitePWA } from "vite-plugin-pwa";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ 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: { resolve: {
alias: { alias: {