Compare commits
165 commits
anton/expo
...
main
Author | SHA1 | Date | |
---|---|---|---|
98141d0d58 | |||
4be998074b | |||
97331c9b73 | |||
23ca4bcb29 | |||
20190bdb49 | |||
0492100ef7 | |||
e4c6ae836d | |||
033504b8d8 | |||
4f13b70ac8 | |||
7ded4a21bb | |||
ee42625d66 | |||
f715a4ab9d | |||
ab3e2b9dc4 | |||
924a6bf647 | |||
6e82675557 | |||
1b531b1152 | |||
131b3747de | |||
883559d8a5 | |||
626a355c5c | |||
4ecb39ceff | |||
45ad07a906 | |||
5bcb76a60e | |||
c40b53b200 | |||
c9c6df20e0 | |||
05e464e825 | |||
4dc183f52b | |||
363b5bb541 | |||
c2b495f8a7 | |||
8a85cc054d | |||
afa834739c | |||
8d2e0deee6 | |||
9e50d95d7b | |||
020c7c6cb9 | |||
065b0aa6d5 | |||
cd6c9bfd93 | |||
557ee051ab | |||
0ee5a92c95 | |||
22359c3bea | |||
3da0f4cd49 | |||
a8e2b05d8e | |||
52a35be6c5 | |||
3f3aa040d9 | |||
260478af69 | |||
64806953b2 | |||
059b0fa9f2 | |||
3be63061a8 | |||
6e69e84316 | |||
fbc039c35a | |||
9215ac6fd8 | |||
c83b4670cc | |||
a749bfb45e | |||
2176c6c3cf | |||
f74c10d4aa | |||
fa45559127 | |||
c2dfe35c7f | |||
01b744175f | |||
05f8ebfb1f | |||
b106ea6396 | |||
5050011f29 | |||
18d67fe34e | |||
683d862560 | |||
ecec0f4e6f | |||
f2ace3a2e5 | |||
acdeef47bf | |||
da0408cc4d | |||
dfb5547bd2 | |||
bfa6eb2347 | |||
6154518cbd | |||
79a4594878 | |||
fc33a7dae7 | |||
fcd07a15d0 | |||
faadc71b1b | |||
e9fce9baec | |||
080b6a2b78 | |||
81b7f533c7 | |||
ec477b7d72 | |||
be2bd5e6e3 | |||
9ef171b913 | |||
844bd9a8d5 | |||
cb03dd0b47 | |||
d1e935fe20 | |||
368169f80f | |||
d6743f0d16 | |||
faa691b834 | |||
b3125a26da | |||
2853835d31 | |||
600bb47260 | |||
395a6439eb | |||
78a9d206c3 | |||
467dfd8c1b | |||
0e430d1c9d | |||
f9f32fc519 | |||
361a1c9519 | |||
b1dbb806c6 | |||
354d3b8972 | |||
5ab65ef0c1 | |||
3c4380c91b | |||
ce8157e6bf | |||
604ae30901 | |||
9206a657c1 | |||
a01764e471 | |||
8f4731e08a | |||
761d998edc | |||
ff253c73f0 | |||
1f8349f8d0 | |||
03c0ee58f2 | |||
b611f689b9 | |||
4cf5b989e0 | |||
1b93b28258 | |||
2f60cc51a3 | |||
0508e40494 | |||
8849d3e273 | |||
94b9e7daff | |||
2f13e294d9 | |||
bf3e0cd245 | |||
9f34d821fd | |||
e9a3e0c872 | |||
5bb107e53a | |||
075c598a86 | |||
c219420239 | |||
794de7e3c8 | |||
66026d667d | |||
9a1ff79f66 | |||
03710dfdae | |||
b7b84f8390 | |||
ed50d8e446 | |||
61052805ae | |||
51e9452901 | |||
6717aafd99 | |||
76bb7f8da5 | |||
7d28710157 | |||
847ae14a11 | |||
0fb7a563d0 | |||
8f23a688cb | |||
6f135af4a6 | |||
8c5b4429b1 | |||
d6f28022f0 | |||
967d815784 | |||
08616481e4 | |||
8074dbf5c4 | |||
4148b3191e | |||
747adf7c69 | |||
aa5d50360f | |||
0f6c699f10 | |||
ea7f6e5ec2 | |||
d7596c3f2d | |||
86e36c9c60 | |||
792be3b497 | |||
762a9f2b8e | |||
5c2865a9e5 | |||
28ac0a835b | |||
f3ffdb1ffd | |||
225b686de6 | |||
d62436722a | |||
c0bfc00862 | |||
52deb253c1 | |||
41b300fb72 | |||
8664836a20 | |||
60d6150105 | |||
0d559c9365 | |||
d98afa259e | |||
85289069ba | |||
562f6ab1f2 | |||
e70e6644a6 | |||
c1e9784b4a |
5
.dockerignore
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# NodeJs
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.git/
|
||||||
|
.env
|
|
@ -1 +1,5 @@
|
||||||
SERVER_ADDRESS = serveradress
|
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
|
||||||
|
|
5
.env.production
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
VITE_SERVER_ADDRESS = __SERVERADDRESS__
|
||||||
|
VITE_APP_NAME_OVERWRITE = __APPNAMEOVERWRITE__
|
||||||
|
VITE_IMPRINT_LINK = __IMPRINTLINK__
|
||||||
|
VITE_PRIVACY_LINK = __PRIVACYLINK__
|
||||||
|
VITE_CUSTOM_LOGIN_MESSAGE = __CUSTOMLOGINMESSAGE__
|
25
Dockerfile
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
FROM node:18-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
RUN npm run build-only
|
||||||
|
|
||||||
|
FROM nginx:stable-alpine AS prod
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
COPY ./entrypoint.sh /entrypoint.sh
|
||||||
|
RUN apk add --no-cache dos2unix
|
||||||
|
RUN dos2unix /entrypoint.sh && chmod +x /entrypoint.sh
|
||||||
|
ENTRYPOINT [ "/entrypoint.sh" ]
|
85
README.md
|
@ -1,22 +1,81 @@
|
||||||
# member-administration-ui
|
# FF Admin
|
||||||
|
|
||||||
Memberadministration
|
Administration für Feuerwehren und Vereine.
|
||||||
|
|
||||||
|
## Einleitung
|
||||||
|
|
||||||
|
Dieses Repository dient hauptsächlich zur Verwaltung der Mitgliederdaten, aber auch zur Verwaltung weiterer Daten der Feuerwehr oder eines Vereins. Es ist ein Frontend-Client, der auf die Daten des [ff-admin-server Backends](https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin-server) zugreift. Die Webapp bietet eine Möglichkeit Mitgliederdaten zu verwalten, Protokolle zu schreiben und Kaledereinträge zu erstellen. Benutzer können eingeladen und Rollen zugewiesen werden.
|
||||||
|
|
||||||
|
Eine Demo dieser Seite finden Sie unter [https://admin-demo.ff-admin.de](https://admin-demo.ff-admin.de).
|
||||||
|
|
||||||
|
Für die Verwendung muss ein TOTP-Code eingegeben werden.
|
||||||
|
|
||||||
|
Die Zugangsdaten (Lesebeschränkt) sind:\
|
||||||
|
EMAIL: demo-besucher\
|
||||||
|
TOTP: ![alt text](demo-totp-qrcode.png)\
|
||||||
|
TOTP-Code: FBMDAJKFOYQXM2DNH47GWWBGJ5KWOUCW
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Requirements
|
### Docker Compose Setup
|
||||||
|
|
||||||
1. Access to the internet
|
Um den Container hochzufahren, erstellen Sie eine `docker-compose.yml` Datei mit folgendem Inhalt:
|
||||||
|
|
||||||
### Configuration
|
```yaml
|
||||||
|
version: "3"
|
||||||
|
|
||||||
1. Copy the .env.example file to .env and fill in the required information
|
services:
|
||||||
2. Install all packages via `npm install`
|
ff-admin-app:
|
||||||
3. Start the backend application
|
image: docker.registry.jk-effects.cloud/ehrenamt/ff-admin/app:latest
|
||||||
4. Start the application
|
container_name: ff_admin
|
||||||
5. Run `npm run dev` to run inside dev-environment
|
restart: unless-stopped
|
||||||
|
|
||||||
### Usage
|
#environment:
|
||||||
|
# - 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>/favicon.ico:/usr/share/nginx/html/favicon.ico # 48x48 px Auflösung
|
||||||
|
# - <volume|local path>/favicon.png:/usr/share/nginx/html/favicon.png # 512x512 px Auflösung - wird als pwa Icon genutzt
|
||||||
|
# - <volume|local path>/Logo.png:/usr/share/nginx/html/Logo.png
|
||||||
|
```
|
||||||
|
|
||||||
1. Open the browser and navigate to `http://localhost:5173` or the URL you specified in the server configuration
|
Wenn keine Server-Adresse angegeben wird, wird versucht das Backend unter der URL des Frontends zu erreichen. Dazu muss das Backend auf der gleichen URL wie das Frontend laufen. Zur Unterscheidung von Frontend und Backend bei gleicher URL müssen alle Anfragen mit dem PathPrefix `/api` an das Backend weitergeleitet werden.
|
||||||
2. Go to route `/setup` to create the first user (this path is disabled after the first user is created)
|
|
||||||
|
Führen Sie dann den folgenden Befehl im Verzeichnis der compose-Datei aus, um den Container zu starten:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manuelle Installation
|
||||||
|
|
||||||
|
Klonen Sie dieses Repository und installieren Sie die Abhängigkeiten:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin.git
|
||||||
|
cd ff-admin
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Konfiguration
|
||||||
|
|
||||||
|
Ein eigenes Favicon und Logo kann über das verwenden Volume ausgetauscht werden. Es dürfen jedoch nur einzelne Dateien ausgetauscht werden.
|
||||||
|
|
||||||
|
## Einrichtung
|
||||||
|
|
||||||
|
1. **Admin Benutzer erstellen**: Erstellen Sie einen Admin Benutzer unter dem Pfad /setup, um auf die Migliederverwaltung Zugriff zu erhalten. Nach der Erstellung des ersten Benutzers wird der Pfad automatisch geblockt.
|
||||||
|
|
||||||
|
2. **Rollen und Berechtigungen**: Unter `Benutzer > Rollen` können die Rollen und Berechtigungen für die Benutzer erstellt und angepasst werden.
|
||||||
|
|
||||||
|
3. **Nutzer einladen**: Unter `Benutzer > Benutzer` können weitere Nutzer eingeladen werden. Diese erhalten dann eine E-Mail mit einem Link, um ein TOTP zu erhalten.
|
||||||
|
|
||||||
|
## Fragen und Wünsche
|
||||||
|
|
||||||
|
Bei Fragen, Anregungen oder Wünschen können Sie sich gerne melden.\
|
||||||
|
Wir freuen uns über Ihr Feedback und helfen Ihnen gerne weiter.\
|
||||||
|
Schreiben Sie dafür eine Mail an julian.krauser@jk-effects.com.
|
||||||
|
|
BIN
demo-totp-qrcode.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
26
docs/calendar.md
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# Kalender
|
||||||
|
|
||||||
|
Der Kalender bietet eine Möglichkeit der Organisation von Terminen in unterschiedlichen Kategorien.
|
||||||
|
|
||||||
|
Die Kategorien können in den Einstellungen gesetzt werden. Dabei gibt es folgende Einstellungsmöglichkeiten:
|
||||||
|
- Bezeichnung
|
||||||
|
- Farbe
|
||||||
|
- Standard Kalender Auslieferung
|
||||||
|
- Passphrase
|
||||||
|
|
||||||
|
Die Standard Kalender Auslieferung gibt immer Termine dieser Art in der Öffentlichen Ansicht, als auch in der externen Kalendern an, sofern der Link nicht weiter spezifiziert ist.
|
||||||
|
|
||||||
|
Die Passphrase kann zugangsbeschränkend für Termintypen angewandt werden.
|
||||||
|
|
||||||
|
## öffentlicher Kalender
|
||||||
|
|
||||||
|
Der öffentliche Kalender ist unter dem Pfad `/public/calendar` erreichbar. Dieser Kalender zeigt immer alle Termine an, welche einem Typ mit Standard-Auslieferung zugewiesen sind.
|
||||||
|
|
||||||
|
## WebCal
|
||||||
|
|
||||||
|
Der Kalender kann auch in den Kalender von zum Beispiel Google, Apple und co eingebunden werden, damit die Termine auch direkt im Kalender des Smartphones oder Outlook... verfügbar sind.
|
||||||
|
Hierfür kann der Link konfiguriert werden. Der Link kann dann so eingestellt werden, dass passwort-geschützte oder nicht standard Typen ausgeliefert werden. Zusätzlich können unter diesen Links auch die Standard-Typen hinzugefügt werden.
|
||||||
|
|
||||||
|
Genutzt werden kann das zum Beispiel, dass Vorstands-Interne Termine oder Mitglieds-Spezifische Termine über einen personalisierten Link ausgegeben werden können.
|
||||||
|
|
||||||
|
Wird der erstellte Link in einem Browser geöffnet, sollte automatisch eine ICS-Datei mit den Terminen heruntergeladen werden, oder die Möglichkeit vorgeschlagen werden, den Link in einen Kalender zu integrieren.
|
28
docs/ff-admin.md
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# FF Admin
|
||||||
|
|
||||||
|
## FF Admin ist eine Verwaltungsoberfläche für die Feuerwehr oder andere Vereine:
|
||||||
|
|
||||||
|
FF Admin bietet folgende Module:
|
||||||
|
- Mitgliederverwaltung
|
||||||
|
- Kalender
|
||||||
|
- Newsletter-Versand
|
||||||
|
- Protokolle
|
||||||
|
- Datenabfrage
|
||||||
|
- Templating-Engine
|
||||||
|
- Benutzerverwaltung
|
||||||
|
- Rollenverwaltung
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
-------
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Struktur
|
||||||
|
|
||||||
|
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.
|
51
docs/member.md
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# Mitgliederverwaltung
|
||||||
|
|
||||||
|
Die Mitgliederverwaltung bietet eine Startansicht, in welcher alle Mitglieder durch Pagination angezeigt werden. Die Suche ermöglicht eine Full-Text-Suche nach Vor- und Nachnamen.
|
||||||
|
|
||||||
|
Ist ein Mitglied ausgewählt, lassen sich innerhalb dessen alle Daten zu einem Mitglied verwalten.
|
||||||
|
- Allgemeine Daten des Mitglieds
|
||||||
|
- Mitgliedschaft
|
||||||
|
- Adressen bzw. Kommunikationswege
|
||||||
|
- Auszeichnungen
|
||||||
|
- Qualifikationen
|
||||||
|
- Vereinsämter
|
||||||
|
|
||||||
|
Jedes dieser Verwaltungsmöglichkeiten benötigt vorher eingestellte Werte, welche dann einem Mitglied hinzugefügt werden können.
|
||||||
|
|
||||||
|
## Allgemeine Daten des Mitglieds
|
||||||
|
|
||||||
|
Die allgemeinen Daten des Mitglieds umfassen die interne Id, Anrede, Vorname, Nachname, Nameaffix und das Geburtsdatum. Diese Daten können über den Stift oben rechts im Eck geändert werden.
|
||||||
|
|
||||||
|
Weiterhin zeigt die Übersicht des Mitglieds auch Informationen zu den Einträgen der übrigen Kategorien.
|
||||||
|
|
||||||
|
## Mitgliedschaft
|
||||||
|
|
||||||
|
Die auswählbaren Mitgliedsarten können in den Einstellungen gesetzt werden.
|
||||||
|
|
||||||
|
Im Mitglied können dann Zeiträume einer bestimmten Mitgliedschafts-Art angelegt werden. Wird ein neuer Zeitraum hinzugefügt, wird ein aktuell laufender Zeitraum mit dem Vortag des neuen Startdatums beendet.
|
||||||
|
Weiterhin kann bei manuellem setzen des Enddatums ein Grund angegeben werden.
|
||||||
|
|
||||||
|
## Adressen bzw. Kommunikationswege
|
||||||
|
|
||||||
|
Die auswählbaren Kommunikationsarten können in den Einstellungen erstellt werden. Hierfür muss zu jeder Kommunikationsart ausgewählt werden, welche Felder ausgefüllt werden sollen.
|
||||||
|
|
||||||
|
Im Miglied kann dann bei jedem kommunikationstyp gesetzt werden, ob dieser bevorzugt wird, und ob der Newsletter dorthin versandt werden soll.
|
||||||
|
Ist eine Telefonnummer in der Auswahl enthalten, besteht zusätzlich die Möglichkeit, diesen Kommunikationsweg für den Versand der SMS Alarmierung auszuwählen.
|
||||||
|
|
||||||
|
## Auszeichnungen
|
||||||
|
|
||||||
|
Die auswählbaren Auszeichnungen können in den Einstellungen erstellt werden.
|
||||||
|
|
||||||
|
Im Mitglied können Auszeichnungen mit dem Vergabedatum hinzugefügt werden. Wird eine Annahme verweigert oder Ausgabe verwehrt, kann ein Grund hierfür angegeben werden.
|
||||||
|
|
||||||
|
## Qualifikation
|
||||||
|
|
||||||
|
Die auswählbaren Qualifikationen können in den Einstellungen erstellt werden.
|
||||||
|
|
||||||
|
Im Mitglied können Qualifikationen mit einem Start und Enddatum hinzugefügt werden. Eine Notiz kann auch hinzugefügt werden. Zusätzlich zum Enddatum kann ein Grund für das Ende gesetzt werden.
|
||||||
|
|
||||||
|
## Vereinsämter
|
||||||
|
|
||||||
|
Die auswählbaren Vereinsämter können in den Einstellungen erstellt werden.
|
||||||
|
|
||||||
|
Im Mitglied können Qualifikationen mit einem Start und Enddatum hinzugefügt werden. Eine Notiz kann auch hinzugefügt werden.
|
143
docs/newsletter.md
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
# Newsletter
|
||||||
|
|
||||||
|
Das Newsletter erlaubt den Druck und Versand von Inhalten zum Verein. Zu einem Newsletter können öffentliche Kalendereinträge hinzugefügt werden.
|
||||||
|
|
||||||
|
## Newsletter erstellen
|
||||||
|
|
||||||
|
Ein Newsletter besteht aus Titel und Zusammenfassung, um einen schnelleren Überblick in der Pagination zu erhalten.
|
||||||
|
|
||||||
|
Im Newsletter können Überschrift, Einleitung/Text und Signatur hinzugefügt werden.
|
||||||
|
Es können Daten ausgewählt werden, welche dann automatisch nach dem Text und vor der Signatur im Standard-Template angezeigt.
|
||||||
|
Und es können Empfänger über eine Vordefinierte Datenabfrage oder manuelles hinzufügen festgelegt werden.
|
||||||
|
Im Tab Druck und Versand wird eine Datei mit allen Hinzugefügten Kalendereinträgen angezeigt, wie auch alle erstellten pdfs wie auch eine pdf, die alle anderen pdfs enthält.
|
||||||
|
|
||||||
|
## Versand
|
||||||
|
In den Einstellungen kann festgelegt werden, welcher Kommunikationstyp wie versandt werden soll. Dies wird zusätzlich nochmals vor dem finalen Versand geprüft.
|
||||||
|
|
||||||
|
Es wird beim Druck unterschieden in Ausgaben mit und ohne Adresse. In der Fußzeile wird dann entweder nur der Name oder auch mit Adresse gedruckt.
|
||||||
|
|
||||||
|
Die Auswahl des Typs Mail versendet nur den Hauptteil des pdfs mit einer ics-Datei im Anhang.
|
||||||
|
|
||||||
|
## Template
|
||||||
|
Über die Templating-Engine können für den Newsletter abweichende Kopf- und Fußzeilen und ein abweichender Hauptteil festgelegt werden.
|
||||||
|
|
||||||
|
Ein Newsletter-Template erhält folgende Daten:
|
||||||
|
``` ts
|
||||||
|
// interface:
|
||||||
|
{
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
newsletterTitle: string;
|
||||||
|
newsletterText: string;
|
||||||
|
newsletterSignatur: string;
|
||||||
|
dates: Array<
|
||||||
|
{
|
||||||
|
title: string; // enthält alternativen Titel bzw. Titel des Kalendereintrags
|
||||||
|
content: string; // enthält alternative Beschreibung bzw. Beschreibung des Kalendereintrags
|
||||||
|
starttime: string;
|
||||||
|
endtime: string;
|
||||||
|
location: string;
|
||||||
|
formattedStarttime: string;
|
||||||
|
formattedFullStarttime: string;
|
||||||
|
formattedEndtime: string;
|
||||||
|
formattedFullEndtime: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
recipient: {
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
salutation: Salutation; // (sir | madam | divers | none)
|
||||||
|
nameaffix: string;
|
||||||
|
street: string;
|
||||||
|
streetNumber: string;
|
||||||
|
streetNumberAdd: string
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// beispieldaten
|
||||||
|
|
||||||
|
{
|
||||||
|
title: "Beispiel Newsletter Daten",
|
||||||
|
description: "Zusammenfassung der Demodaten.",
|
||||||
|
newsletterTitle: "<h1>Sehr geehrtes Feuerwehrmitglied</h1>",
|
||||||
|
newsletterText: "<p>zu folgenden Terminen möchten wir recht herzlich zur Teilnahme einladen:</p>",
|
||||||
|
newsletterSignatur: "<p>Mit freundlichen Grüßen</p><p>...</p>",
|
||||||
|
dates: [
|
||||||
|
{
|
||||||
|
title: "Termin 1",
|
||||||
|
content: "<p>Beschreibung eines Termins</p>",
|
||||||
|
starttime: new Date(),
|
||||||
|
formattedStarttime: "Montag 20. Januar",
|
||||||
|
formattedFullStarttime: "Montag 20. Januar um 19:00",
|
||||||
|
endtime: new Date(),
|
||||||
|
formattedEndtime: "Montag 20. Januar",
|
||||||
|
formattedFullEndtime: "Montag 20. Januar um 21:00",
|
||||||
|
location: "Feuerwehrhaus",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
recipient: {
|
||||||
|
firstname: "Julian",
|
||||||
|
lastname: "Krauser",
|
||||||
|
salutation: "sir",
|
||||||
|
nameaffix: "",
|
||||||
|
street: "Straße",
|
||||||
|
streetNumber: "Hausnummer",
|
||||||
|
streetNumberAdd: "Adresszusatz",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Template ist als HTML definiert und beinhaltet Platzhalter, welche durch `handlebarsjs` ausgetauscht werden.
|
||||||
|
``` html
|
||||||
|
<!-- Standard-Template -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Newsletter</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{{newsletterTitle}}}</h1>
|
||||||
|
<p>{{{newsletterText}}}</p>
|
||||||
|
<br />
|
||||||
|
{{#each dates}}
|
||||||
|
<div>
|
||||||
|
<h2><b>{{this.formattedStarttime}}: {{this.title}}</b></h2>
|
||||||
|
<span>{{{this.content}}}</span>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
{{/each}}
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<p>{{{newsletterSignatur}}}</p>
|
||||||
|
</body>
|
||||||
|
<style>
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
p,
|
||||||
|
span,
|
||||||
|
ul,
|
||||||
|
li {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
color: #990b00;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<!--Footer Template-->
|
||||||
|
<div style="font-size: 10pt; width: 100%; margin: 0 20px; padding-top: 5px; color: #888; border-top: 0.5px solid black">
|
||||||
|
{{recipient.lastname}}, {{recipient.firstname}}{{#if recipient.street}},{{/if}} {{recipient.street}}
|
||||||
|
{{recipient.streetNumber}} {{recipient.streetNumberAdd}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
```
|
1
docs/protocol.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Protokoll
|
1
docs/query.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Query Builder & Query Store
|
1
docs/role.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Rollenverwaltung
|
1
docs/templating.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Templating Engine
|
1
docs/user.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Benutzerverwaltung
|
27
entrypoint.sh
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
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 $files
|
||||||
|
do
|
||||||
|
echo "Processing $file ...";
|
||||||
|
for key in $keys
|
||||||
|
do
|
||||||
|
# Get environment variable
|
||||||
|
value=$(eval echo "\$$key")
|
||||||
|
|
||||||
|
# Set default value for APPNAMEOVERWRITE if empty
|
||||||
|
if [ "$key" = "APPNAMEOVERWRITE" ] && [ -z "$value" ]; then
|
||||||
|
value="FF Admin"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "replace $key by $value"
|
||||||
|
|
||||||
|
# replace __[variable_name]__ value with environment variable
|
||||||
|
sed -i 's|__'"$key"'__|'"$value"'|g' $file
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
nginx -g 'daemon off;'
|
|
@ -2,9 +2,8 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg" href="/FW-Wappen.svg" />
|
<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>
|
||||||
|
|
16
nginx.conf
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
worker_processes 4;
|
||||||
|
|
||||||
|
events { worker_connections 1024; }
|
||||||
|
|
||||||
|
http {
|
||||||
|
include mime.types;
|
||||||
|
|
||||||
|
server {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1820
package-lock.json
generated
34
package.json
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "fireportal-ui",
|
"name": "ff-admin",
|
||||||
"version": "0.0.2",
|
"version": "1.2.0",
|
||||||
"description": "Feuerwehr AlarmPortal UI",
|
"description": "Feuerwehr/Verein Mitgliederverwaltung UI",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
@ -12,11 +12,11 @@
|
||||||
"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",
|
||||||
"url": "https://forgejo.jk-effects.cloud/Ehrenamt/fireportal-ui.git"
|
"url": "https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Feuerwehr"
|
"Feuerwehr"
|
||||||
|
@ -24,18 +24,34 @@
|
||||||
"author": "JK Effects",
|
"author": "JK Effects",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fullcalendar/core": "^6.1.15",
|
||||||
|
"@fullcalendar/daygrid": "^6.1.15",
|
||||||
|
"@fullcalendar/interaction": "^6.1.15",
|
||||||
|
"@fullcalendar/timegrid": "^6.1.15",
|
||||||
|
"@fullcalendar/vue3": "^6.1.15",
|
||||||
"@headlessui/vue": "^1.7.13",
|
"@headlessui/vue": "^1.7.13",
|
||||||
"@heroicons/vue": "^2.1.5",
|
"@heroicons/vue": "^2.1.5",
|
||||||
|
"@vueup/vue-quill": "^1.2.0",
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
|
"event-source-polyfill": "^1.0.31",
|
||||||
|
"grapesjs": "^0.22.4",
|
||||||
|
"grapesjs-preset-newsletter": "^1.0.2",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.difference": "^4.5.0",
|
||||||
|
"lodash.differencewith": "^4.5.0",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
|
"markdown-it-anchor": "^9.2.0",
|
||||||
|
"markdown-it-prism": "^2.3.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pdf-dist": "^1.0.0",
|
"pdf-dist": "^1.0.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.3.0",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
"socket.io-client": "^4.5.0",
|
"socket.io-client": "^4.5.0",
|
||||||
|
"unplugin-vue-markdown": "^0.28.0",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"vue": "^3.4.29",
|
"vue": "^3.4.29",
|
||||||
"vue-router": "^4.3.3"
|
"vue-router": "^4.3.3"
|
||||||
|
@ -44,8 +60,12 @@
|
||||||
"@rushstack/eslint-patch": "^1.8.0",
|
"@rushstack/eslint-patch": "^1.8.0",
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/eslint": "~9.6.0",
|
"@types/eslint": "~9.6.0",
|
||||||
|
"@types/event-source-polyfill": "^1.0.5",
|
||||||
"@types/lodash.clonedeep": "^4.5.9",
|
"@types/lodash.clonedeep": "^4.5.9",
|
||||||
|
"@types/lodash.difference": "^4.5.9",
|
||||||
|
"@types/lodash.differencewith": "^4.5.9",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^20.14.5",
|
"@types/node": "^20.14.5",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
|
@ -66,7 +86,7 @@
|
||||||
"typescript": "~5.4.0",
|
"typescript": "~5.4.0",
|
||||||
"vite": "^5.3.1",
|
"vite": "^5.3.1",
|
||||||
"vite-plugin-pwa": "^0.17.4",
|
"vite-plugin-pwa": "^0.17.4",
|
||||||
"vite-plugin-vue-devtools": "^7.3.1",
|
"vite-plugin-vue-devtools": "^7.6.8",
|
||||||
"vue-tsc": "^2.0.21"
|
"vue-tsc": "^2.0.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1921
public/ChartDB(administration-db).json
Normal file
Before Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 516 KiB |
Before Width: | Height: | Size: 295 KiB |
BIN
public/Logo.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
public/administration-db.png
Normal file
After Width: | Height: | Size: 650 KiB |
1
public/administration-db.svg
Normal file
After Width: | Height: | Size: 11 MiB |
BIN
public/calendar.webp
Normal file
After Width: | Height: | Size: 312 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 9.4 KiB |
BIN
public/favicon.png
Normal file
After Width: | Height: | Size: 29 KiB |
|
@ -7,6 +7,7 @@
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
<Footer @contextmenu.prevent />
|
<Footer @contextmenu.prevent />
|
||||||
|
<Notification />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -16,9 +17,10 @@ import Header from "./components/Header.vue";
|
||||||
import Footer from "./components/Footer.vue";
|
import Footer from "./components/Footer.vue";
|
||||||
import { mapState } from "pinia";
|
import { mapState } from "pinia";
|
||||||
import { useAuthStore } from "./stores/auth";
|
import { useAuthStore } from "./stores/auth";
|
||||||
import { isAuthenticatedPromise } from "./router/authGuards";
|
import { isAuthenticatedPromise } from "./router/authGuard";
|
||||||
import ContextMenu from "./components/ContextMenu.vue";
|
import ContextMenu from "./components/ContextMenu.vue";
|
||||||
import Modal from "./components/Modal.vue";
|
import Modal from "./components/Modal.vue";
|
||||||
|
import Notification from "./components/Notification.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -1,15 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<footer
|
<footer
|
||||||
v-if="authCheck && routeName.includes('admin')"
|
v-if="authCheck && (routeName.includes('admin-') || routeName.includes('account-') || routeName.includes('docs-'))"
|
||||||
class="md:hidden flex flex-row h-16 justify-center md:justify-normal p-1 bg-white"
|
class="md:hidden flex flex-row h-16 min-h-16 justify-center md:justify-normal p-1 bg-white"
|
||||||
>
|
>
|
||||||
<div class="w-full flex flex-row gap-2 h-full align-middle">
|
<div class="w-full flex flex-row gap-2 h-full align-middle">
|
||||||
<TopLevelLink v-for="item in topLevel" :key="item.key" :link="item" :disableSubLink="true" />
|
<TopLevelLink
|
||||||
|
v-if="routeName == 'admin' || routeName.includes('admin-')"
|
||||||
|
v-for="item in topLevel"
|
||||||
|
:key="item.key"
|
||||||
|
:link="item"
|
||||||
|
:disableSubLink="true"
|
||||||
|
/>
|
||||||
|
<TopLevelLink
|
||||||
|
v-else-if="routeName == 'account' || routeName.includes('account-') || routeName == 'docs' || routeName.includes('docs-')"
|
||||||
|
:link="{ key: 'club', title: 'Zur Verwaltung', levelDefault: '' }"
|
||||||
|
:disableSubLink="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
import { mapState } from "pinia";
|
import { mapState } from "pinia";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useNavigationStore } from "@/stores/admin/navigation";
|
import { useNavigationStore } from "@/stores/admin/navigation";
|
||||||
|
@ -17,7 +29,6 @@ import TopLevelLink from "./admin/TopLevelLink.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useAuthStore, ["authCheck"]),
|
...mapState(useAuthStore, ["authCheck"]),
|
||||||
|
|
20
src/components/FormBottomBar.vue
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col text-gray-400 text-sm mt-4 items-center">
|
||||||
|
<div class="flex flex-row gap-2 justify-center">
|
||||||
|
<a v-if="config.imprint_link" :href="config.imprint_link" target="_blank">Datenschutz</a>
|
||||||
|
<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>
|
||||||
|
<p>
|
||||||
|
©
|
||||||
|
<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'
|
||||||
|
</script>
|
|
@ -1,12 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<header class="flex flex-row 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="/FFW-Logo.svg" 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">Mitgliederverwaltung</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 && routeName.includes('admin')" 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">
|
||||||
<TopLevelLink v-for="item in topLevel" :key="item.key" :link="item" />
|
<TopLevelLink
|
||||||
|
v-if="routeName == 'admin' || routeName.includes('admin-')"
|
||||||
|
v-for="item in topLevel"
|
||||||
|
:key="item.key"
|
||||||
|
:link="item"
|
||||||
|
/>
|
||||||
|
<TopLevelLink
|
||||||
|
v-else-if="routeName == 'account' || routeName.includes('account-') || routeName == 'docs' || routeName.includes('docs-')"
|
||||||
|
:link="{ key: 'club', title: 'Zur Verwaltung', levelDefault: '' }"
|
||||||
|
:disable-sub-link="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<UserMenu v-if="authCheck" />
|
<UserMenu v-if="authCheck" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,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">
|
||||||
|
|
130
src/components/Notification.vue
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="fixed right-0 flex flex-col gap-4 p-2 w-full md:w-80 z-50"
|
||||||
|
:class="position == 'bottom' ? 'bottom-0' : 'top-0'"
|
||||||
|
>
|
||||||
|
<TransitionGroup
|
||||||
|
:enter-active-class="notifications.length > 1 ? [props.enter, props.moveDelay].join(' ') : props.enter"
|
||||||
|
:enter-from-class="props.enterFrom"
|
||||||
|
:enter-to-class="props.enterTo"
|
||||||
|
:leave-active-class="props.leave"
|
||||||
|
:leave-from-class="props.leaveFrom"
|
||||||
|
:leave-to-class="props.leaveTo"
|
||||||
|
:move-class="props.move"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="notification in sortedNotifications"
|
||||||
|
:key="notification.id"
|
||||||
|
class="relative p-2 bg-white flex flex-row gap-2 w-full overflow-hidden rounded-lg shadow-md"
|
||||||
|
:class="[
|
||||||
|
notification.type == 'error' ? 'border border-red-400' : '',
|
||||||
|
notification.type == 'warning' ? 'border border-red-400' : '',
|
||||||
|
notification.type == 'info' ? 'border border-gray-400' : '',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- @mouseover="hovering(notification.id, true)"
|
||||||
|
@mouseleave="hovering(notification.id, false)" -->
|
||||||
|
<ExclamationCircleIcon
|
||||||
|
v-if="notification.type == 'error'"
|
||||||
|
class="flex items-center justify-center min-w-12 w-12 h-12 bg-red-500 rounded-lg text-white p-1"
|
||||||
|
/>
|
||||||
|
<ExclamationTriangleIcon
|
||||||
|
v-if="notification.type == 'warning'"
|
||||||
|
class="flex items-center justify-center min-w-12 w-12 h-12 bg-red-500 rounded-lg text-white p-1"
|
||||||
|
/>
|
||||||
|
<InformationCircleIcon
|
||||||
|
v-if="notification.type == 'info'"
|
||||||
|
class="flex items-center justify-center min-w-12 w-12 h-12 bg-gray-500 rounded-lg text-white p-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span
|
||||||
|
class="font-semibold"
|
||||||
|
:class="[
|
||||||
|
notification.type == 'error' ? 'text-red-500' : '',
|
||||||
|
notification.type == 'warning' ? 'text-red-500' : '',
|
||||||
|
notification.type == 'info' ? 'text-gray-700' : '',
|
||||||
|
]"
|
||||||
|
>{{ notification.title }}</span
|
||||||
|
>
|
||||||
|
<p class="text-sm text-gray-600">{{ notification.text }}</p>
|
||||||
|
</div>
|
||||||
|
<XMarkIcon
|
||||||
|
@click="close(notification.id)"
|
||||||
|
class="absolute top-2 right-2 w-6 h-6 cursor-pointer text-gray-500"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute left-0 bottom-0 h-1 bg-gray-500 transition-[width] duration-[4900ms] ease-linear"
|
||||||
|
:class="notification.indicator ? 'w-0' : 'w-full'"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent, TransitionGroup } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
import {
|
||||||
|
ExclamationTriangleIcon,
|
||||||
|
ExclamationCircleIcon,
|
||||||
|
InformationCircleIcon,
|
||||||
|
XMarkIcon,
|
||||||
|
} from "@heroicons/vue/24/outline";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
maxNotifications?: number;
|
||||||
|
enter?: string;
|
||||||
|
enterFrom?: string;
|
||||||
|
enterTo?: string;
|
||||||
|
leave?: string;
|
||||||
|
leaveFrom?: string;
|
||||||
|
leaveTo?: string;
|
||||||
|
move?: string;
|
||||||
|
moveDelay?: string;
|
||||||
|
position?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
maxNotifications: 10,
|
||||||
|
enter: "transform ease-out duration-300 transition",
|
||||||
|
enterFrom: "translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-4",
|
||||||
|
enterTo: "translate-y-0 opacity-100 sm:translate-x-0",
|
||||||
|
leave: "transition ease-in duration-500",
|
||||||
|
leaveFrom: "opacity-100",
|
||||||
|
leaveTo: "opacity-0",
|
||||||
|
move: "transition duration-500",
|
||||||
|
moveDelay: "delay-300",
|
||||||
|
position: "bottom",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
computed: {
|
||||||
|
...mapState(useNotificationStore, ["notifications", "timeouts"]),
|
||||||
|
sortedNotifications() {
|
||||||
|
if (this.position === "bottom") {
|
||||||
|
return [...this.notifications];
|
||||||
|
}
|
||||||
|
return [...this.notifications].reverse();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useNotificationStore, ["revoke"]),
|
||||||
|
close(id: string) {
|
||||||
|
this.revoke(id);
|
||||||
|
},
|
||||||
|
hovering(id: string, value: boolean, timeout?: number) {
|
||||||
|
if (value) {
|
||||||
|
clearTimeout(this.timeouts[id]);
|
||||||
|
} else {
|
||||||
|
this.timeouts[id] = setTimeout(() => {
|
||||||
|
this.revoke(id);
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
263
src/components/Pagination.vue
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
<template>
|
||||||
|
<div class="grow flex flex-col gap-2 overflow-hidden">
|
||||||
|
<div v-if="useSearch" class="relative self-end flex flex-row items-center gap-2">
|
||||||
|
<Spinner v-if="deferingSearch" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="!max-w-64 !w-64 rounded-md shadow-sm relative block px-3 py-2 pr-5 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="Suche"
|
||||||
|
v-model="searchString"
|
||||||
|
/>
|
||||||
|
<XMarkIcon
|
||||||
|
class="absolute h-4 stroke-2 right-2 top-1/2 -translate-y-1/2 cursor-pointer z-10"
|
||||||
|
@click="searchString = ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col w-full grow gap-2 pr-2 overflow-y-scroll">
|
||||||
|
<div v-if="indicateLoading" class="flex flex-row justify-center items-center w-full p-1">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
<p v-if="visibleRows.length == 0" class="flex flex-row w-full gap-2 p-1">Kein Inhalt</p>
|
||||||
|
<slot
|
||||||
|
v-else
|
||||||
|
name="pageRow"
|
||||||
|
v-for="(item, index) in visibleRows"
|
||||||
|
:key="index"
|
||||||
|
:row="item"
|
||||||
|
@click="$emit('clickRow', item)"
|
||||||
|
>
|
||||||
|
<p>{{ item }}</p>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row w-full justify-between select-none">
|
||||||
|
<p class="text-sm font-normal text-gray-500">
|
||||||
|
Elemente <span class="font-semibold text-gray-900">{{ showingText }}</span> von
|
||||||
|
<span class="font-semibold text-gray-900">{{ entryCount }}</span>
|
||||||
|
</p>
|
||||||
|
<ul class="flex flex-row text-sm h-8">
|
||||||
|
<li
|
||||||
|
class="flex h-8 w-8 items-center justify-center text-gray-500 bg-white border border-gray-300 first:rounded-s-lg last:rounded-e-lg"
|
||||||
|
:class="[currentPage > 0 ? 'cursor-pointer hover:bg-gray-100 hover:text-gray-700' : 'opacity-50']"
|
||||||
|
@click="loadPage(currentPage - 1)"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon class="h-4" />
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-for="page in displayedPagesNumbers"
|
||||||
|
:key="page"
|
||||||
|
class="flex h-8 w-8 items-center justify-center text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 first:rounded-s-lg last:rounded-e-lg"
|
||||||
|
:class="[currentPage == page ? 'font-bold border-primary' : '', page != '.' ? ' cursor-pointer' : '']"
|
||||||
|
@click="loadPage(page)"
|
||||||
|
>
|
||||||
|
{{ typeof page == "number" ? page + 1 : "..." }}
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="flex h-8 w-8 items-center justify-center text-gray-500 bg-white border border-gray-300 first:rounded-s-lg last:rounded-e-lg"
|
||||||
|
:class="[
|
||||||
|
currentPage + 1 < countOfPages ? 'cursor-pointer hover:bg-gray-100 hover:text-gray-700' : 'opacity-50',
|
||||||
|
]"
|
||||||
|
@click="loadPage(currentPage + 1)"
|
||||||
|
>
|
||||||
|
<ChevronRightIcon class="h-4" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" generic="T extends { id: FieldType }">
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { ChevronRightIcon, ChevronLeftIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
||||||
|
import Spinner from "./Spinner.vue";
|
||||||
|
import type { FieldType } from "@/types/dynamicQueries";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
items: { type: Array<T>, default: [] },
|
||||||
|
maxEntriesPerPage: { type: Number, default: 25 },
|
||||||
|
totalCount: { type: Number, default: null },
|
||||||
|
config: { type: Array<{ key: string }>, default: [] },
|
||||||
|
useSearch: { type: Boolean, default: false },
|
||||||
|
enablePreSearch: { type: Boolean, default: false },
|
||||||
|
indicateLoading: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const slots = defineSlots<{
|
||||||
|
pageRow(props: { row: T }): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const timer = ref(undefined) as undefined | any;
|
||||||
|
const currentPage = ref(0);
|
||||||
|
const searchString = ref("");
|
||||||
|
const deferingSearch = ref(false)
|
||||||
|
|
||||||
|
watch(searchString, async () => {
|
||||||
|
deferingSearch.value = true
|
||||||
|
clearTimeout(timer.value);
|
||||||
|
timer.value = setTimeout(() => {
|
||||||
|
currentPage.value = 0;
|
||||||
|
deferingSearch.value = false
|
||||||
|
emit("search", searchString.value);
|
||||||
|
}, 600);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.totalCount, async () => {
|
||||||
|
currentPage.value = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits({
|
||||||
|
submit(id: number) {
|
||||||
|
return typeof id == "number";
|
||||||
|
},
|
||||||
|
loadData(offset: number, count: number, searchString: string) {
|
||||||
|
return typeof offset == "number" && typeof offset == "number" && typeof searchString == "number";
|
||||||
|
},
|
||||||
|
search(search: string) {
|
||||||
|
return typeof search == "string";
|
||||||
|
},
|
||||||
|
clickRow(elem: T) {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const entryCount = computed(() => props.totalCount ?? props.items.length);
|
||||||
|
const showingStart = computed(() => currentPage.value * props.maxEntriesPerPage);
|
||||||
|
const showingEnd = computed(() => {
|
||||||
|
let max = currentPage.value * props.maxEntriesPerPage + props.maxEntriesPerPage;
|
||||||
|
if (max > entryCount.value) max = entryCount.value;
|
||||||
|
return max;
|
||||||
|
});
|
||||||
|
const showingText = computed(() => `${entryCount.value != 0 ? showingStart.value + 1 : 0} - ${showingEnd.value}`);
|
||||||
|
const countOfPages = computed(() => Math.ceil(entryCount.value / props.maxEntriesPerPage));
|
||||||
|
const displayedPagesNumbers = computed(() => {
|
||||||
|
let stateOfPush = false;
|
||||||
|
|
||||||
|
return [...new Array(countOfPages.value)].reduce((acc, curr, index) => {
|
||||||
|
if (
|
||||||
|
index <= 1 ||
|
||||||
|
index >= countOfPages.value - 2 ||
|
||||||
|
(currentPage.value - 1 <= index && index <= currentPage.value + 1)
|
||||||
|
) {
|
||||||
|
acc.push(index);
|
||||||
|
stateOfPush = false;
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
if (stateOfPush == true) return acc;
|
||||||
|
acc.push(".");
|
||||||
|
stateOfPush = true;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
});
|
||||||
|
const visibleRows = computed(() => filterData(props.items, searchString.value, showingStart.value, showingEnd.value));
|
||||||
|
|
||||||
|
const loadPage = (newPage: number | ".") => {
|
||||||
|
if (newPage == ".") return;
|
||||||
|
if (newPage < 0 || newPage >= countOfPages.value) return;
|
||||||
|
|
||||||
|
let pageStart = newPage * props.maxEntriesPerPage;
|
||||||
|
let pageEnd = newPage * props.maxEntriesPerPage + props.maxEntriesPerPage;
|
||||||
|
if (pageEnd > entryCount.value) pageEnd = entryCount.value;
|
||||||
|
|
||||||
|
let loadedElementCount = filterData(props.items, searchString.value, pageStart, pageEnd).length;
|
||||||
|
|
||||||
|
if (loadedElementCount < props.maxEntriesPerPage && (pageEnd != props.totalCount || loadedElementCount == 0))
|
||||||
|
emit("loadData", pageStart, props.maxEntriesPerPage, searchString.value);
|
||||||
|
|
||||||
|
currentPage.value = newPage;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterData = (array: Array<any>, searchString: string, start: number, end: number): Array<any> => {
|
||||||
|
return array
|
||||||
|
.filter(
|
||||||
|
(elem) =>
|
||||||
|
!props.enablePreSearch ||
|
||||||
|
searchString.trim() == "" ||
|
||||||
|
props.config.some(
|
||||||
|
(col) =>
|
||||||
|
typeof elem?.[col.key] == "string" &&
|
||||||
|
elem[col.key].toLowerCase().includes(searchString.trim().toLowerCase())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter((elem, index) => (elem?.tab_pos ?? index) >= start && (elem?.tab_pos ?? index) < end);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
computed: {
|
||||||
|
entryCount() {
|
||||||
|
return this.totalCount ?? this.items.length;
|
||||||
|
},
|
||||||
|
showingStart() {
|
||||||
|
return this.currentPage * this.maxEntriesPerPage;
|
||||||
|
},
|
||||||
|
showingEnd() {
|
||||||
|
let max = this.currentPage * this.maxEntriesPerPage + this.maxEntriesPerPage;
|
||||||
|
if (max > this.entryCount) max = this.entryCount;
|
||||||
|
return max;
|
||||||
|
},
|
||||||
|
showingText() {
|
||||||
|
return `${this.entryCount != 0 ? this.showingStart + 1 : 0} - ${this.showingEnd}`;
|
||||||
|
},
|
||||||
|
countOfPages() {
|
||||||
|
return Math.ceil(this.entryCount / this.maxEntriesPerPage);
|
||||||
|
},
|
||||||
|
displayedPagesNumbers(): Array<number | "."> {
|
||||||
|
//indicate if "." or page number gets pushed
|
||||||
|
let stateOfPush = false;
|
||||||
|
|
||||||
|
return [...new Array(this.countOfPages)].reduce((acc, curr, index) => {
|
||||||
|
if (
|
||||||
|
// always display first 2 pages
|
||||||
|
index <= 1 ||
|
||||||
|
// always display last 2 pages
|
||||||
|
index >= this.countOfPages - 2 ||
|
||||||
|
// always display 1 pages around current page
|
||||||
|
(this.currentPage - 1 <= index && index <= this.currentPage + 1)
|
||||||
|
) {
|
||||||
|
acc.push(index);
|
||||||
|
stateOfPush = false;
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
// abort if placeholder already added to array
|
||||||
|
if (stateOfPush == true) return acc;
|
||||||
|
// show placeholder if pagenumber is not actively rendered
|
||||||
|
acc.push(".");
|
||||||
|
stateOfPush = true;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
},
|
||||||
|
visibleRows() {
|
||||||
|
return this.filterData(this.items, this.searchString, this.showingStart, this.showingEnd);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadPage(newPage: number | ".") {
|
||||||
|
if (newPage == ".") return;
|
||||||
|
if (newPage < 0 || newPage >= this.countOfPages) return;
|
||||||
|
|
||||||
|
let pageStart = newPage * this.maxEntriesPerPage;
|
||||||
|
let pageEnd = newPage * this.maxEntriesPerPage + this.maxEntriesPerPage;
|
||||||
|
if (pageEnd > this.entryCount) pageEnd = this.entryCount;
|
||||||
|
|
||||||
|
let loadedElementCount = this.filterData(this.items, this.searchString, pageStart, pageEnd).length;
|
||||||
|
if (loadedElementCount < this.maxEntriesPerPage)
|
||||||
|
this.$emit("loadData", { offset: pageStart, count: this.maxEntriesPerPage, search: this.searchString });
|
||||||
|
|
||||||
|
this.currentPage = newPage;
|
||||||
|
},
|
||||||
|
filterData(array: Array<any>, searchString: string, start: number, end: number): Array<any> {
|
||||||
|
return array
|
||||||
|
.filter(
|
||||||
|
(elem) =>
|
||||||
|
!this.enablePreSearch ||
|
||||||
|
searchString.trim() == "" ||
|
||||||
|
this.config.some((col) => typeof elem?.[col.key] == "string" && elem[col.key].includes(searchString.trim()))
|
||||||
|
)
|
||||||
|
.filter((elem, index) => (elem?.tab_pos ?? index) >= start && (elem?.tab_pos ?? index) < end);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script> -->
|
44
src/components/TextCopy.vue
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex relative">
|
||||||
|
<input type="text" :value="copyText" />
|
||||||
|
<ClipboardIcon
|
||||||
|
class="w-5 h-5 p-2 box-content absolute right-1 top-1/2 -translate-y-1/2 bg-white cursor-pointer"
|
||||||
|
@click="copyToClipboard"
|
||||||
|
/>
|
||||||
|
<div v-if="copySuccess" class="absolute w-5 h-5 right-3 top-[10px]">
|
||||||
|
<SuccessCheckmark />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import { ClipboardIcon } from "@heroicons/vue/24/outline";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
copyText: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
timeoutCopy: undefined as any,
|
||||||
|
copySuccess: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
copyToClipboard() {
|
||||||
|
navigator.clipboard.writeText(this.copyText ?? "");
|
||||||
|
this.copySuccess = true;
|
||||||
|
this.timeoutCopy = setTimeout(() => {
|
||||||
|
this.copySuccess = false;
|
||||||
|
}, 2000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -13,7 +13,7 @@
|
||||||
leave-to-class="transform scale-95 opacity-0"
|
leave-to-class="transform scale-95 opacity-0"
|
||||||
>
|
>
|
||||||
<MenuItems
|
<MenuItems
|
||||||
class="absolute right-0 mt-2 w-56 z-10 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
class="absolute right-0 mt-2 w-56 z-20 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
>
|
>
|
||||||
<div class="px-3 py-1 pt-2">
|
<div class="px-3 py-1 pt-2">
|
||||||
<p class="text-xs">Angemeldet als</p>
|
<p class="text-xs">Angemeldet als</p>
|
||||||
|
@ -21,12 +21,19 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="px-1 py-1 w-full flex flex-col gap-2">
|
<div class="px-1 py-1 w-full flex flex-col gap-2">
|
||||||
<MenuItem v-slot="{ close }">
|
<MenuItem v-slot="{ close }">
|
||||||
<RouterLink to="/account">
|
<RouterLink to="/account/me">
|
||||||
<button button primary @click="close">Mein Account</button>
|
<button button primary @click="close">Mein Account</button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem v-slot="{ close }">
|
||||||
|
<RouterLink to="/docs" target="_blank">
|
||||||
|
<button button primary @click="close">Dokumentation</button>
|
||||||
|
</RouterLink>
|
||||||
|
</MenuItem>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<button primary-outline @click="logoutAccount">ausloggen</button>
|
<span>
|
||||||
|
<button primary-outline @click="logoutAccount">ausloggen</button>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</MenuItems>
|
</MenuItems>
|
||||||
|
|
182
src/components/admin/MemberSearchSelect.vue
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<Combobox v-model="selected" :disabled="disabled" multiple>
|
||||||
|
<ComboboxLabel>{{ title }}</ComboboxLabel>
|
||||||
|
<div class="relative mt-1">
|
||||||
|
<ComboboxInput
|
||||||
|
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
|
@input="query = $event.target.value"
|
||||||
|
/>
|
||||||
|
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
|
</ComboboxButton>
|
||||||
|
<TransitionRoot
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
@after-leave="query = ''"
|
||||||
|
>
|
||||||
|
<ComboboxOptions
|
||||||
|
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-none sm:text-sm"
|
||||||
|
>
|
||||||
|
<ComboboxOption v-if="loading || deferingSearch" as="template" disabled>
|
||||||
|
<li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4">
|
||||||
|
<Spinner />
|
||||||
|
<span class="font-normal block truncate">suche</span>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
<ComboboxOption v-else-if="filtered.length === 0 && query == ''" as="template" disabled>
|
||||||
|
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
|
||||||
|
<span class="font-normal block truncate">tippe, um zu suchen...</span>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
<ComboboxOption v-else-if="filtered.length === 0" as="template" disabled>
|
||||||
|
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
|
||||||
|
<span class="font-normal block truncate">Keine Auswahl gefunden.</span>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
|
||||||
|
<ComboboxOption
|
||||||
|
v-if="!(loading || deferingSearch)"
|
||||||
|
v-for="member in filtered"
|
||||||
|
as="template"
|
||||||
|
:key="member.id"
|
||||||
|
:value="member.id"
|
||||||
|
v-slot="{ selected, active }"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="relative cursor-default select-none py-2 pl-10 pr-4"
|
||||||
|
:class="{
|
||||||
|
'bg-primary text-white': active,
|
||||||
|
'text-gray-900': !active,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
|
||||||
|
{{ member.firstname }} {{ member.lastname }} {{ member.nameaffix }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="selected"
|
||||||
|
class="absolute inset-y-0 left-0 flex items-center pl-3"
|
||||||
|
:class="{ 'text-white': active, 'text-primary': !active }"
|
||||||
|
>
|
||||||
|
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
</ComboboxOptions>
|
||||||
|
</TransitionRoot>
|
||||||
|
</div>
|
||||||
|
</Combobox>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import {
|
||||||
|
Combobox,
|
||||||
|
ComboboxLabel,
|
||||||
|
ComboboxInput,
|
||||||
|
ComboboxButton,
|
||||||
|
ComboboxOptions,
|
||||||
|
ComboboxOption,
|
||||||
|
TransitionRoot,
|
||||||
|
} from "@headlessui/vue";
|
||||||
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
|
import { useMemberStore } from "@/stores/admin/club/member/member";
|
||||||
|
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||||
|
import difference from "lodash.difference";
|
||||||
|
import Spinner from "../Spinner.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Array as PropType<Array<number>>,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
title: String,
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["update:model-value", "add:difference", "remove:difference", "add:member", "add:memberByArray"],
|
||||||
|
watch: {
|
||||||
|
modelValue() {
|
||||||
|
if (this.initialLoaded) return;
|
||||||
|
this.initialLoaded = true;
|
||||||
|
this.loadMembersInitial();
|
||||||
|
},
|
||||||
|
query() {
|
||||||
|
this.deferingSearch = true;
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
this.deferingSearch = false;
|
||||||
|
this.search();
|
||||||
|
}, 600);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
initialLoaded: false as boolean,
|
||||||
|
loading: false as boolean,
|
||||||
|
deferingSearch: false as boolean,
|
||||||
|
timer: undefined as any,
|
||||||
|
query: "" as string,
|
||||||
|
filtered: [] as Array<MemberViewModel>,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selected: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(val: Array<number>) {
|
||||||
|
this.$emit("update:model-value", val);
|
||||||
|
if (this.modelValue.length < val.length) {
|
||||||
|
let diff = difference(val, this.modelValue);
|
||||||
|
if (diff.length != 1) return;
|
||||||
|
this.$emit("add:difference", diff[0]);
|
||||||
|
this.$emit("add:member", this.getMemberFromSearch(diff[0]));
|
||||||
|
} else {
|
||||||
|
let diff = difference(this.modelValue, val);
|
||||||
|
if (diff.length != 1) return;
|
||||||
|
this.$emit("remove:difference", diff[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadMembersInitial();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useMemberStore, ["searchMembers", "getMembersByIds"]),
|
||||||
|
search() {
|
||||||
|
this.filtered = [];
|
||||||
|
if (this.query == "") return;
|
||||||
|
this.loading = true;
|
||||||
|
this.searchMembers(this.query)
|
||||||
|
.then((res) => {
|
||||||
|
this.filtered = res.data;
|
||||||
|
})
|
||||||
|
.catch((err) => {})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getMemberFromSearch(id: number) {
|
||||||
|
return this.filtered.find((f) => f.id == id);
|
||||||
|
},
|
||||||
|
loadMembersInitial() {
|
||||||
|
if (this.modelValue.length == 0) return;
|
||||||
|
this.getMembersByIds(this.modelValue)
|
||||||
|
.then((res) => {
|
||||||
|
this.$emit("add:memberByArray", res.data);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2 max-w-2xl mx-auto w-full select-none">
|
<div
|
||||||
|
class="flex flex-col gap-2 max-w-2xl mx-auto w-full select-none"
|
||||||
|
:class="disableEdit ? ' pointer-events-none opacity-60 bg-gray-100/50' : ''"
|
||||||
|
>
|
||||||
<div class="flex flex-row gap-2 h-fit w-full border border-gray-300 rounded-md p-2">
|
<div class="flex flex-row gap-2 h-fit w-full border border-gray-300 rounded-md p-2">
|
||||||
<input type="checkbox" name="admin" id="admin" class="cursor-pointer" :checked="isAdmin" @change="toggleAdmin" />
|
<input type="checkbox" name="admin" id="admin" class="cursor-pointer" :checked="isAdmin" @change="toggleAdmin" />
|
||||||
<label for="admin" class="cursor-pointer">Administratorrecht</label>
|
<label for="admin" class="cursor-pointer">Administratorrecht</label>
|
||||||
|
@ -8,7 +11,7 @@
|
||||||
v-for="section in sections"
|
v-for="section in sections"
|
||||||
:key="section"
|
:key="section"
|
||||||
class="flex flex-col gap-2 h-fit w-full border border-primary rounded-md"
|
class="flex flex-col gap-2 h-fit w-full border border-primary rounded-md"
|
||||||
:class="isAdmin ? ' pointer-events-none opacity-60 bg-gray-100' : ''"
|
:class="isAdmin && !disableEdit ? ' pointer-events-none opacity-60 bg-gray-100' : ''"
|
||||||
>
|
>
|
||||||
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
||||||
<p>Abschnitt: {{ section }}</p>
|
<p>Abschnitt: {{ section }}</p>
|
||||||
|
@ -65,7 +68,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row gap-2 self-end pt-4">
|
<div v-if="!disableEdit" class="flex flex-row gap-2 self-end pt-4">
|
||||||
<button primary-outline class="!w-fit" @click="reset" :disabled="canSaveOrReset">verwerfen</button>
|
<button primary-outline class="!w-fit" @click="reset" :disabled="canSaveOrReset">verwerfen</button>
|
||||||
<button primary class="!w-fit" @click="submit" :disabled="status == 'loading' || canSaveOrReset">
|
<button primary class="!w-fit" @click="submit" :disabled="status == 'loading' || canSaveOrReset">
|
||||||
speichern
|
speichern
|
||||||
|
@ -92,7 +95,7 @@ import { mapState, mapActions } from "pinia";
|
||||||
import { EyeIcon, PencilIcon, PlusIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
import { EyeIcon, PencilIcon, PlusIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
import { useAbilityStore } from "@/stores/ability";
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
import isEqual from "lodash.isEqual";
|
import isEqual from "lodash.isequal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
@ -109,6 +112,10 @@ export default defineComponent({
|
||||||
type: [Object, String, null] as PropType<null | "loading" | { status: "success" | "failed"; message?: string }>,
|
type: [Object, String, null] as PropType<null | "loading" | { status: "success" | "failed"; message?: string }>,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
disableEdit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
permissions() {
|
permissions() {
|
||||||
|
|
|
@ -1,35 +1,36 @@
|
||||||
<template>
|
<template>
|
||||||
<RouterLink v-if="link" :to="{ name: `admin-${activeNavigation}-${link.key}` }">
|
<RouterLink v-if="link" :to="link">
|
||||||
<p
|
<p
|
||||||
class="cursor-pointer w-full px-2 py-3"
|
class="cursor-pointer w-full px-2 py-3"
|
||||||
:class="
|
:class="active ? 'rounded-r-lg bg-red-200 border-l-4 border-l-primary' : 'pl-3 hover:bg-red-200 rounded-lg'"
|
||||||
activeLink == link.key
|
|
||||||
? 'rounded-r-lg bg-red-200 border-l-4 border-l-primary'
|
|
||||||
: 'pl-3 hover:bg-red-200 rounded-lg'
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
{{ link.title }}
|
{{ title }}
|
||||||
</p>
|
</p>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
import { RouterLink } from "vue-router";
|
||||||
import { mapState, mapActions } from "pinia";
|
import { mapState, mapActions } from "pinia";
|
||||||
import { useNavigationStore, type navigationLinkModel } from "@/stores/admin/navigation";
|
import { useNavigationStore, type navigationLinkModel } from "@/stores/admin/navigation";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
import { RouterLink } from "vue-router";
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
link: {
|
title: {
|
||||||
type: Object as PropType<navigationLinkModel>,
|
type: String,
|
||||||
default: null,
|
default: "LINK",
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
type: Object as PropType<string | { name: string, params?:{[key:string]:string} }>,
|
||||||
|
default: "/",
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState(useNavigationStore, ["activeLink", "activeNavigation"]),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -18,13 +18,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
import { RouterLink } from "vue-router";
|
||||||
import { useNavigationStore, type topLevelNavigationModel } from "@/stores/admin/navigation";
|
import { useNavigationStore, type topLevelNavigationModel } from "@/stores/admin/navigation";
|
||||||
import { mapState } from "pinia";
|
import { mapState } from "pinia";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
import { RouterLink } from "vue-router";
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
link: {
|
link: {
|
||||||
|
|
147
src/components/admin/club/calendar/CalendarLinkModal.vue
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Kalenderlink</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div>
|
||||||
|
<Listbox v-model="selectedTypes" name="type" multiple>
|
||||||
|
<ListboxLabel>Typen zur Anzeige auswählen</ListboxLabel>
|
||||||
|
<div class="relative mt-1">
|
||||||
|
<ListboxButton
|
||||||
|
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
|
>
|
||||||
|
<span class="block truncate w-full text-start">
|
||||||
|
{{
|
||||||
|
selectedTypes.length != 0
|
||||||
|
? selectedTypes?.map((t) => t.type).join(", ")
|
||||||
|
: "Standard-Typen werden ausgeliefert"
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</ListboxButton>
|
||||||
|
|
||||||
|
<transition
|
||||||
|
leave-active-class="transition duration-100 ease-in"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<ListboxOptions
|
||||||
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<ListboxOption v-if="calendarTypes.length == 0" disabled as="template">
|
||||||
|
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||||
|
<span :class="['font-normal', 'block truncate']">keine Auswahl vorhanden</span>
|
||||||
|
</li>
|
||||||
|
</ListboxOption>
|
||||||
|
<ListboxOption
|
||||||
|
v-slot="{ active, selected }"
|
||||||
|
v-for="type in calendarTypes"
|
||||||
|
:key="type.id"
|
||||||
|
:value="type"
|
||||||
|
as="template"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
:class="[
|
||||||
|
active ? 'bg-red-200 text-amber-900' : 'text-gray-900',
|
||||||
|
'relative cursor-default select-none py-2 pl-10 pr-4',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span :class="[selected ? 'font-medium' : 'font-normal', 'block truncate']">
|
||||||
|
{{ type.type }}
|
||||||
|
<small v-if="type.passphrase">(passwortgeschützt)</small>
|
||||||
|
<small v-if="type.nscdr">(standard-Auslieferung)</small>
|
||||||
|
</span>
|
||||||
|
<span v-if="selected" class="absolute inset-y-0 left-0 flex items-center pl-3 text-primary">
|
||||||
|
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ListboxOption>
|
||||||
|
</ListboxOptions>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
<p class="flex flex-row text-sm">
|
||||||
|
<InformationCircleIcon class="text-gray-500 h-5 w-5" /> Wenn kein Typ ausgewählt ist, werden die Standard-Typen
|
||||||
|
zur Verfügung gestellt: <br />
|
||||||
|
-> {{ defaultTypes }}
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<TextCopy :copyText="generatedLink" />
|
||||||
|
<div v-if="selectedTypes.length != 0" class="flex flex-row gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="nscdr"
|
||||||
|
v-model="provideNSCDR"
|
||||||
|
/>
|
||||||
|
<label for="nscdr">Standard-Typen trotz Auswahl ausliefern</label>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RouterLink
|
||||||
|
:to="{ name: 'public-calendar' }"
|
||||||
|
title="Zur öffentlichen Kalender Anzeige"
|
||||||
|
class="absolute top-3 right-3"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<CalendarDaysIcon class="text-gray-500 h-5 w-5" />
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end">
|
||||||
|
<div class="flex flex-row gap-4 py-2">
|
||||||
|
<button primary-outline @click="closeModal">schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { RouterLink } from "vue-router";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import { useCalendarTypeStore } from "@/stores/admin/settings/calendarType";
|
||||||
|
import type { CalendarTypeViewModel } from "@/viewmodels/admin/settings/calendarType.models";
|
||||||
|
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||||
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
|
import TextCopy from "@/components/TextCopy.vue";
|
||||||
|
import { CalendarDaysIcon, InformationCircleIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { host } from "@/serverCom";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedTypes: [] as Array<CalendarTypeViewModel>,
|
||||||
|
provideNSCDR: false as boolean
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useModalStore, ["data"]),
|
||||||
|
...mapState(useCalendarTypeStore, ["calendarTypes"]),
|
||||||
|
defaultTypes() {
|
||||||
|
return this.calendarTypes
|
||||||
|
.filter((t) => t.nscdr)
|
||||||
|
.map((t) => t.type)
|
||||||
|
.join(", ");
|
||||||
|
},
|
||||||
|
generatedLink() {
|
||||||
|
let extend = this.selectedTypes.map((t) => [t.type, t.passphrase].filter((at) => at).join(":"));
|
||||||
|
return `webcal://${host || window.location.host}/api/public/calendar${extend.length == 0 ? "" : "?types=" + extend.join("&types=")}${this.provideNSCDR && extend.length != 0 ? '&nscdr=true':''}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchCalendarTypes();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
...mapActions(useCalendarTypeStore, ["fetchCalendarTypes"]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
216
src/components/admin/club/calendar/CreateCalendarModal.vue
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Termin erstellen</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
|
||||||
|
<div>
|
||||||
|
<Listbox v-model="selectedType" name="type">
|
||||||
|
<ListboxLabel>Termintyp</ListboxLabel>
|
||||||
|
<div class="relative mt-1">
|
||||||
|
<ListboxButton
|
||||||
|
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
|
>
|
||||||
|
<span class="block truncate w-full text-start">
|
||||||
|
{{
|
||||||
|
calendarTypes.length != 0 ? (selectedType?.type ?? "bitte auswählen") : "keine Auswahl vorhanden"
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</ListboxButton>
|
||||||
|
|
||||||
|
<transition
|
||||||
|
leave-active-class="transition duration-100 ease-in"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<ListboxOptions
|
||||||
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<ListboxOption v-if="calendarTypes.length == 0" disabled as="template">
|
||||||
|
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||||
|
<span :class="['font-normal', 'block truncate']">keine Auswahl vorhanden</span>
|
||||||
|
</li>
|
||||||
|
</ListboxOption>
|
||||||
|
<ListboxOption
|
||||||
|
v-slot="{ active, selected }"
|
||||||
|
v-for="type in calendarTypes"
|
||||||
|
:key="type.id"
|
||||||
|
:value="type"
|
||||||
|
as="template"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
:class="[
|
||||||
|
active ? 'bg-red-200 text-amber-900' : 'text-gray-900',
|
||||||
|
'relative cursor-default select-none py-2 pl-10 pr-4',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span :class="[selected ? 'font-medium' : 'font-normal', 'block truncate']">{{ type.type }}</span>
|
||||||
|
<span v-if="selected" class="absolute inset-y-0 left-0 flex items-center pl-3 text-primary">
|
||||||
|
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ListboxOption>
|
||||||
|
</ListboxOptions>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="title">Titel</label>
|
||||||
|
<input type="text" id="title" required />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="content">Beschreibung (optional)</label>
|
||||||
|
<textarea id="content" class="h-18"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="allDay"
|
||||||
|
:checked="data.allDay"
|
||||||
|
@change="(e) => (allDay = (e.target as HTMLInputElement).checked)"
|
||||||
|
/>
|
||||||
|
<label for="allDay">ganztägig</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="!allDay" class="flex flex-row gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="starttime">Startzeit</label>
|
||||||
|
<input type="datetime-local" id="starttime" required :value="data.start.split(':00+')[0]" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="endtime">Endzeit</label>
|
||||||
|
<input type="datetime-local" id="endtime" required :value="data.end.split(':00+')[0]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-row gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="startdate">Startdatum</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="startdate"
|
||||||
|
required
|
||||||
|
:value="data.start"
|
||||||
|
@change="($event) => (($refs.enddate as HTMLInputElement).max = ($event.target as HTMLInputElement).value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="enddate">Enddatum</label>
|
||||||
|
<input
|
||||||
|
ref="enddate"
|
||||||
|
type="date"
|
||||||
|
id="enddate"
|
||||||
|
required
|
||||||
|
:value="decrementEndDate(data.end)"
|
||||||
|
:min="data.start"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="location">Ort (optional)</label>
|
||||||
|
<input type="text" id="location" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
|
||||||
|
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||||
|
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end">
|
||||||
|
<div class="flex flex-row gap-4 py-2">
|
||||||
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { useCalendarStore } from "@/stores/admin/club/calendar";
|
||||||
|
import type { CreateCalendarViewModel } from "@/viewmodels/admin/club/calendar.models";
|
||||||
|
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||||
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
|
import { useCalendarTypeStore } from "@/stores/admin/settings/calendarType";
|
||||||
|
import type { CalendarTypeViewModel } from "@/viewmodels/admin/settings/calendarType.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||||
|
timeout: undefined as any,
|
||||||
|
allDay: false,
|
||||||
|
selectedType: undefined as undefined | CalendarTypeViewModel,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useModalStore, ["data"]),
|
||||||
|
...mapState(useCalendarTypeStore, ["calendarTypes"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.allDay = this.data.allDay;
|
||||||
|
this.fetchCalendarTypes();
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
try {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
...mapActions(useCalendarStore, ["createCalendar"]),
|
||||||
|
...mapActions(useCalendarTypeStore, ["fetchCalendarTypes"]),
|
||||||
|
triggerCreate(e: any) {
|
||||||
|
if (!this.selectedType) return;
|
||||||
|
let formData = e.target.elements;
|
||||||
|
let createCalendar: CreateCalendarViewModel = {
|
||||||
|
typeId: this.selectedType.id,
|
||||||
|
starttime: this.allDay
|
||||||
|
? new Date(new Date(formData.startdate.value).setHours(0, 0, 0, 0))
|
||||||
|
: formData.starttime.value,
|
||||||
|
endtime: this.allDay
|
||||||
|
? new Date(new Date(formData.enddate.value).setHours(23, 59, 59, 999))
|
||||||
|
: formData.endtime.value,
|
||||||
|
title: formData.title.value,
|
||||||
|
content: formData.content.value,
|
||||||
|
location: formData.location.value,
|
||||||
|
allDay: this.allDay,
|
||||||
|
};
|
||||||
|
this.status = "loading";
|
||||||
|
this.createCalendar(createCalendar)
|
||||||
|
.then(() => {
|
||||||
|
this.status = { status: "success" };
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
this.closeModal();
|
||||||
|
}, 1500);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = { status: "failed" };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
decrementEndDate(utcDateString: string) {
|
||||||
|
const localDate = new Date(utcDateString);
|
||||||
|
|
||||||
|
const year = localDate.getFullYear();
|
||||||
|
const month = String(localDate.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(localDate.getDate() - 1).padStart(2, "0");
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
97
src/components/admin/club/calendar/DeleteCalendarModal.vue
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Termin {{ calendar?.title }} löschen?</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<p class="text-center">
|
||||||
|
{{
|
||||||
|
new Date(calendar?.starttime ?? "").toLocaleString("de-De", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
bis
|
||||||
|
{{
|
||||||
|
new Date(calendar?.endtime ?? "").toLocaleString("de-De", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button primary :disabled="status == 'loading' || status?.status == 'success'" @click="triggerDelete">
|
||||||
|
unwiederuflich löschen
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||||
|
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end">
|
||||||
|
<div class="flex flex-row gap-4 py-2">
|
||||||
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { useCalendarStore } from "@/stores/admin/club/calendar";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||||
|
timeout: undefined as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
try {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useModalStore, ["data"]),
|
||||||
|
...mapState(useCalendarStore, ["calendars"]),
|
||||||
|
calendar() {
|
||||||
|
return this.calendars.find((r) => r.id == this.data);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
...mapActions(useCalendarStore, ["deleteCalendar"]),
|
||||||
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
|
this.deleteCalendar(this.data)
|
||||||
|
.then(() => {
|
||||||
|
this.status = { status: "success" };
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
this.closeModal();
|
||||||
|
}, 1500);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = { status: "failed" };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
307
src/components/admin/club/calendar/UpdateCalendarModal.vue
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative w-full md:max-w-md">
|
||||||
|
<TrashIcon
|
||||||
|
v-if="can('delete', 'club', 'calendar')"
|
||||||
|
class="absolute top-3 right-3 w-5 h-5 cursor-pointer"
|
||||||
|
@click="deleteCalendar"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Termin erstellen</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||||
|
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
|
||||||
|
<form v-else-if="calendar != null" class="flex flex-col gap-4 py-2" @submit.prevent="triggerUpdate">
|
||||||
|
<div>
|
||||||
|
<Listbox v-model="calendar.type" name="type">
|
||||||
|
<ListboxLabel>Termintyp</ListboxLabel>
|
||||||
|
<div class="relative mt-1">
|
||||||
|
<ListboxButton
|
||||||
|
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
|
>
|
||||||
|
<span class="block truncate w-full text-start">
|
||||||
|
{{
|
||||||
|
calendarTypes.length != 0 ? (calendar.type?.type ?? "bitte auswählen") : "keine Auswahl vorhanden"
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</ListboxButton>
|
||||||
|
|
||||||
|
<transition
|
||||||
|
leave-active-class="transition duration-100 ease-in"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<ListboxOptions
|
||||||
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<ListboxOption v-if="calendarTypes.length == 0" disabled as="template">
|
||||||
|
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||||
|
<span :class="['font-normal', 'block truncate']">keine Auswahl vorhanden</span>
|
||||||
|
</li>
|
||||||
|
</ListboxOption>
|
||||||
|
<ListboxOption
|
||||||
|
v-slot="{ active, selected }"
|
||||||
|
v-for="type in calendarTypes"
|
||||||
|
:key="type.id"
|
||||||
|
:value="type"
|
||||||
|
as="template"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
:class="[
|
||||||
|
active ? 'bg-red-200 text-amber-900' : 'text-gray-900',
|
||||||
|
'relative cursor-default select-none py-2 pl-10 pr-4',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span :class="[selected ? 'font-medium' : 'font-normal', 'block truncate']">{{ type.type }}</span>
|
||||||
|
<span v-if="selected" class="absolute inset-y-0 left-0 flex items-center pl-3 text-primary">
|
||||||
|
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ListboxOption>
|
||||||
|
</ListboxOptions>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="title">Titel</label>
|
||||||
|
<input type="text" id="title" required v-model="calendar.title" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="content">Beschreibung (optional)</label>
|
||||||
|
<textarea id="content" class="h-18" v-model="calendar.content"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-2 items-center">
|
||||||
|
<input type="checkbox" id="allDay" v-model="calendar.allDay" />
|
||||||
|
<label for="allDay">ganztägig</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="calendar.allDay == false" class="flex flex-row gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="starttime">Startzeit</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
id="starttime"
|
||||||
|
required
|
||||||
|
:value="formatForDateTimeLocalInput(calendar.starttime)"
|
||||||
|
@change="
|
||||||
|
($event) => {
|
||||||
|
calendar!.starttime = new Date(($event.target as HTMLInputElement).value).toISOString();
|
||||||
|
($refs.endtime as HTMLInputElement).min = formatForDateTimeLocalInput(
|
||||||
|
($event.target as HTMLInputElement).value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="endtime">Endzeit</label>
|
||||||
|
<input
|
||||||
|
ref="endtime"
|
||||||
|
type="datetime-local"
|
||||||
|
id="endtime"
|
||||||
|
required
|
||||||
|
:value="formatForDateTimeLocalInput(calendar.endtime)"
|
||||||
|
:min="formatForDateTimeLocalInput(calendar.starttime)"
|
||||||
|
@change="
|
||||||
|
($event) => {
|
||||||
|
calendar!.endtime = new Date(($event.target as HTMLInputElement).value).toISOString();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-row gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="startdate">Startdatum</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="startdate"
|
||||||
|
required
|
||||||
|
:value="formatForDateInput(calendar.starttime)"
|
||||||
|
@change="
|
||||||
|
($event) => {
|
||||||
|
calendar!.starttime = new Date(($event.target as HTMLInputElement).value).toISOString();
|
||||||
|
($refs.enddate as HTMLInputElement).min = formatForDateInput(($event.target as HTMLInputElement).value);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="enddate">Enddatum</label>
|
||||||
|
<input
|
||||||
|
ref="enddate"
|
||||||
|
type="date"
|
||||||
|
id="enddate"
|
||||||
|
required
|
||||||
|
:min="formatForDateInput(calendar.starttime)"
|
||||||
|
:value="formatForDateInput(calendar.endtime)"
|
||||||
|
@change="
|
||||||
|
($event) => {
|
||||||
|
calendar!.endtime = new Date(($event.target as HTMLInputElement).value).toISOString();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="location">Ort (optional)</label>
|
||||||
|
<input type="text" id="location" v-model="calendar.location" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button primary-outline type="reset" class="!w-fit" :disabled="canSaveOrReset" @click="resetForm">
|
||||||
|
verwerfen
|
||||||
|
</button>
|
||||||
|
<button primary type="submit" class="!w-fit" :disabled="status == 'loading' || canSaveOrReset">
|
||||||
|
speichern
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||||
|
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end">
|
||||||
|
<div class="flex flex-row gap-4 py-2">
|
||||||
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen / schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { useCalendarStore } from "@/stores/admin/club/calendar";
|
||||||
|
import type {
|
||||||
|
CalendarViewModel,
|
||||||
|
CreateCalendarViewModel,
|
||||||
|
UpdateCalendarViewModel,
|
||||||
|
} from "@/viewmodels/admin/club/calendar.models";
|
||||||
|
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||||
|
import { CheckIcon, ChevronUpDownIcon, TrashIcon } from "@heroicons/vue/20/solid";
|
||||||
|
import { useCalendarTypeStore } from "@/stores/admin/settings/calendarType";
|
||||||
|
import type { CalendarTypeViewModel } from "@/viewmodels/admin/settings/calendarType.models";
|
||||||
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
import isEqual from "lodash.isequal";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: "loading" as "loading" | "fetched" | "failed",
|
||||||
|
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||||
|
origin: null as null | CalendarViewModel,
|
||||||
|
calendar: null as null | CalendarViewModel,
|
||||||
|
timeout: undefined as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useModalStore, ["data"]),
|
||||||
|
...mapState(useCalendarStore, ["calendars"]),
|
||||||
|
...mapState(useCalendarTypeStore, ["calendarTypes"]),
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
canSaveOrReset(): boolean {
|
||||||
|
return isEqual(this.origin, this.calendar);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchCalendarTypes();
|
||||||
|
this.fetchItem();
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
try {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal", "openModal"]),
|
||||||
|
...mapActions(useCalendarStore, ["updateCalendar", "fetchCalendarById"]),
|
||||||
|
...mapActions(useCalendarTypeStore, ["fetchCalendarTypes"]),
|
||||||
|
resetForm() {
|
||||||
|
this.calendar = cloneDeep(this.origin);
|
||||||
|
},
|
||||||
|
fetchItem() {
|
||||||
|
this.fetchCalendarById(this.data ?? "")
|
||||||
|
.then((result) => {
|
||||||
|
this.calendar = result.data;
|
||||||
|
this.origin = cloneDeep(result.data);
|
||||||
|
this.loading = "fetched";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.loading = "failed";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
triggerUpdate(e: any) {
|
||||||
|
if (this.calendar == null) return;
|
||||||
|
let formData = e.target.elements;
|
||||||
|
let updateCalendar: UpdateCalendarViewModel = {
|
||||||
|
id: this.calendar.id,
|
||||||
|
typeId: this.calendar.type.id,
|
||||||
|
starttime: this.calendar.allDay
|
||||||
|
? new Date(new Date(formData.startdate.value).setHours(0, 0, 0, 0)).toISOString()
|
||||||
|
: new Date(formData.starttime.value).toISOString(),
|
||||||
|
endtime: this.calendar.allDay
|
||||||
|
? new Date(new Date(formData.enddate.value).setHours(23, 59, 59, 999)).toISOString()
|
||||||
|
: new Date(formData.endtime.value).toISOString(),
|
||||||
|
title: formData.title.value,
|
||||||
|
content: formData.content.value,
|
||||||
|
location: formData.location.value,
|
||||||
|
allDay: this.calendar.allDay,
|
||||||
|
};
|
||||||
|
this.status = "loading";
|
||||||
|
this.updateCalendar(updateCalendar)
|
||||||
|
.then(() => {
|
||||||
|
this.fetchItem();
|
||||||
|
this.status = { status: "success" };
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = { status: "failed" };
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
this.status = null;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteCalendar() {
|
||||||
|
if (this.origin == null) return;
|
||||||
|
this.openModal(
|
||||||
|
markRaw(defineAsyncComponent(() => import("@/components/admin/club/calendar/DeleteCalendarModal.vue"))),
|
||||||
|
this.origin.id
|
||||||
|
);
|
||||||
|
},
|
||||||
|
formatForDateTimeLocalInput(utcDateString: string) {
|
||||||
|
const localDate = new Date(utcDateString);
|
||||||
|
|
||||||
|
const year = localDate.getFullYear();
|
||||||
|
const month = String(localDate.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(localDate.getDate()).padStart(2, "0");
|
||||||
|
const hours = String(localDate.getHours()).padStart(2, "0");
|
||||||
|
const minutes = String(localDate.getMinutes()).padStart(2, "0");
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||||
|
},
|
||||||
|
formatForDateInput(utcDateString: string) {
|
||||||
|
const localDate = new Date(utcDateString);
|
||||||
|
|
||||||
|
const year = localDate.getFullYear();
|
||||||
|
const month = String(localDate.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(localDate.getDate()).padStart(2, "0");
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -66,6 +66,10 @@
|
||||||
<label for="birthdate">Geburtsdatum</label>
|
<label for="birthdate">Geburtsdatum</label>
|
||||||
<input type="date" id="birthdate" required />
|
<input type="date" id="birthdate" required />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="internalId">Interne ID (optional)</label>
|
||||||
|
<input type="text" id="internalId" />
|
||||||
|
</div>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
|
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
|
||||||
<Spinner v-if="status == 'loading'" class="my-auto" />
|
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||||
|
@ -94,8 +98,8 @@ import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { Salutation } from "@/enums/salutation";
|
import { Salutation } from "@/enums/salutation";
|
||||||
import { useMemberStore } from "@/stores/admin/member";
|
import { useMemberStore } from "@/stores/admin/club/member/member";
|
||||||
import type { CreateMemberViewModel } from "@/viewmodels/admin/member.models";
|
import type { CreateMemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -127,7 +131,9 @@ export default defineComponent({
|
||||||
lastname: formData.lastname.value,
|
lastname: formData.lastname.value,
|
||||||
nameaffix: formData.nameaffix.value,
|
nameaffix: formData.nameaffix.value,
|
||||||
birthdate: formData.birthdate.value,
|
birthdate: formData.birthdate.value,
|
||||||
|
internalId: formData.internalId.value,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createMember(createMember)
|
this.createMember(createMember)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -44,8 +44,8 @@ import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { Salutation } from "@/enums/salutation";
|
import { Salutation } from "@/enums/salutation";
|
||||||
import { useMemberStore } from "@/stores/admin/member";
|
import { useMemberStore } from "@/stores/admin/club/member/member";
|
||||||
import type { CreateMemberViewModel } from "@/viewmodels/admin/member.models";
|
import type { CreateMemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -97,14 +97,14 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { useMembershipStatusStore } from "@/stores/admin/membershipStatus";
|
import { useMembershipStatusStore } from "@/stores/admin/settings/membershipStatus";
|
||||||
import type { MembershipStatusViewModel } from "@/viewmodels/admin/membershipStatus.models";
|
import type { MembershipStatusViewModel } from "@/viewmodels/admin/settings/membershipStatus.models";
|
||||||
import type { CreateMembershipViewModel } from "@/viewmodels/admin/membership.models";
|
import type { CreateMembershipViewModel } from "@/viewmodels/admin/club/member/membership.models";
|
||||||
import { useMembershipStore } from "@/stores/admin/membership";
|
import { useMembershipStore } from "@/stores/admin/club/member/membership";
|
||||||
import { useAwardStore } from "@/stores/admin/award";
|
import { useAwardStore } from "@/stores/admin/settings/award";
|
||||||
import type { AwardViewModel } from "@/viewmodels/admin/award.models";
|
import type { AwardViewModel } from "@/viewmodels/admin/settings/award.models";
|
||||||
import type { CreateMemberAwardViewModel } from "@/viewmodels/admin/memberAward.models";
|
import type { CreateMemberAwardViewModel } from "@/viewmodels/admin/club/member/memberAward.models";
|
||||||
import { useMemberAwardStore } from "@/stores/admin/memberAward";
|
import { useMemberAwardStore } from "@/stores/admin/club/member/memberAward";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -38,7 +38,7 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useMemberAwardStore } from "@/stores/admin/memberAward";
|
import { useMemberAwardStore } from "@/stores/admin/club/member/memberAward";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -98,14 +100,14 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { useAwardStore } from "@/stores/admin/award";
|
import { useAwardStore } from "@/stores/admin/settings/award";
|
||||||
import type {
|
import type {
|
||||||
CreateMemberAwardViewModel,
|
CreateMemberAwardViewModel,
|
||||||
MemberAwardViewModel,
|
MemberAwardViewModel,
|
||||||
UpdateMemberAwardViewModel,
|
UpdateMemberAwardViewModel,
|
||||||
} from "@/viewmodels/admin/memberAward.models";
|
} from "@/viewmodels/admin/club/member/memberAward.models";
|
||||||
import { useMemberAwardStore } from "@/stores/admin/memberAward";
|
import { useMemberAwardStore } from "@/stores/admin/club/member/memberAward";
|
||||||
import isEqual from "lodash.isEqual";
|
import isEqual from "lodash.isequal";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
|
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
|
||||||
<div class="bg-primary p-2 text-white flex flex-row gap-2 justify-between items-center">
|
<div class="bg-primary p-2 text-white flex flex-row gap-2 justify-between items-center">
|
||||||
<p class="grow">{{ award.award }}</p>
|
<p class="grow">{{ award.award }}</p>
|
||||||
<PencilIcon class="w-5 h-5 cursor-pointer" @click="openEditModal" />
|
<PencilIcon v-if="can('update', 'club', 'member')" class="w-5 h-5 cursor-pointer" @click="openEditModal" />
|
||||||
<TrashIcon class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
|
<TrashIcon v-if="can('delete', 'club', 'member')" class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
|
||||||
</div>
|
</div>
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<p>erhalten am: {{ award.date }}</p>
|
<p>erhalten am: {{ award.date }}</p>
|
||||||
|
@ -16,9 +16,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
|
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
|
||||||
import { mapState, mapActions } from "pinia";
|
import { mapState, mapActions } from "pinia";
|
||||||
import type { MemberAwardViewModel } from "@/viewmodels/admin/memberAward.models";
|
import type { MemberAwardViewModel } from "@/viewmodels/admin/club/member/memberAward.models";
|
||||||
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -29,6 +30,9 @@ export default defineComponent({
|
||||||
default: {},
|
default: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useModalStore, ["openModal"]),
|
...mapActions(useModalStore, ["openModal"]),
|
||||||
openEditModal() {
|
openEditModal() {
|
||||||
|
|
|
@ -69,6 +69,10 @@
|
||||||
<label for="email">Mail-Adresse</label>
|
<label for="email">Mail-Adresse</label>
|
||||||
<input type="text" id="email" required />
|
<input type="text" id="email" required />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="selectedCommunicationType?.fields.includes('postalCode')">
|
||||||
|
<label for="postalCode">Postleitzahl</label>
|
||||||
|
<input type="text" id="postalCode" required />
|
||||||
|
</div>
|
||||||
<div v-if="selectedCommunicationType?.fields.includes('city')">
|
<div v-if="selectedCommunicationType?.fields.includes('city')">
|
||||||
<label for="city">Stadt</label>
|
<label for="city">Stadt</label>
|
||||||
<input type="text" id="city" required />
|
<input type="text" id="city" required />
|
||||||
|
@ -93,6 +97,10 @@
|
||||||
<input type="checkbox" id="isNewsletterMain" />
|
<input type="checkbox" id="isNewsletterMain" />
|
||||||
<label for="isNewsletterMain">Newsletter hier hin versenden?</label>
|
<label for="isNewsletterMain">Newsletter hier hin versenden?</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="selectedCommunicationType?.fields.includes('mobile')" class="flex flex-row items-center gap-2">
|
||||||
|
<input type="checkbox" id="isSMSAlarming" />
|
||||||
|
<label for="isSMSAlarming">SMS-Alarmierung hier hin versenden?</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
|
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
|
||||||
|
@ -121,10 +129,10 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { useCommunicationStore } from "@/stores/admin/communication";
|
import { useCommunicationStore } from "@/stores/admin/club/member/communication";
|
||||||
import type { CreateCommunicationViewModel } from "@/viewmodels/admin/communication.models";
|
import type { CreateCommunicationViewModel } from "@/viewmodels/admin/club/member/communication.models";
|
||||||
import { useCommunicationTypeStore } from "@/stores/admin/communicationType";
|
import { useCommunicationTypeStore } from "@/stores/admin/settings/communicationType";
|
||||||
import type { CommunicationTypeViewModel } from "@/viewmodels/admin/communicationType.models";
|
import type { CommunicationTypeViewModel } from "@/viewmodels/admin/settings/communicationType.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -158,13 +166,16 @@ 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,
|
||||||
city: formData.city?.value,
|
city: formData.city?.value,
|
||||||
street: formData.street?.value,
|
street: formData.street?.value,
|
||||||
streetNumber: formData.streetNumber?.value,
|
streetNumber: formData.streetNumber?.value,
|
||||||
streetNumberAddition: formData.streetNumberAddition?.value,
|
streetNumberAddition: formData.streetNumberAddition?.value,
|
||||||
isNewsletterMain: formData.isNewsletterMain.checked,
|
isNewsletterMain: formData.isNewsletterMain.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" };
|
||||||
|
|
|
@ -41,7 +41,7 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useCommunicationStore } from "@/stores/admin/communication";
|
import { useCommunicationStore } from "@/stores/admin/club/member/communication";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<br />
|
<br />
|
||||||
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||||
<p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">↺ laden fehlgeschlagen</p>
|
<p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">↺ laden fehlgeschlagen</p>
|
||||||
<form v-else-if="communication != null" class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
|
<form v-else-if="communication != null" class="flex flex-col gap-4 py-2" @submit.prevent="triggerUpdate">
|
||||||
<div>
|
<div>
|
||||||
<p>Type: {{ communication.type.type }}</p>
|
<p>Type: {{ communication.type.type }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,6 +18,10 @@
|
||||||
<label for="email">Mail-Adresse</label>
|
<label for="email">Mail-Adresse</label>
|
||||||
<input type="text" id="email" required v-model="communication.email" />
|
<input type="text" id="email" required v-model="communication.email" />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="communication.type.fields.includes('postalCode')">
|
||||||
|
<label for="postalCode">Postleitzahl</label>
|
||||||
|
<input type="text" id="postalCode" required v-model="communication.postalCode" />
|
||||||
|
</div>
|
||||||
<div v-if="communication.type.fields.includes('city')">
|
<div v-if="communication.type.fields.includes('city')">
|
||||||
<label for="city">Stadt</label>
|
<label for="city">Stadt</label>
|
||||||
<input type="text" id="city" required v-model="communication.city" />
|
<input type="text" id="city" required v-model="communication.city" />
|
||||||
|
@ -42,6 +46,10 @@
|
||||||
<input type="checkbox" id="isNewsletterMain" v-model="communication.isNewsletterMain" />
|
<input type="checkbox" id="isNewsletterMain" v-model="communication.isNewsletterMain" />
|
||||||
<label for="isNewsletterMain">Newsletter hier hin versenden?</label>
|
<label for="isNewsletterMain">Newsletter hier hin versenden?</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="communication.type.fields.includes('mobile')" class="flex flex-row items-center gap-2">
|
||||||
|
<input type="checkbox" id="isSMSAlarming" v-model="communication.isSMSAlarming" />
|
||||||
|
<label for="isSMSAlarming">SMS-Alarmierung hier hin versenden?</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<button primary-outline type="reset" :disabled="canSaveOrReset" @click="resetForm">verwerfen</button>
|
<button primary-outline type="reset" :disabled="canSaveOrReset" @click="resetForm">verwerfen</button>
|
||||||
|
@ -54,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>
|
||||||
|
@ -67,13 +77,13 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useCommunicationStore } from "@/stores/admin/communication";
|
import { useCommunicationStore } from "@/stores/admin/club/member/communication";
|
||||||
import type {
|
import type {
|
||||||
CreateCommunicationViewModel,
|
CreateCommunicationViewModel,
|
||||||
CommunicationViewModel,
|
CommunicationViewModel,
|
||||||
UpdateCommunicationViewModel,
|
UpdateCommunicationViewModel,
|
||||||
} from "@/viewmodels/admin/communication.models";
|
} from "@/viewmodels/admin/club/member/communication.models";
|
||||||
import isEqual from "lodash.isEqual";
|
import isEqual from "lodash.isequal";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -120,7 +130,7 @@ export default defineComponent({
|
||||||
this.loading = "failed";
|
this.loading = "failed";
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
triggerCreate(e: any) {
|
triggerUpdate(e: any) {
|
||||||
if (this.communication == null) return;
|
if (this.communication == null) return;
|
||||||
let formData = e.target.elements;
|
let formData = e.target.elements;
|
||||||
let updateCommunication: UpdateCommunicationViewModel = {
|
let updateCommunication: UpdateCommunicationViewModel = {
|
||||||
|
@ -128,12 +138,15 @@ 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,
|
||||||
city: formData.city?.value,
|
city: formData.city?.value,
|
||||||
street: formData.street?.value,
|
street: formData.street?.value,
|
||||||
streetNumber: formData.streetNumber?.value,
|
streetNumber: formData.streetNumber?.value,
|
||||||
streetNumberAddition: formData.streetNumberAddition?.value,
|
streetNumberAddition: formData.streetNumberAddition?.value,
|
||||||
isNewsletterMain: formData.isNewsletterMain.checked,
|
isNewsletterMain: formData.isNewsletterMain.checked,
|
||||||
|
isSMSAlarming: formData.isSMSAlarming?.checked,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.updateCommunication(updateCommunication)
|
this.updateCommunication(updateCommunication)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchItem();
|
this.fetchItem();
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
|
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
|
||||||
<div class="bg-primary p-2 text-white flex flex-row gap-2 justify-between items-center">
|
<div class="bg-primary p-2 text-white flex flex-row gap-2 justify-between items-center">
|
||||||
|
<FireIcon class="h-5 w-5 pr-1 box-content" v-if="communication.isSMSAlarming" />
|
||||||
<EnvelopeIcon class="h-5 w-5 pr-1 box-content" v-if="communication.isNewsletterMain" />
|
<EnvelopeIcon class="h-5 w-5 pr-1 box-content" v-if="communication.isNewsletterMain" />
|
||||||
<p class="grow">{{ communication.type.type }} {{ communication.preferred ? "(bevorzugt)" : "" }}</p>
|
<p class="grow">{{ communication.type.type }} {{ communication.preferred ? "(bevorzugt)" : "" }}</p>
|
||||||
<PencilIcon class="w-5 h-5 cursor-pointer" @click="openEditModal" />
|
<PencilIcon v-if="can('update', 'club', 'member')" class="w-5 h-5 cursor-pointer" @click="openEditModal" />
|
||||||
<TrashIcon class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
|
<TrashIcon v-if="can('delete', 'club', 'member')" class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
|
||||||
</div>
|
</div>
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<p v-for="field in communication.type.fields" :key="field">{{ field }}: {{ communication[field] || "--" }}</p>
|
<p v-for="field in communication.type.fields" :key="field">{{ field }}: {{ communication[field] || "--" }}</p>
|
||||||
|
@ -15,9 +16,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
|
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
|
||||||
import { mapState, mapActions } from "pinia";
|
import { mapState, mapActions } from "pinia";
|
||||||
import type { CommunicationViewModel } from "@/viewmodels/admin/communication.models";
|
import type { CommunicationViewModel } from "@/viewmodels/admin/club/member/communication.models";
|
||||||
import { EnvelopeIcon, PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
import { EnvelopeIcon, PencilIcon, TrashIcon, FireIcon } from "@heroicons/vue/24/outline";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -28,6 +30,9 @@ export default defineComponent({
|
||||||
default: {},
|
default: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useModalStore, ["openModal"]),
|
...mapActions(useModalStore, ["openModal"]),
|
||||||
openEditModal() {
|
openEditModal() {
|
||||||
|
|
|
@ -99,14 +99,14 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { useMembershipStatusStore } from "@/stores/admin/membershipStatus";
|
import { useMembershipStatusStore } from "@/stores/admin/settings/membershipStatus";
|
||||||
import type { MembershipStatusViewModel } from "@/viewmodels/admin/membershipStatus.models";
|
import type { MembershipStatusViewModel } from "@/viewmodels/admin/settings/membershipStatus.models";
|
||||||
import type { CreateMembershipViewModel } from "@/viewmodels/admin/membership.models";
|
import type { CreateMembershipViewModel } from "@/viewmodels/admin/club/member/membership.models";
|
||||||
import { useMembershipStore } from "@/stores/admin/membership";
|
import { useMembershipStore } from "@/stores/admin/club/member/membership";
|
||||||
import { useExecutivePositionStore } from "@/stores/admin/executivePosition";
|
import { useExecutivePositionStore } from "@/stores/admin/settings/executivePosition";
|
||||||
import type { ExecutivePositionViewModel } from "@/viewmodels/admin/executivePosition.models";
|
import type { ExecutivePositionViewModel } from "@/viewmodels/admin/settings/executivePosition.models";
|
||||||
import type { CreateMemberExecutivePositionViewModel } from "@/viewmodels/admin/memberExecutivePosition.models";
|
import type { CreateMemberExecutivePositionViewModel } from "@/viewmodels/admin/club/member/memberExecutivePosition.models";
|
||||||
import { useMemberExecutivePositionStore } from "@/stores/admin/memberExecutivePosition";
|
import { useMemberExecutivePositionStore } from "@/stores/admin/club/member/memberExecutivePosition";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -38,7 +38,7 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useMemberExecutivePositionStore } from "@/stores/admin/memberExecutivePosition";
|
import { useMemberExecutivePositionStore } from "@/stores/admin/club/member/memberExecutivePosition";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -104,14 +106,14 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { useExecutivePositionStore } from "@/stores/admin/executivePosition";
|
import { useExecutivePositionStore } from "@/stores/admin/settings/executivePosition";
|
||||||
import type {
|
import type {
|
||||||
CreateMemberExecutivePositionViewModel,
|
CreateMemberExecutivePositionViewModel,
|
||||||
MemberExecutivePositionViewModel,
|
MemberExecutivePositionViewModel,
|
||||||
UpdateMemberExecutivePositionViewModel,
|
UpdateMemberExecutivePositionViewModel,
|
||||||
} from "@/viewmodels/admin/memberExecutivePosition.models";
|
} from "@/viewmodels/admin/club/member/memberExecutivePosition.models";
|
||||||
import { useMemberExecutivePositionStore } from "@/stores/admin/memberExecutivePosition";
|
import { useMemberExecutivePositionStore } from "@/stores/admin/club/member/memberExecutivePosition";
|
||||||
import isEqual from "lodash.isEqual";
|
import isEqual from "lodash.isequal";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
|
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
|
||||||
<div class="bg-primary p-2 text-white flex flex-row gap-2 justify-between items-center">
|
<div class="bg-primary p-2 text-white flex flex-row gap-2 justify-between items-center">
|
||||||
<p class="grow">{{ position.executivePosition }} von {{ position.start }} bis {{ position.end ?? "heute" }}</p>
|
<p class="grow">{{ position.executivePosition }} von {{ position.start }} bis {{ position.end ?? "heute" }}</p>
|
||||||
<PencilIcon class="w-5 h-5 cursor-pointer" @click="openEditModal" />
|
<PencilIcon v-if="can('update', 'club', 'member')" class="w-5 h-5 cursor-pointer" @click="openEditModal" />
|
||||||
<TrashIcon class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
|
<TrashIcon v-if="can('delete', 'club', 'member')" class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="position.note" class="p-2">
|
<div v-if="position.note" class="p-2">
|
||||||
<p v-if="position.note">Notiz: {{ position.note }}</p>
|
<p v-if="position.note">Notiz: {{ position.note }}</p>
|
||||||
|
@ -14,9 +14,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
|
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
|
||||||
import { mapState, mapActions } from "pinia";
|
import { mapState, mapActions } from "pinia";
|
||||||
import type { MemberExecutivePositionViewModel } from "@/viewmodels/admin/memberExecutivePosition.models";
|
import type { MemberExecutivePositionViewModel } from "@/viewmodels/admin/club/member/memberExecutivePosition.models";
|
||||||
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -27,6 +28,9 @@ export default defineComponent({
|
||||||
default: {},
|
default: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useModalStore, ["openModal"]),
|
...mapActions(useModalStore, ["openModal"]),
|
||||||
openEditModal() {
|
openEditModal() {
|
||||||
|
|
|
@ -1,42 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
|
<RouterLink
|
||||||
<RouterLink
|
:to="{ name: 'admin-club-member-overview', params: { memberId: member.id } }"
|
||||||
:to="{ name: 'admin-club-member-overview', params: { memberId: member.id } }"
|
class="flex flex-col h-fit w-full border border-primary rounded-md"
|
||||||
|
>
|
||||||
|
<div
|
||||||
class="bg-primary p-2 text-white flex flex-row justify-between items-center"
|
class="bg-primary p-2 text-white flex flex-row justify-between items-center"
|
||||||
>
|
>
|
||||||
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
||||||
<div v-if="false" class="flex flex-row">
|
|
||||||
<RouterLink
|
|
||||||
v-if="can('read', 'club', 'member')"
|
|
||||||
:to="{ name: 'admin-club-member-overview', params: { memberId: member.id } }"
|
|
||||||
>
|
|
||||||
<CircleStackIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
|
||||||
</RouterLink>
|
|
||||||
<RouterLink
|
|
||||||
v-if="can('update', 'club', 'member')"
|
|
||||||
:to="{ name: 'admin-club-member-edit', params: { id: member.id } }"
|
|
||||||
>
|
|
||||||
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
|
||||||
</RouterLink>
|
|
||||||
<div v-if="can('delete', 'club', 'member')" @click="openDeleteModal">
|
|
||||||
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</RouterLink>
|
|
||||||
<div class="p-2">
|
|
||||||
<p>beigetreten: {{ member.firstMembershipEntry?.start }}</p>
|
|
||||||
<p v-if="member.lastMembershipEntry?.end">ausgetreten: {{ member.lastMembershipEntry?.end }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="p-2">
|
||||||
|
<p v-if="member.internalId">Interne ID: {{ member.internalId }}</p>
|
||||||
|
<p>beigetreten: {{ member.firstMembershipEntry?.start }}</p>
|
||||||
|
<p v-if="member.lastMembershipEntry?.end">ausgetreten: {{ member.lastMembershipEntry?.end }}, da {{member.lastMembershipEntry?.terminationReason ?? '- kein Grund angegeben'}}</p>
|
||||||
|
</div>
|
||||||
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent, defineAsyncComponent, markRaw, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import { mapState, mapActions } from "pinia";
|
import { mapState, mapActions } from "pinia";
|
||||||
import { PencilIcon, TrashIcon, CircleStackIcon } from "@heroicons/vue/24/outline";
|
|
||||||
import { useAbilityStore } from "@/stores/ability";
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||||
import type { MemberViewModel } from "@/viewmodels/admin/member.models";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -47,14 +31,5 @@ export default defineComponent({
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useAbilityStore, ["can"]),
|
...mapState(useAbilityStore, ["can"]),
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
...mapActions(useModalStore, ["openModal"]),
|
|
||||||
openDeleteModal() {
|
|
||||||
// this.openModal(
|
|
||||||
// markRaw(defineAsyncComponent(() => import("@/components/admin/.vue"))),
|
|
||||||
// this.member.id
|
|
||||||
// );
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
60
src/components/admin/club/member/MemberNameListModal.vue
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full flex flex-col gap-2">
|
||||||
|
<Spinner v-if="status == 'loading'" />
|
||||||
|
<div class="grow">
|
||||||
|
<iframe ref="viewer" class="w-full h-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2 justify-end">
|
||||||
|
<a ref="download" button primary class="!w-fit">download</a>
|
||||||
|
<button primary-outline class="!w-fit" @click="closeModal">schließen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
import { useMemberStore } from "@/stores/admin/club/member/member";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useModalStore, ["data"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchItem();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
...mapActions(useMemberStore, ["printMemberList"]),
|
||||||
|
fetchItem() {
|
||||||
|
this.status = "loading";
|
||||||
|
this.printMemberList()
|
||||||
|
.then((response) => {
|
||||||
|
this.status = { status: "success" };
|
||||||
|
const blob = new Blob([response.data], { type: "application/pdf" });
|
||||||
|
(this.$refs.viewer as HTMLIFrameElement).src = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const fileURL = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
|
const fileLink = (this.$refs.download as HTMLAnchorElement)
|
||||||
|
fileLink.href = fileURL;
|
||||||
|
fileLink.setAttribute("download", "Mitgliederliste.pdf");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = { status: "failed" };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -106,14 +106,14 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { useMembershipStatusStore } from "@/stores/admin/membershipStatus";
|
import { useMembershipStatusStore } from "@/stores/admin/settings/membershipStatus";
|
||||||
import type { MembershipStatusViewModel } from "@/viewmodels/admin/membershipStatus.models";
|
import type { MembershipStatusViewModel } from "@/viewmodels/admin/settings/membershipStatus.models";
|
||||||
import type { CreateMembershipViewModel } from "@/viewmodels/admin/membership.models";
|
import type { CreateMembershipViewModel } from "@/viewmodels/admin/club/member/membership.models";
|
||||||
import { useMembershipStore } from "@/stores/admin/membership";
|
import { useMembershipStore } from "@/stores/admin/club/member/membership";
|
||||||
import { useQualificationStore } from "@/stores/admin/qualification";
|
import { useQualificationStore } from "@/stores/admin/settings/qualification";
|
||||||
import type { QualificationViewModel } from "@/viewmodels/admin/qualification.models";
|
import type { QualificationViewModel } from "@/viewmodels/admin/settings/qualification.models";
|
||||||
import type { CreateMemberQualificationViewModel } from "@/viewmodels/admin/memberQualification.models";
|
import type { CreateMemberQualificationViewModel } from "@/viewmodels/admin/club/member/memberQualification.models";
|
||||||
import { useMemberQualificationStore } from "@/stores/admin/memberQualification";
|
import { useMemberQualificationStore } from "@/stores/admin/club/member/memberQualification";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -41,8 +41,8 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useQualificationStore } from "@/stores/admin/qualification";
|
import { useQualificationStore } from "@/stores/admin/settings/qualification";
|
||||||
import { useMemberQualificationStore } from "@/stores/admin/memberQualification";
|
import { useMemberQualificationStore } from "@/stores/admin/club/member/memberQualification";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -105,14 +107,14 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { useQualificationStore } from "@/stores/admin/qualification";
|
import { useQualificationStore } from "@/stores/admin/settings/qualification";
|
||||||
import type {
|
import type {
|
||||||
CreateMemberQualificationViewModel,
|
CreateMemberQualificationViewModel,
|
||||||
MemberQualificationViewModel,
|
MemberQualificationViewModel,
|
||||||
UpdateMemberQualificationViewModel,
|
UpdateMemberQualificationViewModel,
|
||||||
} from "@/viewmodels/admin/memberQualification.models";
|
} from "@/viewmodels/admin/club/member/memberQualification.models";
|
||||||
import { useMemberQualificationStore } from "@/stores/admin/memberQualification";
|
import { useMemberQualificationStore } from "@/stores/admin/club/member/memberQualification";
|
||||||
import isEqual from "lodash.isEqual";
|
import isEqual from "lodash.isequal";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
<p class="grow">
|
<p class="grow">
|
||||||
{{ qualification.qualification }} von {{ qualification.start }} bis {{ qualification.end ?? "heute" }}
|
{{ qualification.qualification }} von {{ qualification.start }} bis {{ qualification.end ?? "heute" }}
|
||||||
</p>
|
</p>
|
||||||
<PencilIcon class="w-5 h-5 cursor-pointer" @click="openEditModal" />
|
<PencilIcon v-if="can('update', 'club', 'member')" class="w-5 h-5 cursor-pointer" @click="openEditModal" />
|
||||||
<TrashIcon class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
|
<TrashIcon v-if="can('delete', 'club', 'member')" class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="qualification.note || qualification.terminationReason" class="p-2">
|
<div v-if="qualification.note || qualification.terminationReason" class="p-2">
|
||||||
<p v-if="qualification.note">Notiz: {{ qualification.note }}</p>
|
<p v-if="qualification.note">Notiz: {{ qualification.note }}</p>
|
||||||
|
@ -17,9 +17,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
|
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
|
||||||
import { mapState, mapActions } from "pinia";
|
import { mapState, mapActions } from "pinia";
|
||||||
import type { MemberQualificationViewModel } from "@/viewmodels/admin/memberQualification.models";
|
import type { MemberQualificationViewModel } from "@/viewmodels/admin/club/member/memberQualification.models";
|
||||||
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -30,6 +31,9 @@ export default defineComponent({
|
||||||
default: {},
|
default: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useModalStore, ["openModal"]),
|
...mapActions(useModalStore, ["openModal"]),
|
||||||
openEditModal() {
|
openEditModal() {
|
||||||
|
|
|
@ -63,10 +63,6 @@
|
||||||
</div>
|
</div>
|
||||||
</Listbox>
|
</Listbox>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label for="internalId">Interne ID (optional)</label>
|
|
||||||
<input type="text" id="internalId" />
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label for="start">Startdatum</label>
|
<label for="start">Startdatum</label>
|
||||||
<input type="date" id="start" required />
|
<input type="date" id="start" required />
|
||||||
|
@ -98,10 +94,10 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { useMembershipStatusStore } from "@/stores/admin/membershipStatus";
|
import { useMembershipStatusStore } from "@/stores/admin/settings/membershipStatus";
|
||||||
import type { MembershipStatusViewModel } from "@/viewmodels/admin/membershipStatus.models";
|
import type { MembershipStatusViewModel } from "@/viewmodels/admin/settings/membershipStatus.models";
|
||||||
import type { CreateMembershipViewModel } from "@/viewmodels/admin/membership.models";
|
import type { CreateMembershipViewModel } from "@/viewmodels/admin/club/member/membership.models";
|
||||||
import { useMembershipStore } from "@/stores/admin/membership";
|
import { useMembershipStore } from "@/stores/admin/club/member/membership";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -132,10 +128,10 @@ export default defineComponent({
|
||||||
if (this.selectedStatus == undefined) return;
|
if (this.selectedStatus == undefined) return;
|
||||||
let formData = e.target.elements;
|
let formData = e.target.elements;
|
||||||
let createMember: CreateMembershipViewModel = {
|
let createMember: CreateMembershipViewModel = {
|
||||||
internalId: formData.internalId.value,
|
|
||||||
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" };
|
||||||
|
|
|
@ -40,7 +40,7 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useMembershipStore } from "@/stores/admin/membership";
|
import { useMembershipStore } from "@/stores/admin/club/member/membership";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -63,10 +63,6 @@
|
||||||
</div>
|
</div>
|
||||||
</Listbox>
|
</Listbox>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label for="internalId">Interne ID (optional)</label>
|
|
||||||
<input type="text" id="internalId" v-model="membership.internalId" />
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label for="start">Startdatum</label>
|
<label for="start">Startdatum</label>
|
||||||
<input type="date" id="start" required v-model="membership.start" />
|
<input type="date" id="start" required v-model="membership.start" />
|
||||||
|
@ -90,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>
|
||||||
|
@ -105,14 +103,14 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { useMembershipStatusStore } from "@/stores/admin/membershipStatus";
|
import { useMembershipStatusStore } from "@/stores/admin/settings/membershipStatus";
|
||||||
import type {
|
import type {
|
||||||
CreateMembershipViewModel,
|
CreateMembershipViewModel,
|
||||||
MembershipViewModel,
|
MembershipViewModel,
|
||||||
UpdateMembershipViewModel,
|
UpdateMembershipViewModel,
|
||||||
} from "@/viewmodels/admin/membership.models";
|
} from "@/viewmodels/admin/club/member/membership.models";
|
||||||
import { useMembershipStore } from "@/stores/admin/membership";
|
import { useMembershipStore } from "@/stores/admin/club/member/membership";
|
||||||
import isEqual from "lodash.isEqual";
|
import isEqual from "lodash.isequal";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -169,12 +167,12 @@ export default defineComponent({
|
||||||
let formData = e.target.elements;
|
let formData = e.target.elements;
|
||||||
let updateMembership: UpdateMembershipViewModel = {
|
let updateMembership: UpdateMembershipViewModel = {
|
||||||
id: this.membership.id,
|
id: this.membership.id,
|
||||||
internalId: formData.internalId.value,
|
|
||||||
start: formData.start.value,
|
start: formData.start.value,
|
||||||
end: formData.end.value,
|
end: formData.end.value,
|
||||||
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();
|
||||||
|
|
|
@ -5,12 +5,11 @@
|
||||||
{{ membership.start }} bis {{ membership.end ?? "heute" }}:
|
{{ membership.start }} bis {{ membership.end ?? "heute" }}:
|
||||||
{{ membership.status }}
|
{{ membership.status }}
|
||||||
</p>
|
</p>
|
||||||
<PencilIcon class="w-5 h-5 cursor-pointer" @click="openEditModal" />
|
<PencilIcon v-if="can('update', 'club', 'member')" class="w-5 h-5 cursor-pointer" @click="openEditModal" />
|
||||||
<TrashIcon class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
|
<TrashIcon v-if="can('delete', 'club', 'member')" class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="membership.terminationReason || membership.internalId" class="p-2">
|
<div v-if="membership.terminationReason" class="p-2">
|
||||||
<p v-if="membership.internalId">Interne ID: {{ membership.internalId }}</p>
|
<p v-if="membership.terminationReason">Grund: {{ membership.terminationReason }}</p>
|
||||||
<p v-if="membership.terminationReason">beendet, weil: {{ membership.terminationReason }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -18,9 +17,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
|
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
|
||||||
import { mapState, mapActions } from "pinia";
|
import { mapState, mapActions } from "pinia";
|
||||||
import type { MembershipViewModel } from "@/viewmodels/admin/membership.models";
|
import type { MembershipViewModel } from "@/viewmodels/admin/club/member/membership.models";
|
||||||
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -31,6 +31,9 @@ export default defineComponent({
|
||||||
default: {},
|
default: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useModalStore, ["openModal"]),
|
...mapActions(useModalStore, ["openModal"]),
|
||||||
openEditModal() {
|
openEditModal() {
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Newsletter erstellen</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<form ref="form" class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
|
||||||
|
<div>
|
||||||
|
<label for="title">Titel</label>
|
||||||
|
<input type="text" id="title" required autocomplete="false" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
|
||||||
|
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||||
|
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end">
|
||||||
|
<div class="flex flex-row gap-4 py-2">
|
||||||
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/club/protocol/protocol";
|
||||||
|
import type { CreateProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models";
|
||||||
|
import { useNewsletterStore } from "../../../../stores/admin/club/newsletter/newsletter";
|
||||||
|
import type { CreateNewsletterViewModel } from "../../../../viewmodels/admin/club/newsletter/newsletter.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||||
|
timeout: undefined as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
try {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
...mapActions(useNewsletterStore, ["createNewsletter"]),
|
||||||
|
triggerCreate(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
let createNewsletter: CreateNewsletterViewModel = {
|
||||||
|
title: formData.title.value,
|
||||||
|
};
|
||||||
|
this.status = "loading";
|
||||||
|
this.createNewsletter(createNewsletter)
|
||||||
|
.then(() => {
|
||||||
|
this.status = { status: "success" };
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
(this.$refs.form as HTMLFormElement).reset();
|
||||||
|
this.closeModal();
|
||||||
|
}, 1500);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = { status: "failed" };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Newsletter wird noch synchronisiert</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p>Es gibt noch Daten, welche synchronisiert werden müssen.</p>
|
||||||
|
<p>Dieses PopUp entfernt sich von selbst nach erfolgreicher Synchronisierung.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/club/protocol/protocol";
|
||||||
|
import type { CreateProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({});
|
||||||
|
</script>
|
29
src/components/admin/club/newsletter/NewsletterListItem.vue
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
|
||||||
|
<RouterLink
|
||||||
|
:to="{ name: 'admin-club-newsletter-overview', params: { newsletterId: newsletter.id } }"
|
||||||
|
class="bg-primary p-2 text-white flex flex-row justify-between items-center"
|
||||||
|
>
|
||||||
|
<p>{{ newsletter.title }}</p>
|
||||||
|
<PaperAirplaneIcon v-if="newsletter.isSent" class="w-5 h-5" />
|
||||||
|
</RouterLink>
|
||||||
|
<div class="p-2 max-h-48 overflow-y-auto">
|
||||||
|
<p v-html="newsletter.description"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import type { NewsletterViewModel } from "@/viewmodels/admin/club/newsletter/newsletter.models";
|
||||||
|
import { PaperAirplaneIcon } from "@heroicons/vue/24/outline";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
newsletter: { type: Object as PropType<NewsletterViewModel>, default: {} },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -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>
|
|
@ -0,0 +1,57 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full flex flex-col gap-2">
|
||||||
|
<Spinner v-if="status == 'loading'" />
|
||||||
|
<div class="grow">
|
||||||
|
<iframe ref="viewer" class="w-full h-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button primary-outline class="!w-fit self-end" @click="closeModal">schließen</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
import { useNewsletterPrintoutStore } from "@/stores/admin/club/newsletter/newsletterPrintout";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useModalStore, ["data"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchItem();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
...mapActions(useNewsletterPrintoutStore, ["fetchNewsletterPrintoutPreview", "fetchNewsletterPrintoutById"]),
|
||||||
|
fetchItem() {
|
||||||
|
this.status = "loading";
|
||||||
|
let query: Promise<AxiosResponse<any, any>>;
|
||||||
|
if (this.data) {
|
||||||
|
query = this.fetchNewsletterPrintoutById(this.data);
|
||||||
|
} else {
|
||||||
|
query = this.fetchNewsletterPrintoutPreview();
|
||||||
|
}
|
||||||
|
query
|
||||||
|
.then((response) => {
|
||||||
|
this.status = { status: "success" };
|
||||||
|
const blob = new Blob([response.data], { type: "application/pdf" });
|
||||||
|
(this.$refs.viewer as HTMLIFrameElement).src = window.URL.createObjectURL(blob);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = { status: "failed" };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -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>
|
123
src/components/admin/club/newsletter/NewsletterSyncing.vue
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
<template>
|
||||||
|
<CloudIcon v-if="syncing == 'synced'" class="w-5 h-5" />
|
||||||
|
<CloudArrowUpIcon
|
||||||
|
v-else-if="syncing == 'detectedChanges'"
|
||||||
|
class="w-5 h-5 cursor-pointer animate-bounce"
|
||||||
|
@click="syncAll"
|
||||||
|
/>
|
||||||
|
<ArrowPathIcon v-else-if="syncing == 'syncing'" class="w-5 h-5 animate-spin" />
|
||||||
|
<ExclamationTriangleIcon
|
||||||
|
v-else
|
||||||
|
class="w-5 h-5 animate-[ping_1s_ease-in-out_3] text-red-500 cursor-pointer"
|
||||||
|
@click="syncAll"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter";
|
||||||
|
import { ArrowPathIcon, CloudArrowUpIcon, CloudIcon, ExclamationTriangleIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { useNewsletterDatesStore } from "@/stores/admin/club/newsletter/newsletterDates";
|
||||||
|
import { useNewsletterRecipientsStore } from "@/stores/admin/club/newsletter/newsletterRecipients";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: ["executeSyncAll"],
|
||||||
|
watch: {
|
||||||
|
executeSyncAll() {
|
||||||
|
this.syncAll();
|
||||||
|
},
|
||||||
|
syncing() {
|
||||||
|
this.$emit("syncState", this.syncing);
|
||||||
|
},
|
||||||
|
detectedChangeNewsletter() {
|
||||||
|
clearTimeout(this.newsletterTimer);
|
||||||
|
this.setNewsletterSyncingState("synced");
|
||||||
|
if (this.detectedChangeNewsletter == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setNewsletterSyncingState("detectedChanges");
|
||||||
|
this.newsletterTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveNewsletter();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
detectedChangeNewsletterDates() {
|
||||||
|
clearTimeout(this.newsletterDatesTimer);
|
||||||
|
if (this.detectedChangeNewsletterDates == false) {
|
||||||
|
this.setNewsletterDatesSyncingState("synced");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setNewsletterDatesSyncingState("detectedChanges");
|
||||||
|
this.newsletterDatesTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveNewsletterDates();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
detectedChangeNewsletterRecipients() {
|
||||||
|
clearTimeout(this.newsletterRecipientsTimer);
|
||||||
|
this.setNewsletterRecipientsSyncingState("synced");
|
||||||
|
if (this.detectedChangeNewsletterRecipients == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setNewsletterRecipientsSyncingState("detectedChanges");
|
||||||
|
this.newsletterRecipientsTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveNewsletterRecipients();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
syncState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
||||||
|
return typeof state == "string";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
newsletterTimer: undefined as undefined | any,
|
||||||
|
newsletterDatesTimer: undefined as undefined | any,
|
||||||
|
newsletterRecipientsTimer: undefined as undefined | any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$emit("syncState", this.syncing);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (!this.newsletterTimer) clearTimeout(this.newsletterTimer);
|
||||||
|
if (!this.newsletterDatesTimer) clearTimeout(this.newsletterDatesTimer);
|
||||||
|
if (!this.newsletterRecipientsTimer) clearTimeout(this.newsletterRecipientsTimer);
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useNewsletterStore, ["syncingNewsletter", "detectedChangeNewsletter"]),
|
||||||
|
...mapState(useNewsletterDatesStore, ["syncingNewsletterDates", "detectedChangeNewsletterDates"]),
|
||||||
|
...mapState(useNewsletterRecipientsStore, ["syncingNewsletterRecipients", "detectedChangeNewsletterRecipients"]),
|
||||||
|
|
||||||
|
syncing(): "synced" | "syncing" | "detectedChanges" | "failed" {
|
||||||
|
let states = [
|
||||||
|
this.syncingNewsletter,
|
||||||
|
this.syncingNewsletterDates,
|
||||||
|
this.syncingNewsletterRecipients,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (states.includes("failed")) return "failed";
|
||||||
|
else if (states.includes("syncing")) return "syncing";
|
||||||
|
else if (states.includes("detectedChanges")) return "detectedChanges";
|
||||||
|
else return "synced";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useNewsletterStore, ["synchronizeActiveNewsletter", "setNewsletterSyncingState"]),
|
||||||
|
...mapActions(useNewsletterDatesStore, ["synchronizeActiveNewsletterDates", "setNewsletterDatesSyncingState"]),
|
||||||
|
...mapActions(useNewsletterRecipientsStore, ["synchronizeActiveNewsletterRecipients", "setNewsletterRecipientsSyncingState"]),
|
||||||
|
|
||||||
|
syncAll() {
|
||||||
|
if (!this.newsletterTimer) clearTimeout(this.newsletterTimer);
|
||||||
|
if (!this.newsletterDatesTimer) clearTimeout(this.newsletterDatesTimer);
|
||||||
|
if (!this.newsletterRecipientsTimer) clearTimeout(this.newsletterRecipientsTimer);
|
||||||
|
|
||||||
|
this.synchronizeActiveNewsletter();
|
||||||
|
this.synchronizeActiveNewsletterDates();
|
||||||
|
this.synchronizeActiveNewsletterRecipients();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
82
src/components/admin/club/protocol/CreateProtocolModal.vue
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Protokoll erstellen</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<form ref="form" class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
|
||||||
|
<div>
|
||||||
|
<label for="title">Titel</label>
|
||||||
|
<input type="text" id="title" required autocomplete="false" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="date">Datum</label>
|
||||||
|
<input type="date" id="date" required />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
|
||||||
|
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||||
|
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end">
|
||||||
|
<div class="flex flex-row gap-4 py-2">
|
||||||
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/club/protocol/protocol";
|
||||||
|
import type { CreateProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||||
|
timeout: undefined as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
try {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
...mapActions(useProtocolStore, ["createProtocol"]),
|
||||||
|
triggerCreate(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
let createProtocol: CreateProtocolViewModel = {
|
||||||
|
title: formData.title.value,
|
||||||
|
date: formData.date.value,
|
||||||
|
};
|
||||||
|
this.status = "loading";
|
||||||
|
this.createProtocol(createProtocol)
|
||||||
|
.then(() => {
|
||||||
|
this.status = { status: "success" };
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
(this.$refs.form as HTMLFormElement).reset();
|
||||||
|
this.closeModal();
|
||||||
|
}, 1500);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = { status: "failed" };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
26
src/components/admin/club/protocol/CurrentlySyncingModal.vue
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Protokoll wird noch synchronisiert</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p>Es gibt noch Daten, welche synchronisiert werden müssen.</p>
|
||||||
|
<p>Dieses PopUp entfernt sich von selbst nach erfolgreicher Synchronisierung.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/club/protocol/protocol";
|
||||||
|
import type { CreateProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({});
|
||||||
|
</script>
|
27
src/components/admin/club/protocol/ProtocolListItem.vue
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
|
||||||
|
<RouterLink
|
||||||
|
:to="{ name: 'admin-club-protocol-overview', params: { protocolId: protocol.id } }"
|
||||||
|
class="bg-primary p-2 text-white flex flex-row justify-between items-center"
|
||||||
|
>
|
||||||
|
<p>{{ protocol.title }} - {{ protocol.date }}</p>
|
||||||
|
</RouterLink>
|
||||||
|
<div class="p-2 max-h-48 overflow-y-auto">
|
||||||
|
<p v-html="protocol.summary"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import type { ProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
protocol: { type: Object as PropType<ProtocolViewModel>, default: {} },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full flex flex-col gap-2">
|
||||||
|
<div class="grow">
|
||||||
|
<iframe ref="viewer" class="w-full h-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button primary-outline class="!w-fit self-end" @click="closeModal">schließen</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import { useProtocolPrintoutStore } from "@/stores/admin/club/protocol/protocolPrintout";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
computed: {
|
||||||
|
...mapState(useModalStore, ["data"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchItem();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
...mapActions(useProtocolPrintoutStore, ["fetchProtocolPrintoutById"]),
|
||||||
|
fetchItem() {
|
||||||
|
this.fetchProtocolPrintoutById(this.data)
|
||||||
|
.then((response) => {
|
||||||
|
const blob = new Blob([response.data], { type: "application/pdf" });
|
||||||
|
(this.$refs.viewer as HTMLIFrameElement).src = window.URL.createObjectURL(blob);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
161
src/components/admin/club/protocol/ProtocolSyncing.vue
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
<template>
|
||||||
|
<CloudIcon v-if="syncing == 'synced'" class="w-5 h-5" />
|
||||||
|
<CloudArrowUpIcon
|
||||||
|
v-else-if="syncing == 'detectedChanges'"
|
||||||
|
class="w-5 h-5 cursor-pointer animate-bounce"
|
||||||
|
@click="syncAll"
|
||||||
|
/>
|
||||||
|
<ArrowPathIcon v-else-if="syncing == 'syncing'" class="w-5 h-5 animate-spin" />
|
||||||
|
<ExclamationTriangleIcon
|
||||||
|
v-else
|
||||||
|
class="w-5 h-5 animate-[ping_1s_ease-in-out_3] text-red-500 cursor-pointer"
|
||||||
|
@click="syncAll"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/club/protocol/protocol";
|
||||||
|
import { ArrowPathIcon, CloudArrowUpIcon, CloudIcon, ExclamationTriangleIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { useProtocolAgendaStore } from "@/stores/admin/club/protocol/protocolAgenda";
|
||||||
|
import { useProtocolPresenceStore } from "@/stores/admin/club/protocol/protocolPresence";
|
||||||
|
import { useProtocolDecisionStore } from "@/stores/admin/club/protocol/protocolDecision";
|
||||||
|
import { useProtocolVotingStore } from "@/stores/admin/club/protocol/protocolVoting";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: ["executeSyncAll"],
|
||||||
|
watch: {
|
||||||
|
executeSyncAll() {
|
||||||
|
this.syncAll();
|
||||||
|
},
|
||||||
|
syncing() {
|
||||||
|
this.$emit("syncState", this.syncing);
|
||||||
|
},
|
||||||
|
detectedChangeProtocol() {
|
||||||
|
clearTimeout(this.protocolTimer);
|
||||||
|
this.setProtocolSyncingState("synced");
|
||||||
|
if (this.detectedChangeProtocol == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setProtocolSyncingState("detectedChanges");
|
||||||
|
this.protocolTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveProtocol();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
detectedChangeProtocolAgenda() {
|
||||||
|
clearTimeout(this.protocolAgendaTimer);
|
||||||
|
if (this.detectedChangeProtocolAgenda == false) {
|
||||||
|
this.setProtocolAgendaSyncingState("synced");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setProtocolAgendaSyncingState("detectedChanges");
|
||||||
|
this.protocolAgendaTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveProtocolAgenda();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
detectedChangeProtocolPresence() {
|
||||||
|
clearTimeout(this.protocolPresenceTimer);
|
||||||
|
this.setProtocolPresenceSyncingState("synced");
|
||||||
|
if (this.detectedChangeProtocolPresence == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setProtocolPresenceSyncingState("detectedChanges");
|
||||||
|
this.protocolPresenceTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveProtocolPresence();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
detectedChangeProtocolDecision() {
|
||||||
|
clearTimeout(this.protocolDecisionTimer);
|
||||||
|
this.setProtocolDecisionSyncingState("synced");
|
||||||
|
if (this.detectedChangeProtocolDecision == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setProtocolDecisionSyncingState("detectedChanges");
|
||||||
|
this.protocolDecisionTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveProtocolDecision();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
detectedChangeProtocolVoting() {
|
||||||
|
clearTimeout(this.protocolVotingTimer);
|
||||||
|
this.setProtocolVotingSyncingState("synced");
|
||||||
|
if (this.detectedChangeProtocolVoting == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setProtocolVotingSyncingState("detectedChanges");
|
||||||
|
this.protocolVotingTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveProtocolVoting();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
syncState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
||||||
|
return typeof state == "string";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
protocolTimer: undefined as undefined | any,
|
||||||
|
protocolAgendaTimer: undefined as undefined | any,
|
||||||
|
protocolPresenceTimer: undefined as undefined | any,
|
||||||
|
protocolDecisionTimer: undefined as undefined | any,
|
||||||
|
protocolVotingTimer: undefined as undefined | any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$emit("syncState", this.syncing);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (!this.protocolTimer) clearTimeout(this.protocolTimer);
|
||||||
|
if (!this.protocolAgendaTimer) clearTimeout(this.protocolAgendaTimer);
|
||||||
|
if (!this.protocolPresenceTimer) clearTimeout(this.protocolPresenceTimer);
|
||||||
|
if (!this.protocolDecisionTimer) clearTimeout(this.protocolDecisionTimer);
|
||||||
|
if (!this.protocolVotingTimer) clearTimeout(this.protocolVotingTimer);
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useProtocolStore, ["syncingProtocol", "detectedChangeProtocol"]),
|
||||||
|
...mapState(useProtocolAgendaStore, ["syncingProtocolAgenda", "detectedChangeProtocolAgenda"]),
|
||||||
|
...mapState(useProtocolPresenceStore, ["syncingProtocolPresence", "detectedChangeProtocolPresence"]),
|
||||||
|
...mapState(useProtocolDecisionStore, ["syncingProtocolDecision", "detectedChangeProtocolDecision"]),
|
||||||
|
...mapState(useProtocolVotingStore, ["syncingProtocolVoting", "detectedChangeProtocolVoting"]),
|
||||||
|
|
||||||
|
syncing(): "synced" | "syncing" | "detectedChanges" | "failed" {
|
||||||
|
let states = [
|
||||||
|
this.syncingProtocol,
|
||||||
|
this.syncingProtocolAgenda,
|
||||||
|
this.syncingProtocolPresence,
|
||||||
|
this.syncingProtocolDecision,
|
||||||
|
this.syncingProtocolVoting,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (states.includes("failed")) return "failed";
|
||||||
|
else if (states.includes("syncing")) return "syncing";
|
||||||
|
else if (states.includes("detectedChanges")) return "detectedChanges";
|
||||||
|
else return "synced";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useProtocolStore, ["synchronizeActiveProtocol", "setProtocolSyncingState"]),
|
||||||
|
...mapActions(useProtocolAgendaStore, ["synchronizeActiveProtocolAgenda", "setProtocolAgendaSyncingState"]),
|
||||||
|
...mapActions(useProtocolPresenceStore, ["synchronizeActiveProtocolPresence", "setProtocolPresenceSyncingState"]),
|
||||||
|
...mapActions(useProtocolDecisionStore, ["synchronizeActiveProtocolDecision", "setProtocolDecisionSyncingState"]),
|
||||||
|
...mapActions(useProtocolVotingStore, ["synchronizeActiveProtocolVoting", "setProtocolVotingSyncingState"]),
|
||||||
|
|
||||||
|
syncAll() {
|
||||||
|
if (!this.protocolTimer) clearTimeout(this.protocolTimer);
|
||||||
|
if (!this.protocolAgendaTimer) clearTimeout(this.protocolAgendaTimer);
|
||||||
|
if (!this.protocolPresenceTimer) clearTimeout(this.protocolPresenceTimer);
|
||||||
|
if (!this.protocolDecisionTimer) clearTimeout(this.protocolDecisionTimer);
|
||||||
|
if (!this.protocolVotingTimer) clearTimeout(this.protocolVotingTimer);
|
||||||
|
|
||||||
|
this.synchronizeActiveProtocol();
|
||||||
|
this.synchronizeActiveProtocolAgenda();
|
||||||
|
this.synchronizeActiveProtocolPresence();
|
||||||
|
this.synchronizeActiveProtocolDecision();
|
||||||
|
this.synchronizeActiveProtocolVoting();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -23,7 +23,7 @@ import { mapState, mapActions } from "pinia";
|
||||||
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
import { useAbilityStore } from "@/stores/ability";
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
import type { AwardViewModel } from "@/viewmodels/admin/award.models";
|
import type { AwardViewModel } from "@/viewmodels/admin/settings/award.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -32,8 +34,8 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useAwardStore } from "@/stores/admin/award";
|
import { useAwardStore } from "@/stores/admin/settings/award";
|
||||||
import type { CreateAwardViewModel } from "@/viewmodels/admin/award.models";
|
import type { CreateAwardViewModel } from "@/viewmodels/admin/settings/award.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -29,8 +31,8 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useRoleStore } from "@/stores/admin/role";
|
import { useRoleStore } from "@/stores/admin/user/role";
|
||||||
import { useAwardStore } from "@/stores/admin/award";
|
import { useAwardStore } from "@/stores/admin/settings/award";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-fit w-full border border-primary rounded-md overflow-hidden">
|
||||||
|
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<div class="rounded-md h-5 w-5" :style="'background-color:' + calendarType.color">
|
||||||
|
<EyeIcon v-if="calendarType.nscdr" class="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<p>{{ calendarType.type }}</p>
|
||||||
|
<small v-if="calendarType.passphrase">(passwortgeschützt)</small>
|
||||||
|
<small v-if="calendarType.nscdr">(standard-Auslieferung)</small>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<RouterLink
|
||||||
|
v-if="can('update', 'settings', 'calendar_type')"
|
||||||
|
:to="{ name: 'admin-settings-calendar_type-edit', params: { id: calendarType.id } }"
|
||||||
|
>
|
||||||
|
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
||||||
|
</RouterLink>
|
||||||
|
<div v-if="can('delete', 'settings', 'calendar_type')" @click="openDeleteModal">
|
||||||
|
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent, defineAsyncComponent, markRaw, type PropType } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { PencilIcon, TrashIcon, EyeIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import type { CalendarTypeViewModel } from "@/viewmodels/admin/settings/calendarType.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
calendarType: { type: Object as PropType<CalendarTypeViewModel>, default: {} },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["openModal"]),
|
||||||
|
openDeleteModal() {
|
||||||
|
this.openModal(
|
||||||
|
markRaw(
|
||||||
|
defineAsyncComponent(() => import("@/components/admin/settings/calendarType/DeleteCalendarTypeModal.vue"))
|
||||||
|
),
|
||||||
|
this.calendarType.id
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,93 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Termintyp erstellen</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
|
||||||
|
<div>
|
||||||
|
<label for="type">Bezeichnung</label>
|
||||||
|
<input type="text" id="type" required />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<input type="color" id="color" required class="!px-1 !py-0 !w-10" />
|
||||||
|
<label for="color">Farbe</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<input type="checkbox" id="nscdr" v-model="nscdr" />
|
||||||
|
<label for="nscdr">Standard Kalender Auslieferung (optional)</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="!nscdr">
|
||||||
|
<label for="passphrase">Passphrase (optional)</label>
|
||||||
|
<input type="text" id="passphrase" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
|
||||||
|
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||||
|
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end">
|
||||||
|
<div class="flex flex-row gap-4 py-2">
|
||||||
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { useCalendarTypeStore } from "@/stores/admin/settings/calendarType";
|
||||||
|
import type { CreateCalendarTypeViewModel } from "@/viewmodels/admin/settings/calendarType.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||||
|
timeout: undefined as any,
|
||||||
|
nscdr: false as boolean,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
try {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
...mapActions(useCalendarTypeStore, ["createCalendarType"]),
|
||||||
|
triggerCreate(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
let createCalendarType: CreateCalendarTypeViewModel = {
|
||||||
|
type: formData.type.value,
|
||||||
|
color: formData.color.value,
|
||||||
|
nscdr: formData.nscdr.checked,
|
||||||
|
passphrase: formData.passphrase?.value,
|
||||||
|
};
|
||||||
|
this.status = "loading";
|
||||||
|
this.createCalendarType(createCalendarType)
|
||||||
|
.then(() => {
|
||||||
|
this.status = { status: "success" };
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
this.closeModal();
|
||||||
|
}, 1500);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = { status: "failed" };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Termintyp {{ calendarType?.type }} löschen?</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button primary :disabled="status == 'loading' || status?.status == 'success'" @click="triggerDelete">
|
||||||
|
unwiederuflich löschen
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||||
|
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end">
|
||||||
|
<div class="flex flex-row gap-4 py-2">
|
||||||
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { useCalendarTypeStore } from "@/stores/admin/settings/calendarType";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||||
|
timeout: undefined as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
try {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useModalStore, ["data"]),
|
||||||
|
...mapState(useCalendarTypeStore, ["calendarTypes"]),
|
||||||
|
calendarType() {
|
||||||
|
return this.calendarTypes.find((r) => r.id == this.data);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
...mapActions(useCalendarTypeStore, ["deleteCalendarType"]),
|
||||||
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
|
this.deleteCalendarType(this.data)
|
||||||
|
.then(() => {
|
||||||
|
this.status = { status: "success" };
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
this.closeModal();
|
||||||
|
}, 1500);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = { status: "failed" };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -4,12 +4,12 @@
|
||||||
<p>{{ communicationType.type }}</p>
|
<p>{{ communicationType.type }}</p>
|
||||||
<div class="flex flex-row">
|
<div class="flex flex-row">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-if="can('update', 'settings', 'communication')"
|
v-if="can('update', 'settings', 'communication_type')"
|
||||||
:to="{ name: 'admin-settings-communication-edit', params: { id: communicationType.id } }"
|
:to="{ name: 'admin-settings-communication_type-edit', params: { id: communicationType.id } }"
|
||||||
>
|
>
|
||||||
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<div v-if="can('delete', 'settings', 'communication')" @click="openDeleteModal">
|
<div v-if="can('delete', 'settings', 'communication_type')" @click="openDeleteModal">
|
||||||
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,7 +33,7 @@ import { mapState, mapActions } from "pinia";
|
||||||
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
import { useAbilityStore } from "@/stores/ability";
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
import type { CommunicationTypeViewModel } from "@/viewmodels/admin/communicationType.models";
|
import type { CommunicationTypeViewModel } from "@/viewmodels/admin/settings/communicationType.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -78,8 +80,8 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useCommunicationTypeStore } from "@/stores/admin/communicationType";
|
import { useCommunicationTypeStore } from "@/stores/admin/settings/communicationType";
|
||||||
import type { CreateCommunicationTypeViewModel } from "@/viewmodels/admin/communicationType.models";
|
import type { CreateCommunicationTypeViewModel } from "@/viewmodels/admin/settings/communicationType.models";
|
||||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import type { CommunicationFieldType } from "@/types/fieldTypes";
|
import type { CommunicationFieldType } from "@/types/fieldTypes";
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -29,7 +31,7 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useCommunicationTypeStore } from "@/stores/admin/communicationType";
|
import { useCommunicationTypeStore } from "@/stores/admin/settings/communicationType";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -32,8 +34,8 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useExecutivePositionStore } from "@/stores/admin/executivePosition";
|
import { useExecutivePositionStore } from "@/stores/admin/settings/executivePosition";
|
||||||
import type { CreateExecutivePositionViewModel } from "@/viewmodels/admin/executivePosition.models";
|
import type { CreateExecutivePositionViewModel } from "@/viewmodels/admin/settings/executivePosition.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -29,8 +31,8 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useRoleStore } from "@/stores/admin/role";
|
import { useRoleStore } from "@/stores/admin/user/role";
|
||||||
import { useExecutivePositionStore } from "@/stores/admin/executivePosition";
|
import { useExecutivePositionStore } from "@/stores/admin/settings/executivePosition";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { mapState, mapActions } from "pinia";
|
||||||
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
import { useAbilityStore } from "@/stores/ability";
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
import type { ExecutivePositionViewModel } from "@/viewmodels/admin/executivePosition.models";
|
import type { ExecutivePositionViewModel } from "@/viewmodels/admin/settings/executivePosition.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -32,8 +34,8 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useMembershipStatusStore } from "@/stores/admin/membershipStatus";
|
import { useMembershipStatusStore } from "@/stores/admin/settings/membershipStatus";
|
||||||
import type { CreateMembershipStatusViewModel } from "@/viewmodels/admin/membershipStatus.models";
|
import type { CreateMembershipStatusViewModel } from "@/viewmodels/admin/settings/membershipStatus.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -29,8 +31,8 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useRoleStore } from "@/stores/admin/role";
|
import { useRoleStore } from "@/stores/admin/user/role";
|
||||||
import { useMembershipStatusStore } from "@/stores/admin/membershipStatus";
|
import { useMembershipStatusStore } from "@/stores/admin/settings/membershipStatus";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { mapState, mapActions } from "pinia";
|
||||||
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
import { useAbilityStore } from "@/stores/ability";
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
import type { MembershipStatusViewModel } from "@/viewmodels/admin/membershipStatus.models";
|
import type { MembershipStatusViewModel } from "@/viewmodels/admin/settings/membershipStatus.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
<template>
|
||||||
|
<form ref="form" class="flex flex-col h-fit w-full border border-primary rounded-md" @submit.prevent="updateUsage">
|
||||||
|
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
||||||
|
<p>Newsletter bei Type "{{ comType.type }}" versenden/exportieren als</p>
|
||||||
|
<div v-if="can('create','settings','newsletter_config')" class="flex flex-row justify-end w-16">
|
||||||
|
<button v-if="status == null" type="submit" class="!p-0 !h-fit !w-fit" title="speichern">
|
||||||
|
<ArchiveBoxArrowDownIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
||||||
|
</button>
|
||||||
|
<Spinner v-else-if="status == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||||
|
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||||
|
<button type="button" class="!p-0 !h-fit !w-fit" title="zurücksetzen" @click="resetForm">
|
||||||
|
<ArchiveBoxXMarkIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col p-2 gap-2">
|
||||||
|
<div class="flex flex-row gap-2 items-center">
|
||||||
|
<select ref="config" id="config" :value="newsletterConfig?.config ?? 'def'">
|
||||||
|
<option value="def">Standard (pdf nur mit Name)</option>
|
||||||
|
<option v-for="config in configs" :key="config" :value="config">{{ config == "pdf" ? "pdf mit Adresse":config }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { ArchiveBoxArrowDownIcon, ArchiveBoxXMarkIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { useNewsletterConfigStore } from "@/stores/admin/settings/newsletterConfig";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import { NewsletterConfigType } from "@/enums/newsletterConfigType";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
import type { CommunicationTypeViewModel } from "@/viewmodels/admin/settings/communicationType.models";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
comType: { type: Object as PropType<CommunicationTypeViewModel>, default: {} },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||||
|
timeout: undefined as any,
|
||||||
|
configs: [] as Array<string>,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed:{
|
||||||
|
...mapState(useNewsletterConfigStore, ["config"]),
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
newsletterConfig() {
|
||||||
|
return this.config.find(c => c.comTypeId == this.comType.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.configs = Object.values(NewsletterConfigType);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
try {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["openModal"]),
|
||||||
|
...mapActions(useNewsletterConfigStore, ["setNewsletterConfig", "deleteNewsletterConfig"]),
|
||||||
|
updateUsage(e: any) {
|
||||||
|
const fromData = e.target.elements;
|
||||||
|
const config = fromData.config.value === "def" ? null : fromData.config.value;
|
||||||
|
|
||||||
|
this.status = "loading"
|
||||||
|
let request: Promise<AxiosResponse<any, any>>
|
||||||
|
if(config){
|
||||||
|
request = this.setNewsletterConfig({
|
||||||
|
comTypeId: this.comType.id,
|
||||||
|
config: config
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
request = this.deleteNewsletterConfig(this.comType.id)
|
||||||
|
}
|
||||||
|
request.then(() => {
|
||||||
|
this.status = { status: "success" };
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
this.status = null;
|
||||||
|
}, 2000);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = { status: "failed" };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetForm() {
|
||||||
|
(this.$refs.config as HTMLSelectElement).value = String(this.newsletterConfig?.config ?? "def");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -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>
|
||||||
|
@ -36,8 +38,8 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useQualificationStore } from "@/stores/admin/qualification";
|
import { useQualificationStore } from "@/stores/admin/settings/qualification";
|
||||||
import type { CreateQualificationViewModel } from "@/viewmodels/admin/qualification.models";
|
import type { CreateQualificationViewModel } from "@/viewmodels/admin/settings/qualification.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -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" };
|
||||||
|
|