refactor: Projektstruktur in app/ und backend/ aufgeteilt
This commit is contained in:
8
backend/.gitignore
vendored
Normal file
8
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/target/
|
||||
/node_modules/
|
||||
/src/main/frontend/generated/
|
||||
/vite.generated.ts
|
||||
/logs/
|
||||
/.env
|
||||
*.log
|
||||
.DS_Store
|
||||
19
backend/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
19
backend/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
wrapperVersion=3.3.2
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||
6
backend/.prettierrc.json
Normal file
6
backend/.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"bracketSameLine": true
|
||||
}
|
||||
|
||||
12
backend/Dockerfile
Normal file
12
backend/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM eclipse-temurin:21-jre
|
||||
|
||||
ARG JAR_FILE=target/*.jar
|
||||
|
||||
# Zeitzone auf Berlin setzen und 24h-Format konfigurieren
|
||||
ENV TZ=Europe/Berlin
|
||||
ENV LC_TIME=de_DE.UTF-8
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
COPY ${JAR_FILE} app.jar
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java", "-jar", "/app.jar", "--spring.profiles.active=production"]
|
||||
441
backend/HANDBUCH.md
Normal file
441
backend/HANDBUCH.md
Normal file
@@ -0,0 +1,441 @@
|
||||
# VotianLT - Benutzerhandbuch
|
||||
|
||||
**Version:** 1.0
|
||||
**Stand:** Februar 2026
|
||||
**Anwendung:** VotianLT - Ihr digitaler Transportpartner
|
||||
|
||||
---
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
|
||||
1. [Einleitung](#1-einleitung)
|
||||
2. [Registrierung und Anmeldung](#2-registrierung-und-anmeldung)
|
||||
3. [Dashboard](#3-dashboard)
|
||||
4. [Auftragsverwaltung](#4-auftragsverwaltung)
|
||||
5. [Kundenverwaltung](#5-kundenverwaltung)
|
||||
6. [App-Nutzer verwalten](#6-app-nutzer-verwalten)
|
||||
7. [Nachrichten](#7-nachrichten)
|
||||
8. [Rechnungen](#8-rechnungen)
|
||||
9. [KI-Statistiken](#9-ki-statistiken)
|
||||
10. [Profil bearbeiten](#10-profil-bearbeiten)
|
||||
11. [Administration](#11-administration)
|
||||
|
||||
---
|
||||
|
||||
## 1. Einleitung
|
||||
|
||||
VotianLT ist eine webbasierte Anwendung zur digitalen Verwaltung von Transportaufträgen. Die Software richtet sich an Solo-Selbstständige und Kleinunternehmer im Transportgewerbe und bietet folgende Kernfunktionen:
|
||||
|
||||
- **Auftragserstellung und -verwaltung** mit vollständiger Abwicklung von der Erstellung bis zur Fertigstellung
|
||||
- **Digitale Auftragsbearbeitung** über eine mobile App für Fahrer und Mitarbeiter im Außendienst
|
||||
- **Echtzeit-Kommunikation** zwischen Disponent und App-Nutzern per Chat
|
||||
- **Rechnungserstellung** für abgeschlossene Aufträge
|
||||
- **KI-gestützte Statistiken** zur Auswertung von Geschäftsdaten
|
||||
- **GPS-Tracking** zur Nachverfolgung von Lieferungen in Echtzeit
|
||||
|
||||
---
|
||||
|
||||
## 2. Registrierung und Anmeldung
|
||||
|
||||
### 2.1 Neues Konto registrieren
|
||||
|
||||
1. Öffnen Sie die Startseite und klicken Sie auf **"Jetzt kostenlos testen"** oder **"Registrieren"**.
|
||||
2. Füllen Sie das Registrierungsformular aus:
|
||||
- **Firmenname** (Pflichtfeld)
|
||||
- **E-Mail-Adresse** (wird als Benutzername verwendet)
|
||||
- **Passwort** und **Passwort bestätigen** (mindestens 6 Zeichen)
|
||||
- **Vorname** und **Nachname**
|
||||
- **Telefonnummer**
|
||||
- **Anschrift** (Straße, Hausnummer, PLZ, Ort)
|
||||
3. Klicken Sie auf **"Registrieren"**. Ein 6-stelliger Bestätigungscode wird an Ihre E-Mail-Adresse gesendet.
|
||||
4. Geben Sie den Code im angezeigten Feld ein und klicken Sie auf **"Code prüfen und registrieren"**.
|
||||
5. Nach erfolgreicher Registrierung werden Sie zur Anmeldeseite weitergeleitet.
|
||||
|
||||
> **Hinweis:** Der Bestätigungscode ist 10 Minuten gültig. Über den Button **"Code erneut senden"** können Sie nach 60 Sekunden einen neuen Code anfordern.
|
||||
|
||||
### 2.2 Anmelden
|
||||
|
||||
1. Geben Sie auf der Anmeldeseite Ihre **E-Mail-Adresse** und Ihr **Passwort** ein.
|
||||
2. Klicken Sie auf **"Anmelden"**.
|
||||
3. Falls die Zwei-Faktor-Authentifizierung (2FA) aktiviert ist, wird ein 6-stelliger Code an Ihre E-Mail gesendet. Geben Sie diesen im angezeigten Feld ein und klicken Sie auf **"Code prüfen"**.
|
||||
|
||||
### 2.3 Passwort zurücksetzen
|
||||
|
||||
1. Klicken Sie auf der Anmeldeseite auf **"Passwort vergessen?"**.
|
||||
2. Geben Sie Ihre registrierte E-Mail-Adresse ein und klicken Sie auf **"E-Mail senden"**.
|
||||
3. Sie erhalten eine E-Mail mit einem Link zum Zurücksetzen. Klicken Sie auf den Link.
|
||||
4. Vergeben Sie ein neues Passwort und bestätigen Sie es. Klicken Sie auf **"Passwort speichern"**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Dashboard
|
||||
|
||||
Nach der Anmeldung gelangen Sie zum Dashboard. Es zeigt eine Willkommensnachricht und eine Übersicht der wichtigsten Funktionen:
|
||||
|
||||
- **Einrichtungsassistent** - Hilfe beim Einstieg in die Anwendung
|
||||
- **Kunden- und Auftragsverwaltung** - Schnellzugriff auf die Verwaltungsbereiche
|
||||
- **Auftragserstellung** - Direkter Link zur Erstellung neuer Aufträge
|
||||
|
||||
Die **Hauptnavigation** am linken Rand bietet Zugriff auf alle Bereiche der Anwendung.
|
||||
|
||||
---
|
||||
|
||||
## 4. Auftragsverwaltung
|
||||
|
||||
### 4.1 Aufträge anzeigen
|
||||
|
||||
Öffnen Sie den Bereich **"Aufträge"** über die Navigation oder den Pfad `/jobs`.
|
||||
|
||||
**Filtermöglichkeiten:**
|
||||
- **Zeitraum**: Wählen Sie Start- und Enddatum (Standard: letzte 30 Tage)
|
||||
- **Auftragsnummer**: Suchen Sie nach einer bestimmten Auftragsnummer
|
||||
- **Status**: Filtern Sie nach "Alle", "Offen" oder "Erledigt"
|
||||
- *Offen* umfasst: Erstellt, In Bearbeitung, Abholung geplant, Abgeholt, In Zustellung
|
||||
- *Erledigt* umfasst: Zugestellt, Abgeschlossen, Storniert
|
||||
|
||||
Klicken Sie auf **"Anwenden"**, um die Filter zu übernehmen.
|
||||
|
||||
**Verfügbare Aktionen pro Auftrag:**
|
||||
|
||||
| Symbol | Aktion | Sichtbar wenn |
|
||||
|--------|--------|---------------|
|
||||
| Häkchen | Auftrag manuell abschließen | Nicht-digitale Aufträge, die noch nicht abgeschlossen sind |
|
||||
| Dollar | Rechnung erstellen | Auftrag ist abgeschlossen |
|
||||
| Papierkorb | Auftrag löschen | Auftrag ist nicht abgeschlossen/storniert/zugestellt |
|
||||
|
||||
**CSV-Export:** Klicken Sie auf **"CSV Export"**, um die aktuell gefilterte Auftragsliste als CSV-Datei herunterzuladen.
|
||||
|
||||
Klicken Sie auf eine Zeile, um die **Auftragszusammenfassung** zu öffnen.
|
||||
|
||||
### 4.2 Neuen Auftrag anlegen
|
||||
|
||||
Navigieren Sie zu **"Auftragserstellung"** oder dem Pfad `/add_job`. Das Formular ist in fünf Tabs unterteilt:
|
||||
|
||||
#### Tab 1: Auftraggeber & Adressen
|
||||
|
||||
1. **Auftraggeber wählen**: Wählen Sie aus der Dropdown-Liste einen bestehenden Kunden. Die Abholadresse wird automatisch mit den Kundendaten vorbefüllt.
|
||||
2. **Abholadresse**: Füllen Sie alle Pflichtfelder aus (Firma, Anrede, Vorname, Nachname, Telefon, Straße, Hausnummer, PLZ, Ort). Optional können Sie die Adresse für zukünftige Aufträge speichern.
|
||||
3. **Zustelladresse**: Geben Sie die Lieferadresse ein. Firmennamen werden per Autovervollständigung vorgeschlagen.
|
||||
4. Mit dem Button **"Vorbelegte Adressfelder leeren"** können Sie alle vorausgefüllten Felder zurücksetzen.
|
||||
|
||||
> **Hinweis:** Beim Wechsel zum nächsten Tab werden die Adressen automatisch validiert und eine Routenberechnung (Entfernung und Dauer) durchgeführt. Bei ungültigen Adressen wird ein Hinweisdialog angezeigt.
|
||||
|
||||
#### Tab 2: Termine & Verarbeitung
|
||||
|
||||
1. **Abholtermin**: Wählen Sie Datum und Uhrzeit für die Abholung.
|
||||
2. **Zustelltermin**: Wählen Sie Datum und Uhrzeit für die Zustellung.
|
||||
3. **Digitale Abwicklung per App**: Aktivieren Sie diese Option, wenn der Auftrag über die mobile App abgewickelt werden soll.
|
||||
4. **App-Nutzer**: Wählen Sie den zuständigen App-Nutzer aus (nur sichtbar bei digitaler Abwicklung).
|
||||
|
||||
#### Tab 3: Ladung
|
||||
|
||||
1. Klicken Sie auf **"Ladung hinzufügen"**, um eine neue Ladungsposition anzulegen.
|
||||
2. Für jede Position geben Sie ein:
|
||||
- **Beschreibung** der Ladung
|
||||
- **Menge**
|
||||
- **Maße** (Länge, Breite, Höhe in cm)
|
||||
- **Gewicht** in kg
|
||||
3. Über das Papierkorb-Symbol können Sie einzelne Positionen entfernen.
|
||||
|
||||
#### Tab 4: Aufgaben (nur bei digitaler Abwicklung)
|
||||
|
||||
Aufgaben definieren, welche Schritte der App-Nutzer bei der Ausführung des Auftrags erledigen muss.
|
||||
|
||||
**Verfügbare Aufgabentypen:**
|
||||
|
||||
| Typ | Beschreibung |
|
||||
|-----|-------------|
|
||||
| Bestätigung | Der App-Nutzer bestätigt eine Aktion (z.B. "Ware übernommen") |
|
||||
| Unterschrift | Einholen einer digitalen Unterschrift |
|
||||
| Foto | Aufnahme eines Fotos (z.B. Liefernachweis) |
|
||||
| Barcode | Scannen eines Barcodes |
|
||||
| Checkliste | Abarbeiten einer Aufgabenliste |
|
||||
| Kommentar | Freitextfeld für Anmerkungen |
|
||||
|
||||
1. Optional: Wählen Sie eine **Aufgabenvorlage** aus der Dropdown-Liste, um vordefinierte Aufgaben zu laden.
|
||||
2. Klicken Sie auf **"Aufgabe hinzufügen"** und wählen Sie den gewünschten Typ.
|
||||
3. Geben Sie eine Beschreibung für die Aufgabe ein.
|
||||
4. Die Reihenfolge der Aufgaben kann per **Drag-and-Drop** angepasst werden.
|
||||
|
||||
#### Tab 5: Leistungen und Preis
|
||||
|
||||
1. **Pflichtleistungen** werden automatisch geladen.
|
||||
2. Über die Dropdown-Liste können Sie weitere **Leistungen hinzufügen**.
|
||||
3. Die Berechnung von **Netto**, **USt.** und **Brutto** erfolgt automatisch.
|
||||
4. Die **Routeninformationen** (Entfernung und Fahrtdauer) werden aus der Adressvalidierung angezeigt.
|
||||
5. Im Feld **Bemerkung** können Sie zusätzliche Anmerkungen zum Auftrag hinterlegen.
|
||||
|
||||
Klicken Sie abschließend auf **"Auftrag anlegen"**, um den Auftrag zu erstellen. Bei digitaler Abwicklung wird der Auftrag automatisch an den ausgewählten App-Nutzer übermittelt.
|
||||
|
||||
### 4.3 Auftragszusammenfassung
|
||||
|
||||
Die Zusammenfassung zeigt alle Details eines Auftrags auf einen Blick:
|
||||
|
||||
- **Abholinformationen**: Adresse, Ansprechpartner, Termin
|
||||
- **Zustellinformationen**: Adresse, Ansprechpartner, Termin
|
||||
- **Aufgaben**: Übersicht aller Aufgaben mit Erledigungsstatus. Abgeschlossene Aufgaben zeigen das Ergebnis (Foto, Barcode-Wert, Unterschrift-Vorschau etc.)
|
||||
- **Ladung**: Liste aller Ladungspositionen mit Maßen und Gewicht
|
||||
- **Auftragsinformationen**: Preis, Bemerkung, Verarbeitungsart, zugewiesener App-Nutzer
|
||||
- **Routenkarte**: Google Maps mit der Route von Abhol- zu Zustelladresse. Bei aktiver digitaler Abwicklung wird die aktuelle **GPS-Position des App-Nutzers** in Echtzeit angezeigt.
|
||||
|
||||
**Verfügbare Aktionen:**
|
||||
- **"Nachricht senden"**: Öffnet den Chat mit dem zugewiesenen App-Nutzer im Kontext dieses Auftrags
|
||||
- **"Job Historie"**: Zeigt den vollständigen Änderungsverlauf des Auftrags
|
||||
- **"Auftrag manuell abschließen"**: Schließt nicht-digitale Aufträge manuell ab (nach Bestätigung)
|
||||
|
||||
### 4.4 Auftragsverlauf (Job Historie)
|
||||
|
||||
Die Job Historie zeigt eine chronologische Zeitleiste aller Ereignisse eines Auftrags:
|
||||
|
||||
- **Statusänderungen** (z.B. "Erstellt", "In Bearbeitung", "Zugestellt")
|
||||
- **Aufgabenabschlüsse** mit zugehörigen Daten (Fotos, Barcodes, Unterschriften)
|
||||
- **Änderungen** an Auftragsdaten
|
||||
|
||||
Jeder Eintrag zeigt den **Zeitstempel**, die **Art der Änderung** und ggf. **zugehörige Daten** (z.B. Foto-Vorschau).
|
||||
|
||||
---
|
||||
|
||||
## 5. Kundenverwaltung
|
||||
|
||||
### 5.1 Kunden anzeigen
|
||||
|
||||
Navigieren Sie zu **"Kunden"** (`/customers`). Die Kundenliste zeigt alle Ihre Kunden mit folgenden Spalten:
|
||||
|
||||
- Firma, Name, E-Mail, Telefon, Straße, Ort
|
||||
|
||||
Die Liste ist **sortierbar** (Klick auf Spaltenüberschrift). Klicken Sie auf einen Eintrag, um den Kunden zu bearbeiten.
|
||||
|
||||
### 5.2 Neuen Kunden anlegen
|
||||
|
||||
1. Klicken Sie auf **"Kunde hinzufügen"** oder navigieren Sie zu `/add-customer`.
|
||||
2. Füllen Sie das Formular aus:
|
||||
- **Firmenname** (Pflichtfeld)
|
||||
- **Anrede** (Herr / Frau / Divers)
|
||||
- **Vorname** und **Nachname** (Pflichtfelder)
|
||||
- **Telefon** (Pflichtfeld)
|
||||
- **Fax** (optional)
|
||||
- **E-Mail** (Pflichtfeld, wird auf Gültigkeit geprüft)
|
||||
- **Straße** und **Hausnummer** (Pflichtfelder)
|
||||
- **Adresszusatz** (optional)
|
||||
- **PLZ** und **Ort** (Pflichtfelder)
|
||||
3. Klicken Sie auf **"Kunden anlegen"**.
|
||||
|
||||
### 5.3 Kunden bearbeiten
|
||||
|
||||
1. Klicken Sie in der Kundenliste auf den gewünschten Kunden.
|
||||
2. Ändern Sie die gewünschten Felder.
|
||||
3. Klicken Sie auf **"Speichern"**, um die Änderungen zu übernehmen.
|
||||
4. Über **"Löschen"** können Sie den Kunden nach Bestätigung endgültig entfernen.
|
||||
|
||||
---
|
||||
|
||||
## 6. App-Nutzer verwalten
|
||||
|
||||
App-Nutzer sind Fahrer oder Mitarbeiter, die die mobile VotianLT-App verwenden, um Aufträge digital abzuwickeln.
|
||||
|
||||
### 6.1 App-Nutzer anzeigen
|
||||
|
||||
Navigieren Sie zu **"App-Nutzer"** (`/app-user`). Die Liste zeigt alle Ihre App-Nutzer mit:
|
||||
|
||||
- Bezeichnung (Kennzeichen/Code), Vorname, Nachname, Telefon, App-Code, E-Mail
|
||||
|
||||
### 6.2 Neuen App-Nutzer anlegen
|
||||
|
||||
1. Klicken Sie auf **"Neuen App-Nutzer anlegen"**.
|
||||
2. Füllen Sie das Formular aus:
|
||||
- **Bezeichnung** (z.B. Fahrzeugkennzeichen im Format "HH H 000", Pflichtfeld)
|
||||
- **Vorname** und **Nachname** (Pflichtfelder)
|
||||
- **Telefon** (Pflichtfeld)
|
||||
- **E-Mail** (Pflichtfeld, muss eindeutig sein)
|
||||
- **Passwort** und **Passwort bestätigen** (mindestens 6 Zeichen)
|
||||
3. Klicken Sie auf **"App-Nutzer anlegen"**.
|
||||
|
||||
> **Hinweis:** Der App-Nutzer meldet sich in der mobilen App mit seiner E-Mail-Adresse und dem vergebenen Passwort an.
|
||||
|
||||
### 6.3 App-Nutzer bearbeiten
|
||||
|
||||
1. Klicken Sie in der Liste auf den gewünschten App-Nutzer.
|
||||
2. Ändern Sie die gewünschten Felder.
|
||||
3. **Passwort ändern**: Lassen Sie die Passwortfelder leer, wenn das Passwort nicht geändert werden soll. Füllen Sie beide Felder aus, um ein neues Passwort zu vergeben.
|
||||
4. Klicken Sie auf **"Speichern"**.
|
||||
5. Über **"Löschen"** können Sie den App-Nutzer nach Bestätigung entfernen.
|
||||
|
||||
---
|
||||
|
||||
## 7. Nachrichten
|
||||
|
||||
Das Nachrichtensystem ermöglicht die Echtzeit-Kommunikation zwischen Ihnen (Disponent) und Ihren App-Nutzern.
|
||||
|
||||
### 7.1 Nachrichtenübersicht
|
||||
|
||||
Navigieren Sie zu **"Nachrichten"** (`/messages`). Die Übersicht zeigt alle Ihre App-Nutzer als Kontakte mit:
|
||||
|
||||
- **Status-Anzeige** (farbiger Punkt bei ungelesenen Nachrichten)
|
||||
- **Name** und **E-Mail** des App-Nutzers
|
||||
- **Gesamtanzahl** der Nachrichten
|
||||
- **Ungelesene Nachrichten** (fett hervorgehoben)
|
||||
- **Letzte Nachricht** (Vorschau, Datum und Uhrzeit)
|
||||
|
||||
> **Hinweis:** Die Übersicht aktualisiert sich automatisch. Bei eingehenden Nachrichten ertönt ein **Benachrichtigungston** und es wird eine **Browser-Benachrichtigung** angezeigt (sofern Benachrichtigungen im Browser erlaubt sind).
|
||||
|
||||
### 7.2 Konversationen eines App-Nutzers
|
||||
|
||||
Klicken Sie auf einen App-Nutzer, um dessen Konversationen zu sehen:
|
||||
|
||||
- **Allgemeine Nachrichten**: Ein allgemeiner Chat ohne Auftragsbezug
|
||||
- **Nachrichten zu Aufträgen**: Separate Konversationen für jeden Auftrag (gruppiert nach Auftragsnummer)
|
||||
|
||||
Jede Konversation zeigt eine Vorschau, den Zeitpunkt der letzten Nachricht und die Anzahl ungelesener Nachrichten.
|
||||
|
||||
### 7.3 Chat / Nachrichtenverlauf
|
||||
|
||||
Öffnen Sie eine Konversation, um den Chatverlauf zu sehen. Die Darstellung ähnelt bekannten Messenger-Apps:
|
||||
|
||||
- **Ihre Nachrichten** (vom Disponenten): rechts, grüner Hintergrund
|
||||
- **Nachrichten des App-Nutzers**: links, weißer Hintergrund
|
||||
- **Datumstrenner** zwischen verschiedenen Tagen
|
||||
- **Zeitstempel** bei jeder Nachricht
|
||||
|
||||
**Nachricht senden:**
|
||||
1. Geben Sie Ihre Nachricht im Textfeld am unteren Rand ein.
|
||||
2. Klicken Sie auf **"Senden"**.
|
||||
|
||||
**Bild senden:**
|
||||
1. Klicken Sie auf das **Büroklammer-Symbol** neben dem Textfeld.
|
||||
2. Wählen Sie ein Bild aus (PNG, JPEG, GIF oder WebP, max. 32 MB).
|
||||
3. Eine Vorschau wird angezeigt. Bestätigen Sie den Versand.
|
||||
|
||||
> **Hinweis:** Neue Nachrichten erscheinen in Echtzeit. Die Ansicht scrollt automatisch zur neuesten Nachricht. Geöffnete Nachrichten werden automatisch als gelesen markiert.
|
||||
|
||||
---
|
||||
|
||||
## 8. Rechnungen
|
||||
|
||||
### 8.1 Rechnung für einen Auftrag erstellen
|
||||
|
||||
1. Navigieren Sie zur **Auftragsliste** (`/jobs`).
|
||||
2. Filtern Sie nach Status **"Erledigt"**.
|
||||
3. Klicken Sie auf das **Dollar-Symbol** neben dem gewünschten Auftrag.
|
||||
4. Das Rechnungsformular zeigt:
|
||||
- **Auftragsdetails** (Abhol- und Zustelladresse, Termine)
|
||||
- **Leistungsdaten**: Geben Sie Kilometer und Zeitaufwand ein
|
||||
- **Leistungen**: Wählen Sie die abzurechnenden Leistungen aus Ihrem Leistungskatalog
|
||||
- **Zusammenfassung**: Automatische Berechnung von Netto, USt. und Brutto
|
||||
5. Klicken Sie auf **"Rechnung erstellen"**.
|
||||
|
||||
### 8.2 Meine Rechnungen
|
||||
|
||||
Unter **"Meine Rechnungen"** (`/my-invoices`) finden Sie eine Übersicht aller erstellten Rechnungen:
|
||||
|
||||
- **Statusanzeige**: Offen (gelb), Bezahlt (grün), Überfällig (rot)
|
||||
- **Rechnungsnummer**, **Datum**, **Betrag**
|
||||
- **Suchfunktion** zum Filtern nach Rechnungsnummer, Status oder Betrag
|
||||
|
||||
Klicken Sie auf eine Rechnung, um die **PDF-Version** herunterzuladen.
|
||||
|
||||
---
|
||||
|
||||
## 9. KI-Statistiken
|
||||
|
||||
Der KI-Statistik-Assistent (`/statistics`) bietet eine interaktive Auswertung Ihrer Geschäftsdaten per Chat-Schnittstelle.
|
||||
|
||||
### Verwendung
|
||||
|
||||
1. Navigieren Sie zu **"KI-Statistiken"**.
|
||||
2. Verwenden Sie die **Schnellaktionen** für häufige Abfragen:
|
||||
- **"Aufträge zählen"**: Zeigt die Gesamtanzahl Ihrer Aufträge
|
||||
- **"Umsatz"**: Berechnet Ihren Gesamtumsatz
|
||||
- **"Monatstrend"**: Zeigt die monatliche Entwicklung als Diagramm
|
||||
3. Oder geben Sie eine **freie Frage** in das Textfeld ein, z.B.:
|
||||
- "Wie viele Aufträge hatte ich im Januar?"
|
||||
- "Zeige mir den Umsatz der letzten 6 Monate als Diagramm"
|
||||
- "Welcher Kunde hat die meisten Aufträge?"
|
||||
|
||||
Der Assistent analysiert Ihre Daten und antwortet mit **Text** und/oder **interaktiven Diagrammen** (Balken-, Linien-, Kreis- und weitere Diagrammtypen).
|
||||
|
||||
---
|
||||
|
||||
## 10. Profil bearbeiten
|
||||
|
||||
Unter **"Profil bearbeiten"** (`/edit-profile`) können Sie Ihre persönlichen Daten und Rechnungseinstellungen verwalten. Die Seite ist in drei Tabs unterteilt:
|
||||
|
||||
### Tab 1: Persönliche Daten / Kontakt
|
||||
|
||||
- **Firmendaten**: Firmenname (Pflicht), Firmenzusatz
|
||||
- **Persönliche Daten**: Vorname, Nachname
|
||||
- **Kontakt**: Telefon (Pflicht), Fax, Mobil, E-Mail (Pflicht)
|
||||
- **Anschrift**: Straße, Hausnummer (Pflicht), Adresszusatz, PLZ, Ort (Pflicht)
|
||||
- **Abweichende Rechnungsadresse**: Separate Adressfelder für die Rechnungsstellung
|
||||
- **Digitale Abwicklung per App vorausgewählt**: Wenn aktiviert, wird bei neuen Aufträgen die digitale Abwicklung standardmäßig vorausgewählt
|
||||
- **Zwei-Faktor-Authentifizierung (2FA)**: Aktivieren/deaktivieren Sie die zusätzliche Sicherheitsabfrage beim Login
|
||||
|
||||
### Tab 2: Rechnungsdaten
|
||||
|
||||
- **Rechnungspräfix** (z.B. "VLT" - wird der Rechnungsnummer vorangestellt)
|
||||
- **USt-IdNr.** und **Steuernummer**
|
||||
- **Bankverbindung**: Bankname und IBAN
|
||||
- **Steuersatz** (z.B. 19%)
|
||||
- **Einleitungstext**: Freitext, der auf Rechnungen als Einleitung erscheint
|
||||
- **AGB/Zahlungsbedingungen**: Text für die Rechnungsfußzeile
|
||||
- **Logo hochladen**: Laden Sie Ihr Firmenlogo hoch, das auf Rechnungen angezeigt wird
|
||||
- **Abrechnung aktiviert**: Aktivieren Sie die Rechnungsfunktion
|
||||
|
||||
### Tab 3: Leistungen / Preisliste
|
||||
|
||||
Verwalten Sie Ihren **Leistungskatalog**, der bei der Auftragserstellung und Rechnungsstellung verwendet wird:
|
||||
|
||||
1. Klicken Sie auf **"Leistung hinzufügen"**, um eine neue Leistung anzulegen.
|
||||
2. Wählen Sie die **Leistungsbezeichnung** aus oder geben Sie eine neue ein.
|
||||
3. Geben Sie den **Preis** und den **USt.-Satz** ein.
|
||||
4. Über das Papierkorb-Symbol können Sie einzelne Leistungen entfernen.
|
||||
|
||||
Rechts neben dem Formular wird eine **Live-Vorschau** Ihrer Rechnungsvorlage angezeigt, die sich bei Änderungen automatisch aktualisiert.
|
||||
|
||||
---
|
||||
|
||||
## 11. Administration
|
||||
|
||||
Die folgenden Bereiche stehen nur Benutzern mit Administratorrechten zur Verfügung.
|
||||
|
||||
### 11.1 Admin Dashboard
|
||||
|
||||
Das Admin Dashboard (`/admin-dashboard`) bietet eine Gesamtübersicht des Systems:
|
||||
|
||||
- **System-Übersicht**: Anzahl Aufträge, Benutzer, App-Benutzer
|
||||
- **Job-Statistiken**: Offene, in Bearbeitung befindliche und abgeschlossene Aufträge sowie Frachtgüter
|
||||
- **Aufgaben-Statistiken**: Gesamtanzahl, abgeschlossene und offene Aufgaben mit Erfolgsquote
|
||||
- **Benutzer-Aktivität**: Anzahl Fotos, Barcodes, Unterschriften und Kommentare
|
||||
- **System-Status**: Datenbankverbindung, WebSocket-Status, Anwendungsstatus, Speicherverbrauch
|
||||
|
||||
### 11.2 Preis-Tabelle
|
||||
|
||||
Unter **"Preis-Tabelle"** (`/admin-price-table`) können Administratoren die Systempreise konfigurieren:
|
||||
|
||||
- **Monatliche Grundpauschale**
|
||||
- **App-Nutzungslizenz**
|
||||
- **Umsatzbeteiligung** (in Prozent)
|
||||
|
||||
### 11.3 Rechnungsgenerator
|
||||
|
||||
Der Rechnungsgenerator (`/invoice-generator`) ist ein visueller Editor zum Erstellen von Rechnungsvorlagen:
|
||||
|
||||
- **Drag-and-Drop** von Vorlagenblöcken (Textfeld, Überschrift, Datum, Kundeninfo, Firmeninfo, Betrag, Linie, Bild) auf eine Arbeitsfläche
|
||||
- **Eigenschaften-Panel** zur Bearbeitung des ausgewählten Elements
|
||||
- **Funktionen**: Speichern, Laden, PDF-Vorschau, Template-Verwaltung
|
||||
|
||||
### 11.4 PDF-Test
|
||||
|
||||
Unter `/pdf-test` können Administratoren die PDF-Generierung testen:
|
||||
|
||||
- **System-Rechnung generieren**: Erzeugt eine PDF aus der System-Rechnungsvorlage
|
||||
- **Kunden-Rechnung generieren**: Erzeugt eine PDF aus der Kunden-Rechnungsvorlage
|
||||
|
||||
---
|
||||
|
||||
## Impressum
|
||||
|
||||
Das Impressum ist unter `/impressum` einsehbar und enthält die gesetzlich vorgeschriebenen Angaben des Betreibers.
|
||||
|
||||
---
|
||||
|
||||
*Dieses Handbuch beschreibt den Funktionsumfang von VotianLT. Bei Fragen oder Problemen wenden Sie sich bitte an den Support.*
|
||||
BIN
backend/HANDBUCH.pdf
Normal file
BIN
backend/HANDBUCH.pdf
Normal file
Binary file not shown.
1410
backend/STYLEGUIDE.md
Normal file
1410
backend/STYLEGUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
82
backend/docker_push.sh
Executable file
82
backend/docker_push.sh
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
readonly SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly REGISTRY_IMAGE="registry.assecutor.org/votianlt"
|
||||
readonly BACKEND_DIR="${SCRIPT_DIR}/backend"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Verwendung:
|
||||
./docker_push.sh [x.y.z]
|
||||
|
||||
Beispiel:
|
||||
./docker_push.sh 0.9.13
|
||||
./docker_push.sh
|
||||
|
||||
Voraussetzungen:
|
||||
- Docker Buildx ist installiert
|
||||
- Login zur Registry wurde bereits ausgeführt:
|
||||
docker login registry.assecutor.org
|
||||
|
||||
Ohne Versionsargument wird automatisch die Version aus der pom.xml verwendet.
|
||||
EOF
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "Fehler: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "'$1' wurde nicht gefunden."
|
||||
}
|
||||
|
||||
resolve_pom_version() {
|
||||
[[ -x "${BACKEND_DIR}/mvnw" ]] || fail "'${BACKEND_DIR}/mvnw' wurde nicht gefunden oder ist nicht ausführbar."
|
||||
|
||||
local version
|
||||
version="$(
|
||||
cd "${BACKEND_DIR}" && ./mvnw -q -DforceStdout help:evaluate -Dexpression=project.version \
|
||||
| awk 'NF { last = $0 } END { print last }'
|
||||
)"
|
||||
|
||||
[[ -n "${version}" ]] || fail "Version konnte nicht aus der pom.xml ermittelt werden."
|
||||
echo "${version}"
|
||||
}
|
||||
|
||||
VERSION="${1:-$(resolve_pom_version)}"
|
||||
|
||||
if [[ "${VERSION}" == "-h" || "${VERSION}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
fail "Versionsnummer muss das Format x.y.z haben."
|
||||
fi
|
||||
|
||||
require_command docker
|
||||
docker buildx version >/dev/null 2>&1 || fail "Docker Buildx ist nicht verfügbar."
|
||||
|
||||
cd "${SCRIPT_DIR}"
|
||||
|
||||
echo "Verwende Release-Version ${VERSION}."
|
||||
echo "Baue Production-JAR für Version ${VERSION} ..."
|
||||
(cd "${BACKEND_DIR}" && ./mvnw -Pproduction -DskipTests -Drevision="${VERSION}" package)
|
||||
|
||||
JAR_FILE_REL="target/votianlt-${VERSION}.jar"
|
||||
JAR_FILE_ABS="${BACKEND_DIR}/${JAR_FILE_REL}"
|
||||
[[ -f "${JAR_FILE_ABS}" ]] || fail "Release-JAR wurde nicht gefunden: ${JAR_FILE_ABS}"
|
||||
|
||||
echo "Pushe Image ${REGISTRY_IMAGE}:${VERSION} ..."
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
-f "${BACKEND_DIR}/Dockerfile" \
|
||||
--build-arg "JAR_FILE=${JAR_FILE_REL}" \
|
||||
-t "${REGISTRY_IMAGE}:${VERSION}" \
|
||||
--push \
|
||||
"${BACKEND_DIR}"
|
||||
|
||||
echo "Fertig: ${REGISTRY_IMAGE}:${VERSION}"
|
||||
283
backend/eclipse-formatter.xml
Normal file
283
backend/eclipse-formatter.xml
Normal file
@@ -0,0 +1,283 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<profiles version="13">
|
||||
<profile kind="CodeFormatterProfile" name="Vaadin Java Conventions 20241010" version="13">
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.source" value="17"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="120"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="8"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="@formatter:on"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.problem.assertIdentifier" value="error"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_method_declaration" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.problem.enumIdentifier" value="error"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.compliance" value="17"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode" value="enabled"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_label" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="80"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.wrap_before_binary_operator" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="17"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="0"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="48"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
|
||||
<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
|
||||
</profile>
|
||||
</profiles>
|
||||
159
backend/flutter_websocket_test.html
Normal file
159
backend/flutter_websocket_test.html
Normal file
@@ -0,0 +1,159 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Flutter WebSocket Test</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.0/bundles/stomp.umd.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Flutter WebSocket STOMP Test</h1>
|
||||
<div id="status">Nicht verbunden</div>
|
||||
<button onclick="connectWebSocket()">WebSocket Verbinden</button>
|
||||
<button onclick="connectSockJS()">SockJS Verbinden</button>
|
||||
<button onclick="disconnect()">Trennen</button>
|
||||
<br><br>
|
||||
<input type="text" id="messageInput" placeholder="Nachricht eingeben" />
|
||||
<button onclick="sendMessage()">Nachricht senden</button>
|
||||
<br><br>
|
||||
<div id="messages"></div>
|
||||
|
||||
<script>
|
||||
let stompClient = null;
|
||||
|
||||
function connectWebSocket() {
|
||||
// WICHTIG: Für native WebSocket MUSS ws:// verwendet werden, NICHT http://
|
||||
const socket = new WebSocket('ws://192.168.180.196:8080/websocket');
|
||||
stompClient = Stomp.over(socket);
|
||||
|
||||
stompClient.debug = function(str) {
|
||||
console.log('STOMP Debug: ' + str);
|
||||
addMessage('DEBUG: ' + str);
|
||||
};
|
||||
|
||||
stompClient.connect({}, function(frame) {
|
||||
console.log('WebSocket Connected: ' + frame);
|
||||
document.getElementById('status').innerHTML = 'WebSocket Verbunden';
|
||||
addMessage('WebSocket erfolgreich verbunden');
|
||||
|
||||
// Nachrichten abonnieren
|
||||
stompClient.subscribe('/topic/messages', function(message) {
|
||||
addMessage('Empfangen: ' + message.body);
|
||||
});
|
||||
}, function(error) {
|
||||
console.error('WebSocket Connection Error:', error);
|
||||
document.getElementById('status').innerHTML = 'WebSocket Fehler: ' + error;
|
||||
addMessage('WebSocket Fehler: ' + error);
|
||||
});
|
||||
}
|
||||
|
||||
function connectSockJS() {
|
||||
// Für SockJS kann http:// verwendet werden
|
||||
const socket = new SockJS('http://192.168.180.196:8080/ws');
|
||||
stompClient = Stomp.over(socket);
|
||||
|
||||
stompClient.debug = function(str) {
|
||||
console.log('STOMP Debug: ' + str);
|
||||
addMessage('DEBUG: ' + str);
|
||||
};
|
||||
|
||||
stompClient.connect({}, function(frame) {
|
||||
console.log('SockJS Connected: ' + frame);
|
||||
document.getElementById('status').innerHTML = 'SockJS Verbunden';
|
||||
addMessage('SockJS erfolgreich verbunden');
|
||||
|
||||
// Nachrichten abonnieren
|
||||
stompClient.subscribe('/topic/messages', function(message) {
|
||||
addMessage('Empfangen: ' + message.body);
|
||||
});
|
||||
}, function(error) {
|
||||
console.error('SockJS Connection Error:', error);
|
||||
document.getElementById('status').innerHTML = 'SockJS Fehler: ' + error;
|
||||
addMessage('SockJS Fehler: ' + error);
|
||||
});
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (stompClient !== null) {
|
||||
stompClient.disconnect();
|
||||
document.getElementById('status').innerHTML = 'Getrennt';
|
||||
addMessage('Verbindung getrennt');
|
||||
}
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
if (stompClient && messageInput.value) {
|
||||
stompClient.send('/app/message', {}, JSON.stringify({
|
||||
'content': messageInput.value,
|
||||
'sender': 'WebTest'
|
||||
}));
|
||||
addMessage('Gesendet: ' + messageInput.value);
|
||||
messageInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function addMessage(message) {
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.innerHTML = new Date().toLocaleTimeString() + ': ' + message;
|
||||
messagesDiv.appendChild(messageElement);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
</script>
|
||||
|
||||
<h2>Flutter Dart Code Beispiel:</h2>
|
||||
<pre><code>
|
||||
import 'package:stomp_dart_client/stomp.dart';
|
||||
import 'package:stomp_dart_client/stomp_config.dart';
|
||||
import 'package:stomp_dart_client/stomp_frame.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
// RICHTIG: ws:// für WebSocket verwenden
|
||||
final stompClient = StompClient(
|
||||
config: StompConfig(
|
||||
url: 'ws://192.168.180.196:8080/websocket', // ws:// NICHT http://
|
||||
onConnect: (StompFrame frame) {
|
||||
print('Connected to STOMP server');
|
||||
|
||||
// Nachrichten abonnieren
|
||||
stompClient.subscribe(
|
||||
destination: '/topic/messages',
|
||||
callback: (StompFrame frame) {
|
||||
print('Received: ${frame.body}');
|
||||
},
|
||||
);
|
||||
},
|
||||
onWebSocketError: (dynamic error) => print('WebSocket Error: $error'),
|
||||
onStompError: (StompFrame frame) => print('Stomp Error: ${frame.body}'),
|
||||
onDisconnect: (StompFrame frame) => print('Disconnected'),
|
||||
),
|
||||
);
|
||||
|
||||
// Verbindung aktivieren
|
||||
stompClient.activate();
|
||||
|
||||
// Nachricht senden
|
||||
void sendMessage(String content) {
|
||||
stompClient.send(
|
||||
destination: '/app/message',
|
||||
body: jsonEncode({
|
||||
'content': content,
|
||||
'sender': 'FlutterApp',
|
||||
}),
|
||||
);
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h2>Verfügbare Endpunkte:</h2>
|
||||
<ul>
|
||||
<li><strong>ws://192.168.180.196:8080/websocket</strong> - Native WebSocket (empfohlen für Flutter)</li>
|
||||
<li><strong>ws://192.168.180.196:8080/stomp</strong> - Alternative WebSocket Endpunkt</li>
|
||||
<li><strong>http://192.168.180.196:8080/ws</strong> - SockJS Endpunkt (nur für Browser)</li>
|
||||
</ul>
|
||||
|
||||
<h2>Häufiger Fehler:</h2>
|
||||
<p><strong>FALSCH:</strong> <code>http://192.168.180.196:8080/websocket</code></p>
|
||||
<p><strong>RICHTIG:</strong> <code>ws://192.168.180.196:8080/websocket</code></p>
|
||||
</body>
|
||||
</html>
|
||||
259
backend/mvnw
vendored
Executable file
259
backend/mvnw
vendored
Executable file
@@ -0,0 +1,259 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
149
backend/mvnw.cmd
vendored
Normal file
149
backend/mvnw.cmd
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
|
||||
}
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
8626
backend/package-lock.json
generated
Normal file
8626
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
110
backend/package.json
Normal file
110
backend/package.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"name": "no-name",
|
||||
"license": "UNLICENSED",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@vaadin/bundles": "24.7.1",
|
||||
"@vaadin/common-frontend": "0.0.19",
|
||||
"@vaadin/polymer-legacy-adapter": "24.7.1",
|
||||
"@vaadin/react-components": "24.7.1",
|
||||
"@vaadin/react-components-pro": "24.7.1",
|
||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||
"@vaadin/vaadin-lumo-styles": "24.7.1",
|
||||
"@vaadin/vaadin-material-styles": "24.7.1",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.1",
|
||||
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||
"construct-style-sheets-polyfill": "3.1.0",
|
||||
"date-fns": "2.29.3",
|
||||
"lit": "3.2.1",
|
||||
"proj4": "2.15.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-router": "7.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-react": "7.26.3",
|
||||
"@preact/signals-react-transform": "0.5.1",
|
||||
"@rollup/plugin-replace": "6.0.2",
|
||||
"@rollup/pluginutils": "5.1.4",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"async": "3.2.6",
|
||||
"glob": "11.0.1",
|
||||
"rollup-plugin-brotli": "3.1.0",
|
||||
"rollup-plugin-visualizer": "5.14.0",
|
||||
"strip-css-comments": "5.0.0",
|
||||
"transform-ast": "2.4.4",
|
||||
"typescript": "5.7.3",
|
||||
"vite": "6.2.2",
|
||||
"vite-plugin-checker": "0.8.0",
|
||||
"workbox-build": "7.3.0",
|
||||
"workbox-core": "7.3.0",
|
||||
"workbox-precaching": "7.3.0"
|
||||
},
|
||||
"vaadin": {
|
||||
"dependencies": {
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@vaadin/bundles": "24.7.1",
|
||||
"@vaadin/common-frontend": "0.0.19",
|
||||
"@vaadin/polymer-legacy-adapter": "24.7.1",
|
||||
"@vaadin/react-components": "24.7.1",
|
||||
"@vaadin/react-components-pro": "24.7.1",
|
||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||
"@vaadin/vaadin-lumo-styles": "24.7.1",
|
||||
"@vaadin/vaadin-material-styles": "24.7.1",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.1",
|
||||
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||
"construct-style-sheets-polyfill": "3.1.0",
|
||||
"date-fns": "2.29.3",
|
||||
"lit": "3.2.1",
|
||||
"proj4": "2.15.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-router": "7.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-react": "7.26.3",
|
||||
"@preact/signals-react-transform": "0.5.1",
|
||||
"@rollup/plugin-replace": "6.0.2",
|
||||
"@rollup/pluginutils": "5.1.4",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"async": "3.2.6",
|
||||
"glob": "11.0.1",
|
||||
"rollup-plugin-brotli": "3.1.0",
|
||||
"rollup-plugin-visualizer": "5.14.0",
|
||||
"strip-css-comments": "5.0.0",
|
||||
"transform-ast": "2.4.4",
|
||||
"typescript": "5.7.3",
|
||||
"vite": "6.2.2",
|
||||
"vite-plugin-checker": "0.8.0",
|
||||
"workbox-build": "7.3.0",
|
||||
"workbox-core": "7.3.0",
|
||||
"workbox-precaching": "7.3.0"
|
||||
},
|
||||
"hash": "45492b1b7298b15a429b43657e872572fe696eb0499802a234b6743768eb5105"
|
||||
},
|
||||
"overrides": {
|
||||
"@vaadin/bundles": "$@vaadin/bundles",
|
||||
"@vaadin/polymer-legacy-adapter": "$@vaadin/polymer-legacy-adapter",
|
||||
"@vaadin/vaadin-development-mode-detector": "$@vaadin/vaadin-development-mode-detector",
|
||||
"@vaadin/vaadin-usage-statistics": "$@vaadin/vaadin-usage-statistics",
|
||||
"@vaadin/react-components": "$@vaadin/react-components",
|
||||
"@vaadin/react-components-pro": "$@vaadin/react-components-pro",
|
||||
"@vaadin/common-frontend": "$@vaadin/common-frontend",
|
||||
"react-dom": "$react-dom",
|
||||
"construct-style-sheets-polyfill": "$construct-style-sheets-polyfill",
|
||||
"lit": "$lit",
|
||||
"@polymer/polymer": "$@polymer/polymer",
|
||||
"react": "$react",
|
||||
"react-router": "$react-router",
|
||||
"date-fns": "$date-fns",
|
||||
"proj4": "$proj4",
|
||||
"@vaadin/vaadin-themable-mixin": "$@vaadin/vaadin-themable-mixin",
|
||||
"@vaadin/vaadin-lumo-styles": "$@vaadin/vaadin-lumo-styles",
|
||||
"@vaadin/vaadin-material-styles": "$@vaadin/vaadin-material-styles"
|
||||
}
|
||||
}
|
||||
367
backend/pom.xml
Normal file
367
backend/pom.xml
Normal file
@@ -0,0 +1,367 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>de.assecutor.votianlt</groupId>
|
||||
<artifactId>votianlt</artifactId>
|
||||
<version>${revision}</version>
|
||||
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<revision>0.9.13</revision>
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<vaadin.version>24.7.0</vaadin.version>
|
||||
<archunit.version>1.3.0</archunit.version>
|
||||
</properties>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.3</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-bom</artifactId>
|
||||
<version>${vaadin.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<!-- Spring AI BOM -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Vaadin -->
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<!-- Replace artifactId with vaadin-core to use only free components -->
|
||||
<artifactId>vaadin</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Additional Spring Dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring WebSocket for messaging plugin -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-mongodb</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Security Dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.38</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.librepdf</groupId>
|
||||
<artifactId>openpdf</artifactId>
|
||||
<version>1.3.30</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Mail Starter -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- .env file support -->
|
||||
<dependency>
|
||||
<groupId>me.paulschwarz</groupId>
|
||||
<artifactId>spring-dotenv</artifactId>
|
||||
<version>4.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring WebFlux for direct LLM API calls (like aimailassistant) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson JSR310 module for Java 8 date/time support -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
<!-- iText8 for PDF generation -->
|
||||
<dependency>
|
||||
<groupId>com.itextpdf</groupId>
|
||||
<artifactId>itext-core</artifactId>
|
||||
<version>8.0.5</version>
|
||||
<type>pom</type>
|
||||
</dependency>
|
||||
|
||||
<!-- iText8 Kernel (PDF core functionality) -->
|
||||
<dependency>
|
||||
<groupId>com.itextpdf</groupId>
|
||||
<artifactId>kernel</artifactId>
|
||||
<version>8.0.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- iText8 Layout (document layout) -->
|
||||
<dependency>
|
||||
<groupId>com.itextpdf</groupId>
|
||||
<artifactId>layout</artifactId>
|
||||
<version>8.0.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- iText8 HTML to PDF converter -->
|
||||
<dependency>
|
||||
<groupId>com.itextpdf</groupId>
|
||||
<artifactId>html2pdf</artifactId>
|
||||
<version>5.0.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring AI OpenAI (LM Studio kompatibel) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MCP Server mit WebMVC Transport -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test Dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<defaultGoal>spring-boot:run</defaultGoal>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<release>21</release>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.38</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.diffplug.spotless</groupId>
|
||||
<artifactId>spotless-maven-plugin</artifactId>
|
||||
<version>2.43.0</version>
|
||||
<configuration>
|
||||
<java>
|
||||
<eclipse>
|
||||
<version>4.33</version>
|
||||
<file>${project.basedir}/eclipse-formatter.xml</file>
|
||||
</eclipse>
|
||||
</java>
|
||||
<typescript>
|
||||
<includes>
|
||||
<include>src/main/frontend/**/*.ts</include>
|
||||
<include>src/main/frontend/**/*.tsx</include>
|
||||
</includes>
|
||||
<excludes>
|
||||
<exclude>src/main/frontend/generated/**</exclude>
|
||||
</excludes>
|
||||
<prettier>
|
||||
<prettierVersion>3.3.3</prettierVersion>
|
||||
<configFile>${project.basedir}/.prettierrc.json</configFile>
|
||||
</prettier>
|
||||
</typescript>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-maven-plugin</artifactId>
|
||||
<version>${vaadin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>prepare-frontend</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-wrapper-plugin</artifactId>
|
||||
<version>3.3.2</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
<includes>
|
||||
<include>application.properties</include>
|
||||
</includes>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>false</filtering>
|
||||
<excludes>
|
||||
<exclude>application.properties</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
</resources>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>production</id>
|
||||
<dependencies>
|
||||
<!-- Exclude development dependencies from production -->
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-core</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-dev</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-maven-plugin</artifactId>
|
||||
<version>${vaadin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build-frontend</goal>
|
||||
</goals>
|
||||
<phase>compile</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>integration-test</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>integration-test</goal>
|
||||
<goal>verify</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<trimStackTrace>false</trimStackTrace>
|
||||
<enableAssertions>true</enableAssertions>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>vaadin-directory</id>
|
||||
<url>https://maven.vaadin.com/vaadin-addons</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>vaadin-prereleases</id>
|
||||
<url>https://maven.vaadin.com/vaadin-prereleases</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>vaadin-prereleases</id>
|
||||
<url>https://maven.vaadin.com/vaadin-prereleases</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
</project>
|
||||
32
backend/src/main/bundles/README.md
Normal file
32
backend/src/main/bundles/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
This directory is automatically generated by Vaadin and contains the pre-compiled
|
||||
frontend files/resources for your project (frontend development bundle).
|
||||
|
||||
It should be added to Version Control System and committed, so that other developers
|
||||
do not have to compile it again.
|
||||
|
||||
Frontend development bundle is automatically updated when needed:
|
||||
- an npm/pnpm package is added with @NpmPackage or directly into package.json
|
||||
- CSS, JavaScript or TypeScript files are added with @CssImport, @JsModule or @JavaScript
|
||||
- Vaadin add-on with front-end customizations is added
|
||||
- Custom theme imports/assets added into 'theme.json' file
|
||||
- Exported web component is added.
|
||||
|
||||
If your project development needs a hot deployment of the frontend changes,
|
||||
you can switch Flow to use Vite development server (default in Vaadin 23.3 and earlier versions):
|
||||
- set `vaadin.frontend.hotdeploy=true` in `application.properties`
|
||||
- configure `vaadin-maven-plugin`:
|
||||
```
|
||||
<configuration>
|
||||
<frontendHotdeploy>true</frontendHotdeploy>
|
||||
</configuration>
|
||||
```
|
||||
- configure `jetty-maven-plugin`:
|
||||
```
|
||||
<configuration>
|
||||
<systemProperties>
|
||||
<vaadin.frontend.hotdeploy>true</vaadin.frontend.hotdeploy>
|
||||
</systemProperties>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
Read more [about Vaadin development mode](https://vaadin.com/docs/next/flow/configuration/development-mode#precompiled-bundle).
|
||||
BIN
backend/src/main/bundles/dev.bundle
Normal file
BIN
backend/src/main/bundles/dev.bundle
Normal file
Binary file not shown.
BIN
backend/src/main/bundles/prod.bundle
Normal file
BIN
backend/src/main/bundles/prod.bundle
Normal file
Binary file not shown.
23
backend/src/main/frontend/index.html
Normal file
23
backend/src/main/frontend/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This file is auto-generated by Vaadin.
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
body, #outlet {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
|
||||
</head>
|
||||
<body>
|
||||
<!-- This outlet div is where the views are rendered -->
|
||||
<div id="outlet"></div>
|
||||
</body>
|
||||
</html>
|
||||
878
backend/src/main/frontend/invoice-generator/invoice-generator.js
Normal file
878
backend/src/main/frontend/invoice-generator/invoice-generator.js
Normal file
@@ -0,0 +1,878 @@
|
||||
// Invoice Generator - Native HTML5 Canvas Implementation
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
class InvoiceGenerator {
|
||||
constructor(containerId) {
|
||||
this.canvas = null;
|
||||
this.ctx = null;
|
||||
this.elements = new Map();
|
||||
this.selectedElement = null;
|
||||
this.elementCounter = 0;
|
||||
this.isDragging = false;
|
||||
this.isResizing = false;
|
||||
this.dragStart = { x: 0, y: 0 };
|
||||
this.elementStart = { x: 0, y: 0, w: 0, h: 0 };
|
||||
this.activeHandle = null;
|
||||
this.container = null;
|
||||
this.containerId = containerId || 'invoice-canvas-container';
|
||||
|
||||
// Page dimensions (A4 at 96 DPI)
|
||||
this.pageX = 50;
|
||||
this.pageY = 30;
|
||||
this.pageWidth = 794;
|
||||
this.pageHeight = 1123;
|
||||
this.scale = 1;
|
||||
|
||||
// Handle configuration
|
||||
this.handleSize = 10;
|
||||
this.handlePadding = 3;
|
||||
|
||||
// Grid configuration (Snap to Grid)
|
||||
this.gridSize = 5;
|
||||
this.showGrid = true;
|
||||
}
|
||||
|
||||
// Snap value to grid
|
||||
snapToGrid(value) {
|
||||
return Math.round(value / this.gridSize) * this.gridSize;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.container = document.getElementById(this.containerId);
|
||||
if (!this.container) {
|
||||
console.error('Canvas container not found: ' + this.containerId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create canvas
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.style.display = 'block';
|
||||
this.updateCanvasSize();
|
||||
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.container.innerHTML = '';
|
||||
this.container.appendChild(this.canvas);
|
||||
|
||||
// Bind events
|
||||
this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
|
||||
this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this));
|
||||
this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this));
|
||||
this.canvas.addEventListener('mouseleave', this.onMouseUp.bind(this));
|
||||
|
||||
// Keyboard navigation for selected element
|
||||
this.canvas.setAttribute('tabindex', '0');
|
||||
this.canvas.addEventListener('keydown', this.onKeyDown.bind(this));
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.updateCanvasSize();
|
||||
this.draw();
|
||||
});
|
||||
|
||||
this.draw();
|
||||
console.log('Invoice generator initialized');
|
||||
}
|
||||
|
||||
updateCanvasSize() {
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.canvas.width = rect.width;
|
||||
this.canvas.height = rect.height;
|
||||
|
||||
// Calculate scale to maximize page size while maintaining aspect ratio
|
||||
// Use full available space with minimal padding
|
||||
const padding = 10;
|
||||
const availableWidth = this.canvas.width - padding * 2;
|
||||
const availableHeight = this.canvas.height - padding * 2;
|
||||
|
||||
// Scale to fit the available space (can be larger than 1 if container is bigger than A4)
|
||||
this.scale = Math.min(availableWidth / this.pageWidth, availableHeight / this.pageHeight);
|
||||
|
||||
// Center the scaled page
|
||||
const scaledPageWidth = this.pageWidth * this.scale;
|
||||
const scaledPageHeight = this.pageHeight * this.scale;
|
||||
this.pageX = (this.canvas.width - scaledPageWidth) / 2;
|
||||
this.pageY = (this.canvas.height - scaledPageHeight) / 2;
|
||||
}
|
||||
|
||||
getMousePos(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
// Convert screen coordinates to canvas coordinates
|
||||
const canvasX = e.clientX - rect.left;
|
||||
const canvasY = e.clientY - rect.top;
|
||||
// Convert to page coordinates (accounting for scale and offset)
|
||||
return {
|
||||
x: (canvasX - this.pageX) / this.scale,
|
||||
y: (canvasY - this.pageY) / this.scale
|
||||
};
|
||||
}
|
||||
|
||||
// Check if point is inside element
|
||||
hitTest(x, y, el) {
|
||||
const w = el.width || 150;
|
||||
const h = el.height || 30;
|
||||
return x >= el.x && x <= el.x + w && y >= el.y && y <= el.y + h;
|
||||
}
|
||||
|
||||
// Check if point is on a resize handle
|
||||
getHandleAt(x, y, el) {
|
||||
if (!el) return null;
|
||||
|
||||
const w = el.width || 150;
|
||||
const h = el.height || 30;
|
||||
const hs = this.handleSize + this.handlePadding;
|
||||
|
||||
// Define handle positions (center points)
|
||||
const handles = {
|
||||
'nw': { x: el.x, y: el.y },
|
||||
'n': { x: el.x + w / 2, y: el.y },
|
||||
'ne': { x: el.x + w, y: el.y },
|
||||
'w': { x: el.x, y: el.y + h / 2 },
|
||||
'e': { x: el.x + w, y: el.y + h / 2 },
|
||||
'sw': { x: el.x, y: el.y + h },
|
||||
's': { x: el.x + w / 2, y: el.y + h },
|
||||
'se': { x: el.x + w, y: el.y + h }
|
||||
};
|
||||
|
||||
for (const [name, pos] of Object.entries(handles)) {
|
||||
const dx = x - pos.x;
|
||||
const dy = y - pos.y;
|
||||
if (Math.abs(dx) <= hs && Math.abs(dy) <= hs) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onMouseDown(e) {
|
||||
const pos = this.getMousePos(e);
|
||||
|
||||
// First check if clicking on a handle of selected element
|
||||
if (this.selectedElement) {
|
||||
const handle = this.getHandleAt(pos.x, pos.y, this.selectedElement);
|
||||
if (handle) {
|
||||
console.log('Resize handle clicked:', handle);
|
||||
this.isResizing = true;
|
||||
this.activeHandle = handle;
|
||||
this.dragStart = { x: pos.x, y: pos.y };
|
||||
this.elementStart = {
|
||||
x: this.selectedElement.x,
|
||||
y: this.selectedElement.y,
|
||||
w: this.selectedElement.width || 100,
|
||||
h: this.selectedElement.height || 30
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if clicking on an element
|
||||
let clickedElement = null;
|
||||
for (const el of Array.from(this.elements.values()).reverse()) {
|
||||
if (this.hitTest(pos.x, pos.y, el)) {
|
||||
clickedElement = el;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (clickedElement) {
|
||||
this.selectElement(clickedElement.id);
|
||||
this.isDragging = true;
|
||||
this.dragStart = { x: pos.x, y: pos.y };
|
||||
this.elementStart = { x: clickedElement.x, y: clickedElement.y };
|
||||
this.canvas.style.cursor = 'move';
|
||||
} else {
|
||||
this.deselectAll();
|
||||
}
|
||||
|
||||
this.draw();
|
||||
}
|
||||
|
||||
onMouseMove(e) {
|
||||
const pos = this.getMousePos(e);
|
||||
|
||||
if (this.isResizing && this.selectedElement) {
|
||||
this.doResize(pos.x, pos.y);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isDragging && this.selectedElement) {
|
||||
const dx = pos.x - this.dragStart.x;
|
||||
const dy = pos.y - this.dragStart.y;
|
||||
|
||||
let newX = this.elementStart.x + dx;
|
||||
let newY = this.elementStart.y + dy;
|
||||
|
||||
// Constrain to page (in page coordinates, page starts at 0,0)
|
||||
newX = Math.max(0, Math.min(newX,
|
||||
this.pageWidth - (this.selectedElement.width || 100)));
|
||||
newY = Math.max(0, Math.min(newY,
|
||||
this.pageHeight - (this.selectedElement.height || 30)));
|
||||
|
||||
// Snap to grid
|
||||
this.selectedElement.x = this.snapToGrid(newX);
|
||||
this.selectedElement.y = this.snapToGrid(newY);
|
||||
|
||||
this.draw();
|
||||
this.notifyChange();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update cursor
|
||||
if (this.selectedElement) {
|
||||
const handle = this.getHandleAt(pos.x, pos.y, this.selectedElement);
|
||||
if (handle) {
|
||||
const cursors = {
|
||||
'nw': 'nw-resize', 'ne': 'ne-resize', 'sw': 'sw-resize', 'se': 'se-resize',
|
||||
'n': 'ns-resize', 's': 'ns-resize', 'e': 'ew-resize', 'w': 'ew-resize'
|
||||
};
|
||||
this.canvas.style.cursor = cursors[handle] || 'default';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check hover over elements
|
||||
let hovering = false;
|
||||
for (const el of Array.from(this.elements.values()).reverse()) {
|
||||
if (this.hitTest(pos.x, pos.y, el)) {
|
||||
hovering = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.canvas.style.cursor = hovering ? 'move' : 'default';
|
||||
}
|
||||
|
||||
onMouseUp(e) {
|
||||
this.isDragging = false;
|
||||
this.isResizing = false;
|
||||
this.activeHandle = null;
|
||||
this.canvas.style.cursor = 'default';
|
||||
}
|
||||
|
||||
onKeyDown(e) {
|
||||
if (!this.selectedElement) return;
|
||||
|
||||
const step = this.gridSize; // Move by grid size (5px)
|
||||
let moved = false;
|
||||
|
||||
switch(e.key) {
|
||||
case 'ArrowUp':
|
||||
this.selectedElement.y = Math.max(0, this.selectedElement.y - step);
|
||||
moved = true;
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
this.selectedElement.y = Math.min(
|
||||
this.pageHeight - (this.selectedElement.height || 30),
|
||||
this.selectedElement.y + step
|
||||
);
|
||||
moved = true;
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
this.selectedElement.x = Math.max(0, this.selectedElement.x - step);
|
||||
moved = true;
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
this.selectedElement.x = Math.min(
|
||||
this.pageWidth - (this.selectedElement.width || 100),
|
||||
this.selectedElement.x + step
|
||||
);
|
||||
moved = true;
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
|
||||
if (moved) {
|
||||
this.draw();
|
||||
this.notifyChange();
|
||||
}
|
||||
}
|
||||
|
||||
doResize(mouseX, mouseY) {
|
||||
if (!this.selectedElement || !this.activeHandle) return;
|
||||
|
||||
const el = this.selectedElement;
|
||||
el.manuallyResized = true; // Mark as manually resized
|
||||
|
||||
const start = this.elementStart;
|
||||
const minSize = 20;
|
||||
|
||||
// Calculate new dimensions based on handle
|
||||
switch (this.activeHandle) {
|
||||
case 'se':
|
||||
el.width = Math.max(minSize, mouseX - start.x);
|
||||
el.height = Math.max(minSize, mouseY - start.y);
|
||||
break;
|
||||
|
||||
case 'sw':
|
||||
const newWsw = start.w + (start.x - mouseX);
|
||||
if (newWsw >= minSize) {
|
||||
el.x = mouseX;
|
||||
el.width = newWsw;
|
||||
}
|
||||
el.height = Math.max(minSize, mouseY - start.y);
|
||||
break;
|
||||
|
||||
case 'ne':
|
||||
el.width = Math.max(minSize, mouseX - start.x);
|
||||
const newHne = start.h + (start.y - mouseY);
|
||||
if (newHne >= minSize) {
|
||||
el.y = mouseY;
|
||||
el.height = newHne;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'nw':
|
||||
const newWnw = start.w + (start.x - mouseX);
|
||||
const newHnw = start.h + (start.y - mouseY);
|
||||
if (newWnw >= minSize) {
|
||||
el.x = mouseX;
|
||||
el.width = newWnw;
|
||||
}
|
||||
if (newHnw >= minSize) {
|
||||
el.y = mouseY;
|
||||
el.height = newHnw;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'e':
|
||||
el.width = Math.max(minSize, mouseX - start.x);
|
||||
break;
|
||||
|
||||
case 'w':
|
||||
const newWw = start.w + (start.x - mouseX);
|
||||
if (newWw >= minSize) {
|
||||
el.x = mouseX;
|
||||
el.width = newWw;
|
||||
}
|
||||
break;
|
||||
|
||||
case 's':
|
||||
el.height = Math.max(minSize, mouseY - start.y);
|
||||
break;
|
||||
|
||||
case 'n':
|
||||
const newHn = start.h + (start.y - mouseY);
|
||||
if (newHn >= minSize) {
|
||||
el.y = mouseY;
|
||||
el.height = newHn;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.draw();
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
notifyChange() {
|
||||
if (this.selectedElement && window.invoiceGeneratorView?.$server) {
|
||||
const el = this.selectedElement;
|
||||
window.invoiceGeneratorView.$server.updatePropertiesPanel(
|
||||
el.id, el.type, el.text || '', el.x, el.y, el.fontSize || 14, el.color || '#000000'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (!this.ctx) return;
|
||||
|
||||
const ctx = this.ctx;
|
||||
|
||||
// Clear
|
||||
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Background
|
||||
ctx.fillStyle = '#e8e8e8';
|
||||
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
ctx.save();
|
||||
|
||||
// Apply scale and offset
|
||||
ctx.translate(this.pageX, this.pageY);
|
||||
ctx.scale(this.scale, this.scale);
|
||||
|
||||
// Page shadow (drawn before scaling offset)
|
||||
ctx.save();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.2)';
|
||||
ctx.fillRect(this.pageX + 4 * this.scale, this.pageY + this.pageHeight * this.scale, this.pageWidth * this.scale, 4);
|
||||
ctx.fillRect(this.pageX + this.pageWidth * this.scale, this.pageY + 4 * this.scale, 4, this.pageHeight * this.scale);
|
||||
ctx.restore();
|
||||
|
||||
// Page
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, this.pageWidth, this.pageHeight);
|
||||
ctx.strokeStyle = '#cccccc';
|
||||
ctx.lineWidth = 1 / this.scale;
|
||||
ctx.strokeRect(0, 0, this.pageWidth, this.pageHeight);
|
||||
|
||||
// Draw grid if enabled
|
||||
if (this.showGrid) {
|
||||
this.drawGrid(ctx);
|
||||
}
|
||||
|
||||
// Draw elements
|
||||
this.elements.forEach(el => this.drawElement(el));
|
||||
|
||||
// Draw selection
|
||||
if (this.selectedElement) {
|
||||
this.drawSelection(this.selectedElement);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
drawImagePlaceholder(ctx, el) {
|
||||
ctx.fillStyle = '#f0f0f0';
|
||||
ctx.fillRect(el.x, el.y, el.width || 100, el.height || 100);
|
||||
ctx.strokeStyle = '#999999';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(el.x, el.y, el.width || 100, el.height || 100);
|
||||
ctx.fillStyle = '#666666';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('Bild', el.x + (el.width || 100) / 2, el.y + (el.height || 100) / 2);
|
||||
}
|
||||
|
||||
drawElement(el) {
|
||||
const ctx = this.ctx;
|
||||
ctx.save();
|
||||
|
||||
switch (el.type) {
|
||||
case 'line':
|
||||
ctx.strokeStyle = el.color || '#333333';
|
||||
ctx.lineWidth = el.strokeWidth || 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(el.x, el.y);
|
||||
ctx.lineTo(el.x + (el.width || 200), el.y);
|
||||
ctx.stroke();
|
||||
break;
|
||||
|
||||
case 'vline':
|
||||
ctx.strokeStyle = el.color || '#333333';
|
||||
ctx.lineWidth = el.strokeWidth || 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(el.x, el.y);
|
||||
ctx.lineTo(el.x, el.y + (el.height || 200));
|
||||
ctx.stroke();
|
||||
break;
|
||||
|
||||
case 'image':
|
||||
if (el.imageData) {
|
||||
// Draw actual image
|
||||
const img = el.imageObj;
|
||||
if (img && img.complete) {
|
||||
// Draw image scaled to fit element while maintaining aspect ratio
|
||||
const imgAspect = img.width / img.height;
|
||||
const elAspect = (el.width || 100) / (el.height || 100);
|
||||
|
||||
let drawWidth, drawHeight, drawX, drawY;
|
||||
|
||||
if (imgAspect > elAspect) {
|
||||
// Image is wider than element
|
||||
drawWidth = el.width || 100;
|
||||
drawHeight = drawWidth / imgAspect;
|
||||
drawX = el.x;
|
||||
drawY = el.y + ((el.height || 100) - drawHeight) / 2;
|
||||
} else {
|
||||
// Image is taller than element
|
||||
drawHeight = el.height || 100;
|
||||
drawWidth = drawHeight * imgAspect;
|
||||
drawX = el.x + ((el.width || 100) - drawWidth) / 2;
|
||||
drawY = el.y;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
|
||||
|
||||
// Draw border
|
||||
ctx.strokeStyle = '#cccccc';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(el.x, el.y, el.width || 100, el.height || 100);
|
||||
} else {
|
||||
// Image still loading or failed
|
||||
this.drawImagePlaceholder(ctx, el);
|
||||
}
|
||||
} else {
|
||||
// No image uploaded yet - draw placeholder
|
||||
this.drawImagePlaceholder(ctx, el);
|
||||
}
|
||||
break;
|
||||
|
||||
default: // text elements
|
||||
this.drawTextElement(el, ctx);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Wrap text to fit within maxWidth
|
||||
wrapText(ctx, text, maxWidth) {
|
||||
const words = text.split(' ');
|
||||
const lines = [];
|
||||
let currentLine = words[0];
|
||||
|
||||
for (let i = 1; i < words.length; i++) {
|
||||
const word = words[i];
|
||||
const width = ctx.measureText(currentLine + ' ' + word).width;
|
||||
if (width < maxWidth) {
|
||||
currentLine += ' ' + word;
|
||||
} else {
|
||||
lines.push(currentLine);
|
||||
currentLine = word;
|
||||
}
|
||||
}
|
||||
lines.push(currentLine);
|
||||
return lines;
|
||||
}
|
||||
|
||||
drawTextElement(el, ctx) {
|
||||
ctx.font = `${el.fontStyle || ''} ${el.fontSize || 14}px ${el.fontFamily || 'Arial'}`;
|
||||
ctx.fillStyle = el.color || '#333333';
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
const maxWidth = el.width || 150;
|
||||
const lineHeight = (el.fontSize || 14) * 1.2;
|
||||
const maxHeight = el.height || 1000;
|
||||
|
||||
// Calculate max lines that fit
|
||||
const maxLines = Math.floor(maxHeight / lineHeight);
|
||||
|
||||
// Split by explicit newlines first, then wrap each line
|
||||
const explicitLines = (el.text || '').split('\n');
|
||||
const allLines = [];
|
||||
|
||||
for (const line of explicitLines) {
|
||||
const metrics = ctx.measureText(line);
|
||||
if (metrics.width <= maxWidth) {
|
||||
allLines.push(line);
|
||||
} else {
|
||||
const wrapped = this.wrapText(ctx, line, maxWidth);
|
||||
allLines.push(...wrapped);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if we need ellipsis
|
||||
const needsEllipsis = allLines.length > maxLines;
|
||||
const linesToDraw = needsEllipsis ? maxLines : allLines.length;
|
||||
|
||||
// Draw lines
|
||||
let y = el.y;
|
||||
for (let i = 0; i < linesToDraw; i++) {
|
||||
const line = allLines[i];
|
||||
|
||||
// If this is the last line and we need ellipsis
|
||||
if (needsEllipsis && i === linesToDraw - 1) {
|
||||
const ellipsis = '...';
|
||||
// Check if the line with ellipsis would fit
|
||||
if (ctx.measureText(line + ellipsis).width <= maxWidth) {
|
||||
ctx.fillText(line + ellipsis, el.x, y);
|
||||
} else {
|
||||
// Need to trim the line to fit ellipsis
|
||||
let trimmed = line;
|
||||
while (trimmed.length > 0 && ctx.measureText(trimmed + ellipsis).width > maxWidth) {
|
||||
trimmed = trimmed.slice(0, -1);
|
||||
}
|
||||
ctx.fillText(trimmed + ellipsis, el.x, y);
|
||||
}
|
||||
} else {
|
||||
ctx.fillText(line, el.x, y);
|
||||
}
|
||||
y += lineHeight;
|
||||
}
|
||||
|
||||
// Update height based on actual content if not manually resized
|
||||
if (!el.manuallyResized) {
|
||||
el.height = Math.max(lineHeight, allLines.length * lineHeight);
|
||||
}
|
||||
}
|
||||
|
||||
drawSelection(el) {
|
||||
const ctx = this.ctx;
|
||||
const x = el.x;
|
||||
const y = el.y;
|
||||
const w = el.width || 150;
|
||||
const h = el.height || 30;
|
||||
const hs = this.handleSize / this.scale;
|
||||
|
||||
// Selection border
|
||||
ctx.strokeStyle = '#1976d2';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([5, 3]);
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Handles
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.strokeStyle = '#1976d2';
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
const positions = [
|
||||
[x - hs/2, y - hs/2], // nw
|
||||
[x + w/2 - hs/2, y - hs/2], // n
|
||||
[x + w - hs/2, y - hs/2], // ne
|
||||
[x - hs/2, y + h/2 - hs/2], // w
|
||||
[x + w - hs/2, y + h/2 - hs/2], // e
|
||||
[x - hs/2, y + h - hs/2], // sw
|
||||
[x + w/2 - hs/2, y + h - hs/2], // s
|
||||
[x + w - hs/2, y + h - hs/2] // se
|
||||
];
|
||||
|
||||
positions.forEach(([hx, hy]) => {
|
||||
ctx.fillRect(hx, hy, hs, hs);
|
||||
ctx.strokeRect(hx, hy, hs, hs);
|
||||
});
|
||||
}
|
||||
|
||||
drawGrid(ctx) {
|
||||
ctx.save();
|
||||
ctx.strokeStyle = 'rgba(200, 200, 200, 0.3)';
|
||||
ctx.lineWidth = 0.5 / this.scale;
|
||||
|
||||
// Vertical lines (draw in page coordinates)
|
||||
for (let x = 0; x <= this.pageWidth; x += this.gridSize) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, this.pageHeight);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Horizontal lines
|
||||
for (let y = 0; y <= this.pageHeight; y += this.gridSize) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(this.pageWidth, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
addElement(type, label, dropX, dropY) {
|
||||
this.elementCounter++;
|
||||
const id = `element-${this.elementCounter}`;
|
||||
|
||||
// dropX and dropY are in container coordinates, convert to page coordinates
|
||||
const pageCoordX = (dropX - this.pageX) / this.scale;
|
||||
const pageCoordY = (dropY - this.pageY) / this.scale;
|
||||
|
||||
// Snap initial position to grid (relative to page origin)
|
||||
const rawX = Math.max(10, pageCoordX - 50);
|
||||
const rawY = Math.max(10, pageCoordY - 15);
|
||||
const x = this.snapToGrid(rawX);
|
||||
const y = this.snapToGrid(rawY);
|
||||
|
||||
let el = {
|
||||
id, type, x, y,
|
||||
width: 150,
|
||||
height: 30,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Arial',
|
||||
color: '#333333'
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'text':
|
||||
el.text = 'Text eingeben...';
|
||||
el.height = 20;
|
||||
break;
|
||||
case 'header':
|
||||
el.text = 'Überschrift';
|
||||
el.fontSize = 24;
|
||||
el.fontStyle = 'bold';
|
||||
el.color = '#000000';
|
||||
el.width = 200;
|
||||
el.height = 30;
|
||||
break;
|
||||
case 'date':
|
||||
el.text = `Datum: ${new Date().toLocaleDateString('de-DE')}`;
|
||||
el.fontSize = 12;
|
||||
el.color = '#666666';
|
||||
el.height = 16;
|
||||
break;
|
||||
case 'customer':
|
||||
case 'company':
|
||||
el.text = type === 'customer' ? 'Kundenname\nStraße Nr.\nPLZ Ort' : 'Ihr Unternehmen\nIhre Straße\nIhre PLZ Ort';
|
||||
el.height = 50;
|
||||
el.fontSize = 12;
|
||||
break;
|
||||
case 'amount':
|
||||
el.text = 'Gesamtbetrag: 0,00 €';
|
||||
el.fontStyle = 'bold';
|
||||
el.width = 180;
|
||||
el.height = 20;
|
||||
break;
|
||||
case 'line':
|
||||
el.text = '';
|
||||
el.width = 200;
|
||||
el.height = 2;
|
||||
break;
|
||||
case 'vline':
|
||||
el.text = '';
|
||||
el.width = 2;
|
||||
el.height = 200;
|
||||
break;
|
||||
case 'image':
|
||||
el.text = 'Bild';
|
||||
el.width = 100;
|
||||
el.height = 100;
|
||||
break;
|
||||
default:
|
||||
el.text = label || 'Neues Element';
|
||||
el.height = 20;
|
||||
}
|
||||
|
||||
this.elements.set(id, el);
|
||||
this.selectElement(id);
|
||||
this.draw();
|
||||
}
|
||||
|
||||
selectElement(id) {
|
||||
this.selectedElement = this.elements.get(id) || null;
|
||||
this.draw();
|
||||
if (this.selectedElement) {
|
||||
this.notifyChange();
|
||||
// Focus canvas for keyboard navigation
|
||||
if (this.canvas) {
|
||||
this.canvas.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deselectAll() {
|
||||
this.selectedElement = null;
|
||||
this.draw();
|
||||
if (window.invoiceGeneratorView?.$server) {
|
||||
window.invoiceGeneratorView.$server.resetPropertiesPanel();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementText(id, text) {
|
||||
const el = this.elements.get(id);
|
||||
if (el) {
|
||||
el.text = text;
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementPosition(id, x, y) {
|
||||
const el = this.elements.get(id);
|
||||
if (el) {
|
||||
if (x !== null) el.x = this.snapToGrid(x);
|
||||
if (y !== null) el.y = this.snapToGrid(y);
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementFontSize(id, size) {
|
||||
const el = this.elements.get(id);
|
||||
if (el) {
|
||||
el.fontSize = size;
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementColor(id, color) {
|
||||
const el = this.elements.get(id);
|
||||
if (el) {
|
||||
el.color = color;
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementImage(id, imageDataUrl) {
|
||||
const el = this.elements.get(id);
|
||||
if (el && el.type === 'image') {
|
||||
el.imageData = imageDataUrl;
|
||||
|
||||
// Create image object
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
el.imageObj = img;
|
||||
this.draw();
|
||||
};
|
||||
img.onerror = () => {
|
||||
console.error('Failed to load image');
|
||||
el.imageData = null;
|
||||
el.imageObj = null;
|
||||
this.draw();
|
||||
};
|
||||
img.src = imageDataUrl;
|
||||
}
|
||||
}
|
||||
|
||||
deleteElement(id) {
|
||||
this.elements.delete(id);
|
||||
if (this.selectedElement?.id === id) {
|
||||
this.selectedElement = null;
|
||||
}
|
||||
this.draw();
|
||||
}
|
||||
|
||||
clearCanvas() {
|
||||
this.elements.clear();
|
||||
this.selectedElement = null;
|
||||
this.draw();
|
||||
}
|
||||
|
||||
getCanvasData() {
|
||||
return {
|
||||
elements: Array.from(this.elements.values())
|
||||
};
|
||||
}
|
||||
|
||||
exportTemplate() {
|
||||
const data = JSON.stringify(this.getCanvasData(), null, 2);
|
||||
const blob = new Blob([data], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'template.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
exportTemplateJson() {
|
||||
// Return template data as JSON string for Java processing
|
||||
const data = this.getCanvasData();
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
|
||||
generatePreview() {
|
||||
const temp = document.createElement('canvas');
|
||||
temp.width = this.pageWidth;
|
||||
temp.height = this.pageHeight;
|
||||
const tctx = temp.getContext('2d');
|
||||
|
||||
tctx.fillStyle = '#ffffff';
|
||||
tctx.fillRect(0, 0, temp.width, temp.height);
|
||||
|
||||
// Draw elements offset by page position
|
||||
const originalPageX = this.pageX;
|
||||
const originalPageY = this.pageY;
|
||||
this.pageX = 0;
|
||||
this.pageY = 0;
|
||||
|
||||
this.elements.forEach(el => {
|
||||
const ox = el.x, oy = el.y;
|
||||
el.x = ox - originalPageX;
|
||||
el.y = oy - originalPageY;
|
||||
this.drawElement.call({ ctx: tctx, elements: this.elements }, el);
|
||||
el.x = ox;
|
||||
el.y = oy;
|
||||
});
|
||||
|
||||
this.pageX = originalPageX;
|
||||
this.pageY = originalPageY;
|
||||
|
||||
const win = window.open();
|
||||
if (win) {
|
||||
win.document.write(`<img src="${temp.toDataURL()}" style="max-width:100%">`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.invoiceGenerator = new InvoiceGenerator();
|
||||
window.InvoiceGenerator = InvoiceGenerator;
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
4
backend/src/main/frontend/themes/default/styles.css
Normal file
4
backend/src/main/frontend/themes/default/styles.css
Normal file
@@ -0,0 +1,4 @@
|
||||
/* Breite des linken Menüs (Drawer) */
|
||||
vaadin-app-layout {
|
||||
--vaadin-app-layout-drawer-width: 286px;
|
||||
}
|
||||
9
backend/src/main/frontend/themes/default/theme.json
Normal file
9
backend/src/main/frontend/themes/default/theme.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"lumoImports": [
|
||||
"typography",
|
||||
"color",
|
||||
"spacing",
|
||||
"badge",
|
||||
"utility"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
[part="tabs-container"] {
|
||||
background: white;
|
||||
border-radius: 24px 24px 0 0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
1894
backend/src/main/frontend/themes/votian-modern/styles.css
Normal file
1894
backend/src/main/frontend/themes/votian-modern/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"lumoImports": [
|
||||
"typography",
|
||||
"color",
|
||||
"spacing",
|
||||
"badge",
|
||||
"utility"
|
||||
]
|
||||
}
|
||||
56
backend/src/main/frontend/utils/language-cookie.ts
Normal file
56
backend/src/main/frontend/utils/language-cookie.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Cookie names for language preference
|
||||
*/
|
||||
const LANGUAGE_COOKIE_NAME = 'votianlt.language';
|
||||
const COOKIE_MAX_AGE_DAYS = 365; // Cookie gültig für 1 Jahr
|
||||
|
||||
/**
|
||||
* Sets the language cookie with the selected language code
|
||||
* @param languageCode - The language code (e.g., 'de', 'en', 'fr', 'es')
|
||||
*/
|
||||
export function setLanguageCookie(languageCode: string): void {
|
||||
const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60; // Convert days to seconds
|
||||
document.cookie = `${LANGUAGE_COOKIE_NAME}=${languageCode};path=/;max-age=${maxAge};SameSite=Lax`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the language code from the cookie
|
||||
* @returns The language code or null if not found
|
||||
*/
|
||||
export function getLanguageCookie(): string | null {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (const cookie of cookies) {
|
||||
const [name, value] = cookie.trim().split('=');
|
||||
if (name === LANGUAGE_COOKIE_NAME) {
|
||||
return decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the language cookie
|
||||
*/
|
||||
export function clearLanguageCookie(): void {
|
||||
document.cookie = `${LANGUAGE_COOKIE_NAME}=;path=/;max-age=0;SameSite=Lax`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps Language enum values to locale strings
|
||||
*/
|
||||
export const languageToLocale: Record<string, string> = {
|
||||
DE: 'de',
|
||||
EN: 'en',
|
||||
FR: 'fr',
|
||||
ES: 'es',
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps locale strings to Language enum values
|
||||
*/
|
||||
export const localeToLanguage: Record<string, string> = {
|
||||
de: 'DE',
|
||||
en: 'EN',
|
||||
fr: 'FR',
|
||||
es: 'ES',
|
||||
};
|
||||
30
backend/src/main/java/de/assecutor/votianlt/Application.java
Normal file
30
backend/src/main/java/de/assecutor/votianlt/Application.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package de.assecutor.votianlt;
|
||||
|
||||
import com.vaadin.flow.component.page.AppShellConfigurator;
|
||||
import com.vaadin.flow.component.page.Push;
|
||||
import com.vaadin.flow.theme.Theme;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
import java.time.Clock;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
@Theme("votian-modern")
|
||||
@Push
|
||||
public class Application implements AppShellConfigurator {
|
||||
|
||||
@Bean
|
||||
public Clock clock() {
|
||||
return Clock.systemDefaultZone(); // You can also use Clock.systemUTC()
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package de.assecutor.votianlt.ai.config;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Configuration for LLM integration via LM Studio. The LM Studio instance
|
||||
* exposes an OpenAI-compatible API at {@code /v1/chat/completions}.
|
||||
*/
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class LlmConfig {
|
||||
|
||||
@Value("${app.ai.lmstudio.base-url}")
|
||||
private String lmstudioBaseUrl;
|
||||
|
||||
@Value("${app.ai.lmstudio.model}")
|
||||
private String lmstudioModel;
|
||||
|
||||
@Value("${app.ai.lmstudio.htaccess-username}")
|
||||
private String lmstudioHtaccessUsername;
|
||||
|
||||
@Value("${app.ai.lmstudio.htaccess-password}")
|
||||
private String lmstudioHtaccessPassword;
|
||||
|
||||
@PostConstruct
|
||||
public void logConfig() {
|
||||
log.info("=== LLM Configuration ===");
|
||||
log.info("Provider: lmstudio");
|
||||
log.info("Base URL: {}", lmstudioBaseUrl);
|
||||
log.info("Model: {}", lmstudioModel);
|
||||
log.info("HTACCESS auth: {}", hasHtaccessCredentials() ? "configured" : "not configured");
|
||||
testConnection(lmstudioBaseUrl, lmstudioModel);
|
||||
}
|
||||
|
||||
private void testConnection(String baseUrl, String model) {
|
||||
log.info("Testing LLM connection to: {}", baseUrl);
|
||||
|
||||
// Test 1: Models endpoint
|
||||
testEndpoint(baseUrl + "/v1/models", "GET", null);
|
||||
|
||||
// Test 2: Chat completions (no streaming)
|
||||
String testPayload = "{\"model\":\"" + model
|
||||
+ "\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":1,\"stream\":false}";
|
||||
testEndpoint(baseUrl + "/v1/chat/completions", "POST", testPayload);
|
||||
}
|
||||
|
||||
private void testEndpoint(String endpoint, String method, String payload) {
|
||||
try {
|
||||
log.info("Testing endpoint: {} {}", method, endpoint);
|
||||
URL url = URI.create(endpoint).toURL();
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod(method);
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(10000);
|
||||
|
||||
if (hasHtaccessCredentials()) {
|
||||
String credentials = lmstudioHtaccessUsername + ":" + lmstudioHtaccessPassword;
|
||||
String encoded = Base64.getEncoder().encodeToString(credentials.getBytes());
|
||||
connection.setRequestProperty("Authorization", "Basic " + encoded);
|
||||
}
|
||||
|
||||
if (payload != null) {
|
||||
connection.setDoOutput(true);
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
try (var os = connection.getOutputStream()) {
|
||||
os.write(payload.getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
String responseMessage = connection.getResponseMessage();
|
||||
|
||||
if (responseCode >= 200 && responseCode < 300) {
|
||||
log.info(" -> SUCCESS (HTTP {} {})", responseCode, responseMessage);
|
||||
} else {
|
||||
String errorBody = "";
|
||||
try (var is = connection.getErrorStream()) {
|
||||
if (is != null) {
|
||||
errorBody = new String(is.readAllBytes());
|
||||
}
|
||||
}
|
||||
log.warn(" -> HTTP {} {} - {}", responseCode, responseMessage, errorBody);
|
||||
}
|
||||
connection.disconnect();
|
||||
} catch (java.net.ConnectException e) {
|
||||
log.error(" -> FAILED - Connection refused: {}", e.getMessage());
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.error(" -> FAILED - Timeout: {}", e.getMessage());
|
||||
} catch (java.net.UnknownHostException e) {
|
||||
log.error(" -> FAILED - Unknown host: {}", e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.error(" -> FAILED: {} - {}", e.getClass().getSimpleName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public String getBaseUrl() {
|
||||
return lmstudioBaseUrl;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return lmstudioModel;
|
||||
}
|
||||
|
||||
public boolean hasHtaccessCredentials() {
|
||||
return lmstudioHtaccessUsername != null && !lmstudioHtaccessUsername.isBlank()
|
||||
&& lmstudioHtaccessPassword != null && !lmstudioHtaccessPassword.isBlank();
|
||||
}
|
||||
|
||||
public String getLmstudioHtaccessUsername() {
|
||||
return lmstudioHtaccessUsername;
|
||||
}
|
||||
|
||||
public String getLmstudioHtaccessPassword() {
|
||||
return lmstudioHtaccessPassword;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,162 @@
|
||||
package de.assecutor.votianlt.ai.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Direct REST client for LM Studio LLM API. Communicates via the
|
||||
* OpenAI-compatible /v1/chat/completions endpoint.
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class LlmRestClient {
|
||||
|
||||
private static final Pattern THINK_BLOCK_PATTERN = Pattern.compile("(?is)<think>.*?</think>");
|
||||
private static final Pattern THINK_TAG_PATTERN = Pattern.compile("(?is)</?think>");
|
||||
|
||||
private final WebClient webClient;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final String model;
|
||||
|
||||
public LlmRestClient(@Value("${app.ai.lmstudio.base-url}") String lmstudioBaseUrl,
|
||||
@Value("${app.ai.lmstudio.model}") String lmstudioModel,
|
||||
@Value("${app.ai.lmstudio.htaccess-username}") String lmstudioHtaccessUsername,
|
||||
@Value("${app.ai.lmstudio.htaccess-password}") String lmstudioHtaccessPassword, ObjectMapper objectMapper) {
|
||||
|
||||
this.model = lmstudioModel;
|
||||
this.objectMapper = objectMapper;
|
||||
|
||||
WebClient.Builder builder = WebClient.builder();
|
||||
builder.baseUrl(lmstudioBaseUrl + "/v1/chat/completions");
|
||||
|
||||
if (lmstudioHtaccessUsername != null && !lmstudioHtaccessUsername.isBlank() && lmstudioHtaccessPassword != null
|
||||
&& !lmstudioHtaccessPassword.isBlank()) {
|
||||
String credentials = lmstudioHtaccessUsername + ":" + lmstudioHtaccessPassword;
|
||||
String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
|
||||
builder.defaultHeader(HttpHeaders.AUTHORIZATION, "Basic " + encoded);
|
||||
log.info("LlmRestClient initialized (with HTACCESS auth) - URL: {}/v1/chat/completions, Model: {}",
|
||||
lmstudioBaseUrl, lmstudioModel);
|
||||
} else {
|
||||
log.info("LlmRestClient initialized - URL: {}/v1/chat/completions, Model: {}", lmstudioBaseUrl,
|
||||
lmstudioModel);
|
||||
}
|
||||
|
||||
this.webClient = builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a chat completion request.
|
||||
*
|
||||
* @param systemPrompt
|
||||
* System prompt for context
|
||||
* @param userMessage
|
||||
* User message/question
|
||||
* @return LLM response text, or null on error
|
||||
*/
|
||||
public String chat(String systemPrompt, String userMessage) {
|
||||
return chat(systemPrompt, userMessage, 0.7, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a chat completion request with custom parameters.
|
||||
*
|
||||
* @param systemPrompt
|
||||
* System prompt for context
|
||||
* @param userMessage
|
||||
* User message/question
|
||||
* @param temperature
|
||||
* Temperature for response randomness (0.0-1.0)
|
||||
* @param maxTokens
|
||||
* Maximum tokens in response
|
||||
* @return LLM response text, or null on error
|
||||
*/
|
||||
public String chat(String systemPrompt, String userMessage, double temperature, int maxTokens) {
|
||||
try {
|
||||
Map<String, Object> request = Map.of("model", model, "messages",
|
||||
List.of(Map.of("role", "system", "content", systemPrompt != null ? systemPrompt : ""),
|
||||
Map.of("role", "user", "content", userMessage)),
|
||||
"temperature", temperature, "max_tokens", maxTokens, "stream", false);
|
||||
|
||||
log.info("Sending request to LLM (model: {}, prompt length: {} chars)...", model, userMessage.length());
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
String response = webClient.post().contentType(MediaType.APPLICATION_JSON).bodyValue(request).retrieve()
|
||||
.bodyToMono(String.class).timeout(Duration.ofSeconds(120)).block();
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
log.info("LLM response received in {}ms", duration);
|
||||
log.debug("LLM response payload received ({} chars)", response != null ? response.length() : 0);
|
||||
|
||||
return extractContent(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error calling LLM API: {} - {}", e.getClass().getSimpleName(), e.getMessage());
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Full stack trace:", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple chat without system prompt.
|
||||
*/
|
||||
public String chat(String userMessage) {
|
||||
return chat(null, userMessage);
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
private String extractContent(String response) {
|
||||
if (response == null || response.isBlank()) {
|
||||
log.warn("LLM returned null or blank response");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(response);
|
||||
JsonNode choices = root.path("choices");
|
||||
if (choices.isArray() && !choices.isEmpty()) {
|
||||
String content = choices.get(0).path("message").path("content").asText();
|
||||
if (content == null || content.isBlank()) {
|
||||
log.warn("LLM response content is empty");
|
||||
return null;
|
||||
}
|
||||
String sanitizedContent = sanitizeAssistantContent(content);
|
||||
if (sanitizedContent.isBlank()) {
|
||||
log.warn("LLM response content is empty after sanitization");
|
||||
return null;
|
||||
}
|
||||
return sanitizedContent;
|
||||
}
|
||||
log.warn("Unexpected response structure (no choices): {}", response);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.error("Error parsing LLM response: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String sanitizeAssistantContent(String content) {
|
||||
String sanitized = THINK_BLOCK_PATTERN.matcher(content).replaceAll(" ");
|
||||
sanitized = THINK_TAG_PATTERN.matcher(sanitized).replaceAll(" ");
|
||||
sanitized = sanitized.replace("\r", "");
|
||||
sanitized = sanitized.replaceAll("[ \\t]+", " ");
|
||||
sanitized = sanitized.replaceAll("\\n{3,}", "\n\n");
|
||||
return sanitized.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package de.assecutor.votianlt.config;
|
||||
|
||||
import de.assecutor.votianlt.model.User;
|
||||
import de.assecutor.votianlt.repository.UserRepository;
|
||||
import de.assecutor.votianlt.service.DemoModeService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
public class DataInitializer implements CommandLineRunner {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DataInitializer.class);
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final DemoModeService demoModeService;
|
||||
|
||||
public DataInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder,
|
||||
DemoModeService demoModeService) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.demoModeService = demoModeService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
initializeTestUsers();
|
||||
demoModeService.ensureDemoUser();
|
||||
}
|
||||
|
||||
private void initializeTestUsers() {
|
||||
log.info("Initializing test users...");
|
||||
|
||||
// Admin User
|
||||
if (!userRepository.existsByEmail("admin@votianlt.de")) {
|
||||
User adminUser = new User();
|
||||
adminUser.setEmail("admin@votianlt.de");
|
||||
adminUser.setPassword(passwordEncoder.encode("admin123"));
|
||||
adminUser.setName("Administrator");
|
||||
adminUser.setFirstname("Admin");
|
||||
adminUser.setIsActivated((byte) 1);
|
||||
adminUser.setIsEmailConfirmed((byte) 1);
|
||||
adminUser.setCreatedAt(LocalDateTime.now());
|
||||
adminUser.setUpdatedAt(LocalDateTime.now());
|
||||
adminUser.setRoles(Set.of("USER", "ADMIN"));
|
||||
adminUser.setTwoFactorEnabled(false); // 2FA deaktiviert für Admin
|
||||
|
||||
userRepository.save(adminUser);
|
||||
log.info("Created admin user: admin@votianlt.de / admin123 (2FA enabled)");
|
||||
} else {
|
||||
// Stelle sicher, dass bestehender Admin 2FA deaktiviert hat
|
||||
userRepository.findByEmail("admin@votianlt.de").ifPresent(adminUser -> {
|
||||
if (adminUser.isTwoFactorEnabled()) {
|
||||
adminUser.setTwoFactorEnabled(false);
|
||||
userRepository.save(adminUser);
|
||||
log.info("Updated admin user: 2FA disabled");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
log.info("Test users initialization completed.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.assecutor.votianlt.config;
|
||||
|
||||
import com.vaadin.flow.server.VaadinServiceInitListener;
|
||||
import de.assecutor.votianlt.service.DemoModeService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class DemoSessionCleanupConfig {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DemoSessionCleanupConfig.class);
|
||||
|
||||
@Bean
|
||||
public VaadinServiceInitListener demoSessionCleanupListener(DemoModeService demoModeService) {
|
||||
return event -> event.getSource().addSessionDestroyListener(sessionDestroyEvent -> {
|
||||
try {
|
||||
var wrappedSession = sessionDestroyEvent.getSession().getSession();
|
||||
if (wrappedSession != null) {
|
||||
demoModeService.cleanupAndReleaseIfOwned(wrappedSession.getId());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("Demo session destroy cleanup failed: {}", ex.getMessage(), ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.assecutor.votianlt.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
/**
|
||||
* Jackson configuration for consistent JSON serialization across the
|
||||
* application. Ensures all date/time fields are serialized as ISO 8601 strings.
|
||||
*/
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
/**
|
||||
* Creates a configured ObjectMapper bean that serializes dates as ISO 8601
|
||||
* strings. This bean is used throughout the application for JSON serialization.
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
public ObjectMapper objectMapper() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
// Serialize dates as ISO 8601 strings instead of timestamps/arrays
|
||||
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
return objectMapper;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package de.assecutor.votianlt.config;
|
||||
|
||||
import com.vaadin.flow.component.UI;
|
||||
import com.vaadin.flow.server.ServiceInitEvent;
|
||||
import com.vaadin.flow.server.VaadinServiceInitListener;
|
||||
import de.assecutor.votianlt.model.Language;
|
||||
import de.assecutor.votianlt.security.CustomUserPrincipal;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Sets the user's preferred locale on the UI BEFORE any layout or view is
|
||||
* constructed. Registered via {@code UIInitListener} →
|
||||
* {@code BeforeEnterListener}, which fires prior to the router creating the
|
||||
* layout component tree.
|
||||
*
|
||||
* For authenticated users: Uses the language preference from the user profile.
|
||||
* For anonymous users: Uses the language from the 'votianlt.language' cookie or
|
||||
* falls back to the browser's preferred locale.
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class LocaleVaadinInitListener implements VaadinServiceInitListener {
|
||||
|
||||
private static final String LANGUAGE_COOKIE_NAME = "votianlt.language";
|
||||
|
||||
@Override
|
||||
public void serviceInit(ServiceInitEvent event) {
|
||||
event.getSource().addUIInitListener(uiInitEvent -> {
|
||||
UI ui = uiInitEvent.getUI();
|
||||
ui.addBeforeEnterListener(beforeEnterEvent -> applyLocale(ui));
|
||||
});
|
||||
}
|
||||
|
||||
private void applyLocale(UI ui) {
|
||||
try {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken)) {
|
||||
// Authenticated user: use profile language
|
||||
applyLocaleFromAuthenticatedUser(ui, auth);
|
||||
} else {
|
||||
// Anonymous user: use cookie or browser locale
|
||||
applyLocaleFromCookieOrBrowser(ui);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not apply locale: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void applyLocaleFromAuthenticatedUser(UI ui, Authentication auth) {
|
||||
if (!(auth.getPrincipal() instanceof CustomUserPrincipal cup)) {
|
||||
return;
|
||||
}
|
||||
Language language = cup.getUser().getLanguage();
|
||||
if (language == null) {
|
||||
return;
|
||||
}
|
||||
Locale targetLocale = getLocaleFromLanguage(language);
|
||||
if (!targetLocale.equals(ui.getLocale())) {
|
||||
ui.setLocale(targetLocale);
|
||||
log.debug("Locale set to {} for authenticated user {}", targetLocale, cup.getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
private void applyLocaleFromCookieOrBrowser(UI ui) {
|
||||
Locale targetLocale = null;
|
||||
|
||||
// Try to get locale from cookie first
|
||||
String cookieLanguage = getLanguageFromCookie(ui);
|
||||
if (cookieLanguage != null) {
|
||||
targetLocale = getLocaleFromLanguageCode(cookieLanguage);
|
||||
log.debug("Using locale {} from cookie for anonymous user", targetLocale);
|
||||
}
|
||||
|
||||
// If no cookie, use browser's preferred locale if supported
|
||||
if (targetLocale == null) {
|
||||
targetLocale = getSupportedLocaleFromBrowser(ui);
|
||||
if (targetLocale != null) {
|
||||
log.debug("Using browser locale {} for anonymous user", targetLocale);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the locale if different from current
|
||||
if (targetLocale != null && !targetLocale.equals(ui.getLocale())) {
|
||||
ui.setLocale(targetLocale);
|
||||
log.debug("Locale set to {} for anonymous user", targetLocale);
|
||||
}
|
||||
}
|
||||
|
||||
private String getLanguageFromCookie(UI ui) {
|
||||
try {
|
||||
var request = com.vaadin.flow.server.VaadinRequest.getCurrent();
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Cookie[] cookies = request.getCookies();
|
||||
if (cookies == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Cookie cookie : cookies) {
|
||||
if (LANGUAGE_COOKIE_NAME.equals(cookie.getName())) {
|
||||
return cookie.getValue();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not read language cookie: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Locale getSupportedLocaleFromBrowser(UI ui) {
|
||||
// Get the browser's preferred locales
|
||||
var locale = ui.getSession().getBrowser().getLocale();
|
||||
if (locale == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if the browser locale is supported
|
||||
String language = locale.getLanguage().toLowerCase();
|
||||
return switch (language) {
|
||||
case "de" -> Locale.GERMAN;
|
||||
case "en" -> Locale.ENGLISH;
|
||||
case "fr" -> Locale.FRENCH;
|
||||
case "es" -> Locale.of("es", "ES");
|
||||
default -> null; // Return null to use Vaadin's default
|
||||
};
|
||||
}
|
||||
|
||||
private Locale getLocaleFromLanguage(Language language) {
|
||||
return switch (language) {
|
||||
case DE -> Locale.GERMAN;
|
||||
case EN -> Locale.ENGLISH;
|
||||
case FR -> Locale.FRENCH;
|
||||
case ES -> Locale.of("es", "ES");
|
||||
case TR -> Locale.of("tr", "TR");
|
||||
case PL -> Locale.of("pl", "PL");
|
||||
case RU -> Locale.of("ru", "RU");
|
||||
case EE -> Locale.of("et", "EE");
|
||||
case LV -> Locale.of("lv", "LV");
|
||||
case LT -> Locale.of("lt", "LT");
|
||||
};
|
||||
}
|
||||
|
||||
private Locale getLocaleFromLanguageCode(String languageCode) {
|
||||
if (languageCode == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (languageCode.toUpperCase()) {
|
||||
case "DE" -> Locale.GERMAN;
|
||||
case "EN" -> Locale.ENGLISH;
|
||||
case "FR" -> Locale.FRENCH;
|
||||
case "ES" -> Locale.of("es", "ES");
|
||||
case "TR" -> Locale.of("tr", "TR");
|
||||
case "PL" -> Locale.of("pl", "PL");
|
||||
case "RU" -> Locale.of("ru", "RU");
|
||||
case "EE" -> Locale.of("et", "EE");
|
||||
case "LV" -> Locale.of("lv", "LV");
|
||||
case "LT" -> Locale.of("lt", "LT");
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package de.assecutor.votianlt.config;
|
||||
|
||||
import de.assecutor.votianlt.model.task.*;
|
||||
import de.assecutor.votianlt.model.task.CommentTask;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
|
||||
import org.bson.Document;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
@Configuration
|
||||
public class MongoConfig {
|
||||
|
||||
@Bean
|
||||
public MongoCustomConversions customConversions() {
|
||||
List<Converter<?, ?>> converters = new ArrayList<>();
|
||||
converters.add(new DocumentToBaseTaskConverter());
|
||||
return new MongoCustomConversions(converters);
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
@Slf4j
|
||||
public static class DocumentToBaseTaskConverter implements Converter<Document, BaseTask> {
|
||||
|
||||
@Override
|
||||
public BaseTask convert(Document source) {
|
||||
// Debug logging to see what's in the document
|
||||
log.debug("Converting MongoDB document to BaseTask. Document keys: {}", source.keySet());
|
||||
log.debug("Full document content: {}", source.toJson());
|
||||
|
||||
// Use _class field for type discrimination (MongoDB standard)
|
||||
String className = source.getString("_class");
|
||||
if (className == null) {
|
||||
// Fallback to taskType field if _class is not present
|
||||
String taskType = source.getString("taskType");
|
||||
if (taskType == null) {
|
||||
taskType = source.getString("task_type");
|
||||
}
|
||||
// Map taskType to class name
|
||||
className = mapTaskTypeToClassName(taskType);
|
||||
}
|
||||
|
||||
log.debug("Extracted className: '{}' from document", className);
|
||||
|
||||
BaseTask task;
|
||||
switch (className) {
|
||||
case "de.assecutor.votianlt.model.task.ConfirmationTask":
|
||||
case "ConfirmationTask":
|
||||
log.debug("Creating ConfirmationTask");
|
||||
task = new ConfirmationTask();
|
||||
if (source.containsKey("button_text")) {
|
||||
((ConfirmationTask) task).setButtonText(source.getString("button_text"));
|
||||
}
|
||||
break;
|
||||
case "de.assecutor.votianlt.model.task.SignatureTask":
|
||||
case "SignatureTask":
|
||||
log.debug("Creating SignatureTask");
|
||||
task = new SignatureTask();
|
||||
break;
|
||||
case "de.assecutor.votianlt.model.task.PhotoTask":
|
||||
case "PhotoTask":
|
||||
log.debug("Creating PhotoTask");
|
||||
task = new PhotoTask();
|
||||
if (source.containsKey("min_photo_count")) {
|
||||
((PhotoTask) task).setMinPhotoCount(source.getInteger("min_photo_count"));
|
||||
}
|
||||
if (source.containsKey("max_photo_count")) {
|
||||
((PhotoTask) task).setMaxPhotoCount(source.getInteger("max_photo_count"));
|
||||
}
|
||||
break;
|
||||
case "de.assecutor.votianlt.model.task.TodoListTask":
|
||||
case "TodoListTask":
|
||||
log.debug("Creating TodoListTask");
|
||||
task = new TodoListTask();
|
||||
if (source.containsKey("todo_items")) {
|
||||
// Suppressing unchecked cast warning as MongoDB document structure is validated
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> todoItems = (List<String>) source.get("todo_items");
|
||||
((TodoListTask) task).setTodoItems(todoItems);
|
||||
}
|
||||
break;
|
||||
case "de.assecutor.votianlt.model.task.BarcodeTask":
|
||||
case "BarcodeTask":
|
||||
log.debug("Creating BarcodeTask");
|
||||
task = new BarcodeTask();
|
||||
if (source.containsKey("min_barcode_count")) {
|
||||
((BarcodeTask) task).setMinBarcodeCount(source.getInteger("min_barcode_count"));
|
||||
}
|
||||
if (source.containsKey("max_barcode_count")) {
|
||||
((BarcodeTask) task).setMaxBarcodeCount(source.getInteger("max_barcode_count"));
|
||||
}
|
||||
break;
|
||||
case "de.assecutor.votianlt.model.task.CommentTask":
|
||||
case "CommentTask":
|
||||
log.debug("Creating CommentTask");
|
||||
task = new CommentTask();
|
||||
if (source.containsKey("comment_text")) {
|
||||
((CommentTask) task).setCommentText(source.getString("comment_text"));
|
||||
}
|
||||
if (source.containsKey("required")) {
|
||||
((CommentTask) task).setRequired(source.getBoolean("required", false));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
log.warn("Unknown className '{}', falling back to ConfirmationTask", className);
|
||||
task = new ConfirmationTask(); // fallback
|
||||
break;
|
||||
}
|
||||
|
||||
// Set common fields
|
||||
if (source.containsKey("_id")) {
|
||||
task.setId(source.getObjectId("_id"));
|
||||
}
|
||||
task.setStationId(readObjectId(source, "station_id"));
|
||||
task.setJobId(readObjectId(source, "job_id"));
|
||||
if (source.containsKey("station_order")) {
|
||||
task.setStationOrder(source.getInteger("station_order"));
|
||||
}
|
||||
if (source.containsKey("task_order")) {
|
||||
task.setTaskOrder(source.getInteger("task_order", 0));
|
||||
}
|
||||
if (source.containsKey("description")) {
|
||||
task.setDescription(source.getString("description"));
|
||||
}
|
||||
if (source.containsKey("optional")) {
|
||||
task.setOptional(source.getBoolean("optional", false));
|
||||
}
|
||||
if (source.containsKey("completed")) {
|
||||
task.setCompleted(source.getBoolean("completed", false));
|
||||
}
|
||||
if (source.containsKey("completed_at") && source.get("completed_at") != null) {
|
||||
Object completedAtObj = source.get("completed_at");
|
||||
if (completedAtObj instanceof String) {
|
||||
task.setCompletedAt(
|
||||
LocalDateTime.parse((String) completedAtObj, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
||||
} else if (completedAtObj instanceof java.util.Date) {
|
||||
task.setCompletedAt(((java.util.Date) completedAtObj).toInstant()
|
||||
.atZone(java.time.ZoneId.systemDefault()).toLocalDateTime());
|
||||
}
|
||||
}
|
||||
if (source.containsKey("completed_by")) {
|
||||
task.setCompletedBy(source.getString("completed_by"));
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
private ObjectId readObjectId(Document source, String key) {
|
||||
Object value = source.get(key);
|
||||
if (value instanceof ObjectId objectId) {
|
||||
return objectId;
|
||||
}
|
||||
if (value instanceof String stringValue && ObjectId.isValid(stringValue)) {
|
||||
return new ObjectId(stringValue);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String mapTaskTypeToClassName(String taskType) {
|
||||
if (taskType == null) {
|
||||
return "de.assecutor.votianlt.model.task.ConfirmationTask";
|
||||
}
|
||||
switch (taskType) {
|
||||
case "CONFIRMATION":
|
||||
return "de.assecutor.votianlt.model.task.ConfirmationTask";
|
||||
case "SIGNATURE":
|
||||
return "de.assecutor.votianlt.model.task.SignatureTask";
|
||||
case "PHOTO":
|
||||
return "de.assecutor.votianlt.model.task.PhotoTask";
|
||||
case "TODOLIST":
|
||||
return "de.assecutor.votianlt.model.task.TodoListTask";
|
||||
case "BARCODE":
|
||||
return "de.assecutor.votianlt.model.task.BarcodeTask";
|
||||
case "COMMENT":
|
||||
return "de.assecutor.votianlt.model.task.CommentTask";
|
||||
default:
|
||||
return "de.assecutor.votianlt.model.task.ConfirmationTask";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.assecutor.votianlt.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
/**
|
||||
* Separate configuration for PasswordEncoder to avoid circular dependencies
|
||||
* with VaadinWebSecurity configuration.
|
||||
*/
|
||||
@Configuration
|
||||
public class PasswordEncoderConfig {
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package de.assecutor.votianlt.config;
|
||||
|
||||
import com.vaadin.flow.i18n.I18NProvider;
|
||||
import de.assecutor.votianlt.model.Language;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.ResourceBundle.Control;
|
||||
|
||||
@Component
|
||||
public class TranslationProvider implements I18NProvider {
|
||||
|
||||
public static final String BUNDLE_PREFIX = "messages";
|
||||
private static final Locale DEFAULT_LOCALE = Locale.GERMAN;
|
||||
|
||||
// Custom Control to map language codes to file names
|
||||
private static final Control BUNDLE_CONTROL = new Control() {
|
||||
@Override
|
||||
public List<Locale> getCandidateLocales(String baseName, Locale locale) {
|
||||
// Map Estonian "et" to "ee" file, Latvian "lv" to "lv", Lithuanian "lt" to "lt"
|
||||
String language = locale.getLanguage();
|
||||
|
||||
// Create a locale that matches our file naming convention
|
||||
Locale mappedLocale = switch (language) {
|
||||
case "et" -> Locale.of("ee"); // Estonian -> messages_ee.properties
|
||||
case "lv" -> Locale.of("lv"); // Latvian -> messages_lv.properties
|
||||
case "lt" -> Locale.of("lt"); // Lithuanian -> messages_lt.properties
|
||||
case "ru" -> Locale.of("ru"); // Russian -> messages_ru.properties
|
||||
case "pl" -> Locale.of("pl"); // Polish -> messages_pl.properties
|
||||
case "tr" -> Locale.of("tr"); // Turkish -> messages_tr.properties
|
||||
case "es" -> Locale.of("es"); // Spanish -> messages_es.properties
|
||||
case "fr" -> Locale.of("fr"); // French -> messages_fr.properties
|
||||
case "en" -> Locale.of("en"); // English -> messages_en.properties
|
||||
case "de" -> Locale.of("de"); // German -> messages_de.properties
|
||||
default -> locale;
|
||||
};
|
||||
|
||||
return super.getCandidateLocales(baseName, mappedLocale);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public List<Locale> getProvidedLocales() {
|
||||
return Collections.unmodifiableList(Arrays.asList(Locale.GERMAN, Locale.ENGLISH, Locale.FRENCH,
|
||||
Locale.of("es", "ES"), Locale.of("tr", "TR"), Locale.of("pl", "PL"), Locale.of("ru", "RU"),
|
||||
Locale.of("et", "EE"), Locale.of("lv", "LV"), Locale.of("lt", "LT")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTranslation(String key, Locale locale, Object... params) {
|
||||
if (key == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
String value = findTranslation(key, locale);
|
||||
if (value == null) {
|
||||
return key;
|
||||
}
|
||||
|
||||
if (params.length > 0) {
|
||||
value = MessageFormat.format(value, params);
|
||||
}
|
||||
|
||||
return value;
|
||||
} catch (MissingResourceException e) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
private String findTranslation(String key, Locale locale) {
|
||||
Locale effectiveLocale = locale != null ? locale : DEFAULT_LOCALE;
|
||||
ResourceBundle localizedBundle = ResourceBundle.getBundle(BUNDLE_PREFIX, effectiveLocale, BUNDLE_CONTROL);
|
||||
if (localizedBundle.containsKey(key)) {
|
||||
return localizedBundle.getString(key);
|
||||
}
|
||||
|
||||
if (!DEFAULT_LOCALE.getLanguage().equals(effectiveLocale.getLanguage())) {
|
||||
ResourceBundle germanBundle = ResourceBundle.getBundle(BUNDLE_PREFIX, DEFAULT_LOCALE, BUNDLE_CONTROL);
|
||||
if (germanBundle.containsKey(key)) {
|
||||
return germanBundle.getString(key);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getTranslation(String key, Language language) {
|
||||
Locale locale = switch (language) {
|
||||
case DE -> Locale.GERMAN;
|
||||
case EN -> Locale.ENGLISH;
|
||||
case FR -> Locale.FRENCH;
|
||||
case ES -> Locale.of("es", "ES");
|
||||
case TR -> Locale.of("tr", "TR");
|
||||
case PL -> Locale.of("pl", "PL");
|
||||
case RU -> Locale.of("ru", "RU");
|
||||
case EE -> Locale.of("et", "EE");
|
||||
case LV -> Locale.of("lv", "LV");
|
||||
case LT -> Locale.of("lt", "LT");
|
||||
};
|
||||
return getTranslation(key, locale);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package de.assecutor.votianlt.controller;
|
||||
|
||||
import de.assecutor.votianlt.model.LocationPosition;
|
||||
import de.assecutor.votianlt.service.LocationService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* REST-Controller für Location-bezogene API-Endpunkte.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/location")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LocationApiController {
|
||||
|
||||
private final LocationService locationService;
|
||||
|
||||
/**
|
||||
* Gibt die aktuelle Position eines App-Nutzers zurück.
|
||||
*
|
||||
* @param appUserId
|
||||
* die ID des App-Nutzers
|
||||
* @return die aktuelle Position oder 404 wenn keine vorhanden
|
||||
*/
|
||||
@GetMapping("/{appUserId}")
|
||||
public ResponseEntity<LocationResponse> getCurrentPosition(@PathVariable String appUserId) {
|
||||
LocationPosition position = locationService.getLatestPosition(appUserId);
|
||||
|
||||
if (position == null || position.getLatitude() == null || position.getLongitude() == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
LocationResponse response = new LocationResponse();
|
||||
response.setLatitude(position.getLatitude());
|
||||
response.setLongitude(position.getLongitude());
|
||||
response.setAccuracy(position.getAccuracy());
|
||||
response.setSpeed(position.getSpeed());
|
||||
response.setTimestamp(position.getTimestamp());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class LocationResponse {
|
||||
private Double latitude;
|
||||
private Double longitude;
|
||||
private Double accuracy;
|
||||
private Double speed;
|
||||
private Instant timestamp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package de.assecutor.votianlt.controller;
|
||||
|
||||
import de.assecutor.votianlt.model.Message;
|
||||
import de.assecutor.votianlt.model.MessageContentType;
|
||||
import de.assecutor.votianlt.model.MessageOrigin;
|
||||
import de.assecutor.votianlt.service.MessageService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* REST API controller for message operations. Provides endpoints for sending
|
||||
* messages, retrieving messages, and marking messages as read.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/messages")
|
||||
@Slf4j
|
||||
public class MessageApiController {
|
||||
|
||||
private final MessageService messageService;
|
||||
|
||||
public MessageApiController(MessageService messageService) {
|
||||
this.messageService = messageService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a general message to a client POST /api/messages/send Body: { "content":
|
||||
* "message text", "receiver": "appUserId", "contentType": "TEXT|IMAGE" }
|
||||
*/
|
||||
@PostMapping("/send")
|
||||
public ResponseEntity<Message> sendGeneralMessage(@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
String content = request.get("content");
|
||||
String receiver = request.get("receiver");
|
||||
MessageContentType contentType = resolveContentType(request.get("contentType"));
|
||||
|
||||
if (content == null || content.isBlank() || receiver == null || receiver.isBlank()) {
|
||||
log.warn("Invalid message request: missing required fields");
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
Message message = messageService.sendGeneralMessageToClient(content, receiver, contentType);
|
||||
log.info("General message sent to AppUser '{}'", receiver);
|
||||
return ResponseEntity.ok(message);
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Invalid general message request: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Error sending general message: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a job-related message to a client POST /api/messages/send-job-message
|
||||
* Body: { "content": "message text", "receiver": "appUserId", "jobId": "job
|
||||
* id", "jobNumber": "job number", "contentType": "TEXT|IMAGE" }
|
||||
*/
|
||||
@PostMapping("/send-job-message")
|
||||
public ResponseEntity<Message> sendJobMessage(@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
String content = request.get("content");
|
||||
String receiver = request.get("receiver");
|
||||
String jobIdStr = request.get("jobId");
|
||||
String jobNumber = request.get("jobNumber");
|
||||
MessageContentType contentType = resolveContentType(request.get("contentType"));
|
||||
|
||||
if (content == null || content.isBlank() || receiver == null || receiver.isBlank() || jobIdStr == null
|
||||
|| jobIdStr.isBlank()) {
|
||||
log.warn("Invalid job message request: missing required fields");
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
ObjectId jobId = new ObjectId(jobIdStr);
|
||||
Message message = messageService.sendJobMessageToClient(content, receiver, contentType, jobId, jobNumber);
|
||||
log.info("Job-related message sent to AppUser '{}' for job {}", receiver, jobNumber);
|
||||
return ResponseEntity.ok(message);
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Invalid job message request: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Error sending job message: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all messages for a specific receiver GET
|
||||
* /api/messages/receiver/{username}
|
||||
*/
|
||||
@GetMapping("/receiver/{username}")
|
||||
public ResponseEntity<List<Message>> getMessagesForReceiver(@PathVariable String username) {
|
||||
try {
|
||||
List<Message> messages = messageService.getMessagesForReceiver(username);
|
||||
return ResponseEntity.ok(messages);
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving messages for receiver {}: {}", username, e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unread messages for a specific receiver GET
|
||||
* /api/messages/receiver/{username}/unread
|
||||
*/
|
||||
@GetMapping("/receiver/{username}/unread")
|
||||
public ResponseEntity<List<Message>> getUnreadMessagesForReceiver(@PathVariable String username) {
|
||||
try {
|
||||
List<Message> messages = messageService.getUnreadMessagesForReceiver(username);
|
||||
return ResponseEntity.ok(messages);
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving unread messages for receiver {}: {}", username, e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unread message count for a specific receiver GET
|
||||
* /api/messages/receiver/{username}/unread-count
|
||||
*/
|
||||
@GetMapping("/receiver/{username}/unread-count")
|
||||
public ResponseEntity<Map<String, Long>> getUnreadMessageCount(@PathVariable String username) {
|
||||
try {
|
||||
long count = messageService.getUnreadMessageCount(username);
|
||||
return ResponseEntity.ok(Map.of("count", count));
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving unread message count for receiver {}: {}", username, e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all messages related to a specific job GET /api/messages/job/{jobId}
|
||||
*/
|
||||
@GetMapping("/job/{jobId}")
|
||||
public ResponseEntity<List<Message>> getMessagesForJob(@PathVariable String jobId) {
|
||||
try {
|
||||
ObjectId objectId = new ObjectId(jobId);
|
||||
List<Message> messages = messageService.getMessagesForJob(objectId);
|
||||
return ResponseEntity.ok(messages);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Invalid jobId format: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving messages for job {}: {}", jobId, e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all messages (for admin/overview) GET /api/messages/all
|
||||
*/
|
||||
@GetMapping("/all")
|
||||
public ResponseEntity<List<Message>> getAllMessages() {
|
||||
try {
|
||||
List<Message> messages = messageService.getAllMessages();
|
||||
return ResponseEntity.ok(messages);
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving all messages: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages by origin (incoming/outgoing/server) GET
|
||||
* /api/messages/origin/{origin}
|
||||
*/
|
||||
@GetMapping("/origin/{origin}")
|
||||
public ResponseEntity<List<Message>> getMessagesByOrigin(@PathVariable String origin) {
|
||||
try {
|
||||
MessageOrigin messageOrigin = MessageOrigin.valueOf(origin.toUpperCase());
|
||||
List<Message> messages = messageService.getMessagesByOrigin(messageOrigin);
|
||||
return ResponseEntity.ok(messages);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Invalid origin: {}", origin);
|
||||
return ResponseEntity.badRequest().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving messages by origin {}: {}", origin, e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a message as read PUT /api/messages/{messageId}/mark-read
|
||||
*/
|
||||
@PutMapping("/{messageId}/mark-read")
|
||||
public ResponseEntity<Void> markMessageAsRead(@PathVariable String messageId) {
|
||||
try {
|
||||
ObjectId objectId = new ObjectId(messageId);
|
||||
messageService.markAsRead(objectId);
|
||||
return ResponseEntity.ok().build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Invalid messageId format: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Error marking message as read {}: {}", messageId, e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a message DELETE /api/messages/{messageId}
|
||||
*/
|
||||
@DeleteMapping("/{messageId}")
|
||||
public ResponseEntity<Void> deleteMessage(@PathVariable String messageId) {
|
||||
try {
|
||||
ObjectId objectId = new ObjectId(messageId);
|
||||
messageService.deleteMessage(objectId);
|
||||
return ResponseEntity.ok().build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Invalid messageId format: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Error deleting message {}: {}", messageId, e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
private MessageContentType resolveContentType(String rawValue) {
|
||||
if (rawValue == null || rawValue.isBlank()) {
|
||||
return MessageContentType.TEXT;
|
||||
}
|
||||
|
||||
try {
|
||||
return MessageContentType.valueOf(rawValue.trim().toUpperCase());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new IllegalArgumentException("Unsupported contentType: " + rawValue, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
package de.assecutor.votianlt.controller;
|
||||
|
||||
import de.assecutor.votianlt.dto.AppLoginRequest;
|
||||
import de.assecutor.votianlt.dto.AppLoginResponse;
|
||||
import de.assecutor.votianlt.dto.ChatMessageInboundPayload;
|
||||
import de.assecutor.votianlt.dto.JobWithRelatedDataDTO;
|
||||
import de.assecutor.votianlt.model.AppUser;
|
||||
import de.assecutor.votianlt.model.CargoItem;
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.task.BaseTask;
|
||||
import de.assecutor.votianlt.pages.service.AppUserService;
|
||||
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||
import de.assecutor.votianlt.repository.CargoItemRepository;
|
||||
import de.assecutor.votianlt.repository.JobRepository;
|
||||
import de.assecutor.votianlt.repository.PhotoRepository;
|
||||
import de.assecutor.votianlt.repository.TaskRepository;
|
||||
import de.assecutor.votianlt.repository.BarcodeRepository;
|
||||
import de.assecutor.votianlt.repository.SignatureRepository;
|
||||
import de.assecutor.votianlt.repository.CommentRepository;
|
||||
import de.assecutor.votianlt.model.Photo;
|
||||
import de.assecutor.votianlt.model.Barcode;
|
||||
import de.assecutor.votianlt.model.Signature;
|
||||
import de.assecutor.votianlt.model.Comment;
|
||||
import de.assecutor.votianlt.service.JobHistoryService;
|
||||
import de.assecutor.votianlt.service.JobUpdateBroadcaster;
|
||||
import de.assecutor.votianlt.service.EmailService;
|
||||
import de.assecutor.votianlt.service.MessageService;
|
||||
import de.assecutor.votianlt.service.TaskAssignmentService;
|
||||
import de.assecutor.votianlt.model.JobStatus;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import de.assecutor.votianlt.messaging.MessagingPublisher;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
/**
|
||||
* Message controller for handling real-time communication with apps. Provides
|
||||
* endpoints for sending and receiving messages via WebSocket.
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class MessageController {
|
||||
|
||||
private final MessagingPublisher messagingPublisher;
|
||||
|
||||
private final AppUserRepository appUserRepository;
|
||||
|
||||
private final AppUserService appUserService;
|
||||
|
||||
private final JobRepository jobRepository;
|
||||
|
||||
private final CargoItemRepository cargoItemRepository;
|
||||
|
||||
private final TaskRepository taskRepository;
|
||||
private final PhotoRepository photoRepository;
|
||||
private final BarcodeRepository barcodeRepository;
|
||||
private final SignatureRepository signatureRepository;
|
||||
private final CommentRepository commentRepository;
|
||||
private final JobHistoryService jobHistoryService;
|
||||
private final JobUpdateBroadcaster jobUpdateBroadcaster;
|
||||
private final EmailService emailService;
|
||||
private final MessageService messageService;
|
||||
private final TaskAssignmentService taskAssignmentService;
|
||||
|
||||
public MessageController(MessagingPublisher messagingPublisher, AppUserRepository appUserRepository,
|
||||
AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository,
|
||||
TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository,
|
||||
SignatureRepository signatureRepository, CommentRepository commentRepository,
|
||||
JobHistoryService jobHistoryService, JobUpdateBroadcaster jobUpdateBroadcaster, EmailService emailService,
|
||||
MessageService messageService, TaskAssignmentService taskAssignmentService) {
|
||||
this.messagingPublisher = messagingPublisher;
|
||||
this.appUserRepository = appUserRepository;
|
||||
this.appUserService = appUserService;
|
||||
this.jobRepository = jobRepository;
|
||||
this.cargoItemRepository = cargoItemRepository;
|
||||
this.taskRepository = taskRepository;
|
||||
this.photoRepository = photoRepository;
|
||||
this.barcodeRepository = barcodeRepository;
|
||||
this.signatureRepository = signatureRepository;
|
||||
this.commentRepository = commentRepository;
|
||||
this.jobHistoryService = jobHistoryService;
|
||||
this.jobUpdateBroadcaster = jobUpdateBroadcaster;
|
||||
this.emailService = emailService;
|
||||
this.messageService = messageService;
|
||||
this.taskAssignmentService = taskAssignmentService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication endpoint for mobile app users via WebSocket. Client sends to
|
||||
* /server/login with payload { email, password }. Returns the result to the
|
||||
* caller (MessagingConfig) which handles session registration and response
|
||||
* sending.
|
||||
*/
|
||||
public AppLoginResponse handleAppLogin(AppLoginRequest request) {
|
||||
if (request == null || request.getEmail() == null || request.getPassword() == null
|
||||
|| request.getEmail().isBlank() || request.getPassword().isBlank()) {
|
||||
return new AppLoginResponse(false, "E-Mail und Passwort sind erforderlich", null);
|
||||
}
|
||||
|
||||
AppUser user = appUserRepository.findByEmail(request.getEmail());
|
||||
if (user == null) {
|
||||
return new AppLoginResponse(false, "Benutzer nicht gefunden", null);
|
||||
}
|
||||
|
||||
boolean ok = appUserService.verifyPassword(request.getPassword(), user.getPassword());
|
||||
if (!ok) {
|
||||
return new AppLoginResponse(false, "Ungültige Anmeldedaten", null);
|
||||
}
|
||||
|
||||
return new AppLoginResponse(true, "Anmeldung erfolgreich", user.getIdAsString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve jobs assigned to a specific app user with related cargo items and
|
||||
* tasks. The appUserId is determined from the authenticated WebSocket session.
|
||||
* Response is sent back on /client/jobs.
|
||||
*/
|
||||
public void handleGetAssignedJobs(String appUserId) {
|
||||
if (appUserId == null || appUserId.isBlank()) {
|
||||
log.warn("[JOBS] appUserId is null or blank, cannot retrieve jobs");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("[JOBS] Retrieving assigned jobs for appUserId: {}", appUserId);
|
||||
|
||||
List<Job> assignedJobs = jobRepository.findByAppUser(appUserId);
|
||||
log.info("[JOBS] Found {} jobs for appUserId: {}", assignedJobs.size(), appUserId);
|
||||
|
||||
List<JobWithRelatedDataDTO> jobsWithRelatedData = assignedJobs.stream().map(job -> {
|
||||
List<CargoItem> cargoItems = cargoItemRepository.findByJobId(job.getId());
|
||||
List<BaseTask> tasks = taskAssignmentService.findTasksForJob(job);
|
||||
return new JobWithRelatedDataDTO(job, cargoItems, tasks);
|
||||
}).toList();
|
||||
|
||||
log.info("[JOBS] Publishing {} jobs to client {} on topic /client/jobs", jobsWithRelatedData.size(), appUserId);
|
||||
messagingPublisher.publishAsJson(appUserId, "jobs", jobsWithRelatedData);
|
||||
log.info("[JOBS] Jobs published successfully for client {}", appUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Report generic task completion from apps. Client sends to /app/task/completed
|
||||
* with payload { taskId, completedBy?, note? }. Broadcasts to
|
||||
* /topic/task-updates and /topic/tasks/{taskId}. This endpoint accepts any task
|
||||
* type (fallback for GENERIC or unknown types).
|
||||
*/
|
||||
public void handleTaskCompleted(Map<String, Object> payload) {
|
||||
// Backward-compatible entry point: extract taskType from payload (if present)
|
||||
// and delegate to the overloaded handler with explicit type.
|
||||
String taskType = null;
|
||||
try {
|
||||
Object tt = payload != null ? payload.get("taskType") : null;
|
||||
if (tt != null)
|
||||
taskType = tt.toString();
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not extract taskType from payload: {}", e.getMessage());
|
||||
}
|
||||
handleTaskCompleted(payload, taskType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Central dispatcher for task_completed messages. Decides handling based on
|
||||
* taskType. PHOTO and CONFIRMATION are routed to specialized handlers; others
|
||||
* go to generic processing.
|
||||
*/
|
||||
public void handleTaskCompleted(Map<String, Object> payload, String taskType) {
|
||||
String key = taskType == null ? "" : taskType.trim().toUpperCase();
|
||||
|
||||
switch (key) {
|
||||
case "PHOTO" -> processPhotoTaskCompletion(payload);
|
||||
case "CONFIRMATION" -> processConfirmationTaskCompletion(payload);
|
||||
case "SIGNATURE" -> processSignatureTaskCompletion(payload);
|
||||
case "TODOLIST" -> processTodoListTaskCompletion(payload);
|
||||
case "BARCODE" -> processBarcodeTaskCompletion(payload);
|
||||
case "COMMENT" -> processCommentTaskCompletion(payload);
|
||||
default -> log.error("[TASK] Unknown taskType: {}", taskType);
|
||||
}
|
||||
}
|
||||
|
||||
private void processConfirmationTaskCompletion(Map<String, Object> payload) {
|
||||
Object taskId = payload.get("taskId");
|
||||
completeTaskWithHistory(taskId, "Bestätigung durchgeführt");
|
||||
}
|
||||
|
||||
private void processTodoListTaskCompletion(Map<String, Object> payload) {
|
||||
Object taskId = payload.get("taskId");
|
||||
completeTaskWithHistory(taskId, "Alle To-Do-Elemente abgehakt");
|
||||
}
|
||||
|
||||
private void processBarcodeTaskCompletion(Map<String, Object> payload) {
|
||||
Object taskId = payload.get("taskId");
|
||||
try {
|
||||
var opt = taskRepository.findById(new ObjectId(taskId.toString()));
|
||||
if (opt.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
BaseTask task = opt.get();
|
||||
|
||||
String extraDataSummary = null;
|
||||
Object extra = payload.get("extraData");
|
||||
if (extra instanceof Map<?, ?> extraData) {
|
||||
Object barcodesObj = extraData.get("barcodes");
|
||||
if (barcodesObj instanceof List<?> barcodesList) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> barcodes = (List<String>) barcodesList;
|
||||
|
||||
if (!barcodes.isEmpty()) {
|
||||
for (String barcodeString : barcodes) {
|
||||
String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown";
|
||||
Barcode barcodeEntry = new Barcode(new ObjectId(taskId.toString()), barcodeString,
|
||||
completedBy);
|
||||
barcodeRepository.save(barcodeEntry);
|
||||
}
|
||||
extraDataSummary = barcodes.size() + " Barcode(s) gescannt: "
|
||||
+ String.join(", ", barcodes.subList(0, Math.min(3, barcodes.size())))
|
||||
+ (barcodes.size() > 3 ? "..." : "");
|
||||
} else {
|
||||
extraDataSummary = "Keine Barcodes gescannt";
|
||||
}
|
||||
} else {
|
||||
extraDataSummary = "Barcode-Daten fehlerhaft";
|
||||
}
|
||||
} else {
|
||||
extraDataSummary = "Keine Extra-Daten";
|
||||
}
|
||||
|
||||
completeTaskWithHistory(taskId, extraDataSummary);
|
||||
} catch (Exception ex) {
|
||||
log.error("[TASK] Barcode completion error: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void processSignatureTaskCompletion(Map<String, Object> payload) {
|
||||
Object taskId = payload.get("taskId");
|
||||
try {
|
||||
var opt = taskRepository.findById(new ObjectId(taskId.toString()));
|
||||
if (opt.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
BaseTask task = opt.get();
|
||||
|
||||
String extraDataSummary = null;
|
||||
Object extra = payload.get("extraData");
|
||||
if (extra instanceof Map<?, ?> extraData) {
|
||||
Object signatureSvgObj = extraData.get("signatureSvg");
|
||||
if (signatureSvgObj instanceof String signatureSvg) {
|
||||
if (!signatureSvg.isBlank()) {
|
||||
String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown";
|
||||
Signature signatureEntry = new Signature(new ObjectId(taskId.toString()), signatureSvg,
|
||||
completedBy);
|
||||
signatureRepository.save(signatureEntry);
|
||||
extraDataSummary = "Unterschrift erfasst (SVG, " + signatureSvg.length() + " Zeichen)";
|
||||
} else {
|
||||
extraDataSummary = "Leere Unterschrift";
|
||||
}
|
||||
} else {
|
||||
extraDataSummary = "Unterschrift-Daten fehlerhaft";
|
||||
}
|
||||
} else {
|
||||
extraDataSummary = "Keine Extra-Daten";
|
||||
}
|
||||
|
||||
completeTaskWithHistory(taskId, extraDataSummary);
|
||||
} catch (Exception ex) {
|
||||
log.error("[TASK] Signature completion error: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void processPhotoTaskCompletion(Map<String, Object> payload) {
|
||||
Object taskId = payload.get("taskId");
|
||||
try {
|
||||
var opt = taskRepository.findById(new ObjectId(taskId.toString()));
|
||||
if (opt.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
BaseTask task = opt.get();
|
||||
|
||||
String extraDataSummary = null;
|
||||
Object extra = payload.get("extraData");
|
||||
if (extra instanceof Map<?, ?> extraData) {
|
||||
Object photosObj = extraData.get("photos");
|
||||
if (photosObj instanceof List<?> photosList) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> photos = (List<String>) photosList;
|
||||
|
||||
if (!photos.isEmpty()) {
|
||||
for (String photoString : photos) {
|
||||
String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown";
|
||||
Photo photoEntry = new Photo(new ObjectId(taskId.toString()), photoString, completedBy);
|
||||
photoRepository.save(photoEntry);
|
||||
}
|
||||
extraDataSummary = photos.size() + " Foto(s) aufgenommen";
|
||||
} else {
|
||||
extraDataSummary = "Keine Fotos aufgenommen";
|
||||
}
|
||||
} else {
|
||||
extraDataSummary = "Foto-Daten fehlerhaft";
|
||||
}
|
||||
} else {
|
||||
extraDataSummary = "Keine Extra-Daten";
|
||||
}
|
||||
|
||||
completeTaskWithHistory(taskId, extraDataSummary);
|
||||
} catch (Exception ex) {
|
||||
log.error("[TASK] Photo completion error: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void processCommentTaskCompletion(Map<String, Object> payload) {
|
||||
Object taskId = payload.get("taskId");
|
||||
try {
|
||||
var opt = taskRepository.findById(new ObjectId(taskId.toString()));
|
||||
if (opt.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
BaseTask task = opt.get();
|
||||
|
||||
String extraDataSummary = null;
|
||||
Object extra = payload.get("extraData");
|
||||
if (extra instanceof Map<?, ?> extraData) {
|
||||
Object commentTextObj = extraData.get("commentText");
|
||||
if (commentTextObj instanceof String commentText && !commentText.isBlank()) {
|
||||
String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown";
|
||||
Comment commentEntry = new Comment(new ObjectId(taskId.toString()), commentText, completedBy);
|
||||
commentRepository.save(commentEntry);
|
||||
extraDataSummary = "Kommentar: " + commentText;
|
||||
} else {
|
||||
extraDataSummary = "Kommentar abgegeben (leer)";
|
||||
}
|
||||
} else {
|
||||
extraDataSummary = "Kommentar abgegeben";
|
||||
}
|
||||
|
||||
completeTaskWithHistory(taskId, extraDataSummary);
|
||||
} catch (Exception ex) {
|
||||
log.error("[TASK] Comment completion error: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void completeTaskWithHistory(Object tid, String extraDataSummary) {
|
||||
String taskIdStr = tid.toString();
|
||||
try {
|
||||
ObjectId taskId = new ObjectId(taskIdStr);
|
||||
var opt = taskRepository.findById(taskId);
|
||||
if (opt.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
BaseTask task = opt.get();
|
||||
task.setCompleted(true);
|
||||
task.setCompletedAt(LocalDateTime.now());
|
||||
taskRepository.save(task);
|
||||
Optional<Job> jobOpt = taskAssignmentService.findJobForTask(task);
|
||||
if (jobOpt.isEmpty()) {
|
||||
log.warn("[TASK] Could not resolve job for task {}", taskIdStr);
|
||||
return;
|
||||
}
|
||||
ObjectId jobId = jobOpt.get().getId();
|
||||
|
||||
// Log detailed task completion in job history
|
||||
try {
|
||||
String taskType = task.getTaskType() != null ? task.getTaskType().toString() : "Unknown";
|
||||
String taskDisplayName = task.getDisplayName() != null ? task.getDisplayName() : taskType;
|
||||
String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown";
|
||||
jobHistoryService.logTaskCompletion(jobId, taskType, taskIdStr, completedBy, taskDisplayName,
|
||||
extraDataSummary);
|
||||
} catch (Exception e) {
|
||||
// Ignore history logging errors
|
||||
}
|
||||
|
||||
// Send email notification for task completion
|
||||
try {
|
||||
String taskType = task.getTaskType() != null ? task.getTaskType().toString() : "Unknown";
|
||||
String completedBy = task.getCompletedBy() != null ? task.getCompletedBy() : "Unknown";
|
||||
emailService.sendTaskCompletionNotification(jobId, taskType, taskIdStr, completedBy);
|
||||
checkAndHandleJobCompletion(jobId, completedBy);
|
||||
} catch (Exception e) {
|
||||
// Ignore email notification errors
|
||||
}
|
||||
|
||||
jobUpdateBroadcaster.broadcast(jobId);
|
||||
} catch (Exception ex) {
|
||||
log.error("[TASK] Completion error: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAndHandleJobCompletion(ObjectId jobId, String completedBy) {
|
||||
try {
|
||||
var allTasks = taskAssignmentService.findTasksForJob(jobId);
|
||||
if (allTasks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var mandatoryTasks = allTasks.stream().filter(task -> !task.isOptional()).toList();
|
||||
if (mandatoryTasks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
boolean allCompleted = mandatoryTasks.stream().allMatch(task -> task.isCompleted());
|
||||
|
||||
if (allCompleted) {
|
||||
updateJobStatusToCompleted(jobId);
|
||||
try {
|
||||
emailService.sendJobCompletionNotification(jobId, completedBy);
|
||||
} catch (Exception e) {
|
||||
// Ignore email notification errors
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore job completion check errors
|
||||
}
|
||||
}
|
||||
|
||||
private void updateJobStatusToCompleted(ObjectId jobId) {
|
||||
try {
|
||||
Optional<Job> jobOpt = jobRepository.findById(jobId);
|
||||
if (jobOpt.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Job job = jobOpt.get();
|
||||
if (job.getStatus() != JobStatus.COMPLETED) {
|
||||
job.setStatus(JobStatus.COMPLETED);
|
||||
job.setUpdatedAt(LocalDateTime.now());
|
||||
jobRepository.save(job);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore job status update errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming message from a client via WebSocket. Client sends to
|
||||
* /server/message with payload: { "content": "message payload", "contentType":
|
||||
* "TEXT|IMAGE", "jobId": "optional job id", "jobNumber": "optional job number"
|
||||
* }
|
||||
*
|
||||
* The appUserId is determined from the authenticated WebSocket session.
|
||||
*/
|
||||
public void handleIncomingMessage(String appUserId, Map<String, Object> payload) {
|
||||
try {
|
||||
if (appUserId == null || appUserId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
payload.put("receiver", appUserId);
|
||||
ChatMessageInboundPayload inboundPayload = ChatMessageInboundPayload.fromPayload(payload);
|
||||
messageService.receiveMessageFromClient(inboundPayload);
|
||||
} catch (Exception e) {
|
||||
// Ignore message handling errors
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
package de.assecutor.votianlt.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class AppLoginRequest {
|
||||
private String email;
|
||||
private String password;
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
package de.assecutor.votianlt.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AppLoginResponse {
|
||||
private boolean success;
|
||||
private String message;
|
||||
/**
|
||||
* Only populated on success, for internal server-side routing. Not sent to
|
||||
* client.
|
||||
*/
|
||||
private String appUserId;
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,76 @@
|
||||
package de.assecutor.votianlt.dto;
|
||||
|
||||
import de.assecutor.votianlt.model.MessageContentType;
|
||||
import java.util.Map;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
/**
|
||||
* Normalized payload for chat messages sent by mobile clients via WebSocket.
|
||||
* receiver = AppUser ID (clientId) extracted from topic
|
||||
*/
|
||||
public record ChatMessageInboundPayload(String receiver, String content, MessageContentType contentType, ObjectId jobId,
|
||||
String jobNumber) {
|
||||
|
||||
public ChatMessageInboundPayload {
|
||||
contentType = contentType != null ? contentType : MessageContentType.TEXT;
|
||||
}
|
||||
|
||||
public static ChatMessageInboundPayload fromPayload(Map<String, Object> payload) {
|
||||
if (payload == null) {
|
||||
throw new IllegalArgumentException("payload must not be null");
|
||||
}
|
||||
|
||||
String receiver = extractRequiredString(payload, "receiver");
|
||||
String content = extractRequiredString(payload, "content");
|
||||
MessageContentType contentType = extractContentType(payload.get("contentType"));
|
||||
ObjectId jobId = extractObjectId(payload.get("jobId"), "jobId");
|
||||
String jobNumber = extractOptionalString(payload.get("jobNumber"));
|
||||
|
||||
return new ChatMessageInboundPayload(receiver, content, contentType, jobId, jobNumber);
|
||||
}
|
||||
|
||||
public boolean hasJobContext() {
|
||||
return jobId != null;
|
||||
}
|
||||
|
||||
private static String extractRequiredString(Map<String, Object> payload, String key) {
|
||||
Object value = payload.get(key);
|
||||
String asString = value != null ? value.toString().trim() : null;
|
||||
if (asString == null || asString.isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing required field '%s'".formatted(key));
|
||||
}
|
||||
return asString;
|
||||
}
|
||||
|
||||
private static String extractOptionalString(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String asString = value.toString().trim();
|
||||
return asString.isEmpty() ? null : asString;
|
||||
}
|
||||
|
||||
private static ObjectId extractObjectId(Object value, String fieldName) {
|
||||
String candidate = extractOptionalString(value);
|
||||
if (candidate == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new ObjectId(candidate);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new IllegalArgumentException("Field '%s' must be a valid MongoDB ObjectId".formatted(fieldName), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static MessageContentType extractContentType(Object value) {
|
||||
String candidate = extractOptionalString(value);
|
||||
if (candidate == null) {
|
||||
return MessageContentType.TEXT;
|
||||
}
|
||||
try {
|
||||
return MessageContentType.valueOf(candidate.trim().toUpperCase());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new IllegalArgumentException("Unsupported contentType '%s'".formatted(candidate), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,22 @@
|
||||
package de.assecutor.votianlt.dto;
|
||||
|
||||
import de.assecutor.votianlt.model.Message;
|
||||
import de.assecutor.votianlt.model.MessageContentType;
|
||||
import de.assecutor.votianlt.model.MessageOrigin;
|
||||
import de.assecutor.votianlt.model.MessageType;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Outbound chat message payload published to subscribers. The receiver is
|
||||
* implicit from the WebSocket session (/client/message)
|
||||
*/
|
||||
public record ChatMessageOutboundPayload(String messageId, String content, MessageContentType contentType,
|
||||
MessageOrigin origin, MessageType messageType, LocalDateTime createdAt, String jobId, String jobNumber,
|
||||
boolean read) {
|
||||
|
||||
public static ChatMessageOutboundPayload fromMessage(Message message) {
|
||||
return new ChatMessageOutboundPayload(message.getIdAsString(), message.getContent(), message.getContentType(),
|
||||
message.getOrigin(), message.getMessageType(), message.getCreatedAt(), message.getJobIdAsString(),
|
||||
message.getJobNumber(), message.isRead());
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
package de.assecutor.votianlt.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* DTO for summarizing message conversations by client
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ClientMessageSummary {
|
||||
|
||||
private String clientId;
|
||||
private String clientName;
|
||||
private String clientEmail;
|
||||
private int totalMessages;
|
||||
private int unreadCount;
|
||||
private LocalDateTime lastMessageDate;
|
||||
private String lastMessagePreview;
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
package de.assecutor.votianlt.dto;
|
||||
|
||||
import de.assecutor.votianlt.model.CargoItem;
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.task.BaseTask;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* DTO for returning job data with related cargo items and tasks. This combines
|
||||
* Job entity with its associated CargoItems and TaskEntries.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class JobWithRelatedDataDTO {
|
||||
private Job job;
|
||||
private List<CargoItem> cargoItems;
|
||||
private List<BaseTask> tasks;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.assecutor.votianlt.event;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* Event published when message read status changes (e.g., messages marked as
|
||||
* read) This allows UI components like the sidebar badge to update accordingly
|
||||
*/
|
||||
public class MessageReadStatusChangedEvent extends ApplicationEvent {
|
||||
|
||||
public MessageReadStatusChangedEvent(Object source) {
|
||||
super(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.assecutor.votianlt.event;
|
||||
|
||||
import de.assecutor.votianlt.model.Message;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* Event published when a new message is received from a client
|
||||
*/
|
||||
public class MessageReceivedEvent extends ApplicationEvent {
|
||||
|
||||
private final Message message;
|
||||
|
||||
public MessageReceivedEvent(Object source, Message message) {
|
||||
super(source);
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Message getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.assecutor.votianlt.mcp.config;
|
||||
|
||||
import de.assecutor.votianlt.mcp.tools.JobQueryTool;
|
||||
import de.assecutor.votianlt.mcp.tools.JobStatisticsTool;
|
||||
import de.assecutor.votianlt.mcp.tools.TaskCompletionTool;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Configuration for the MCP (Model Context Protocol) server. Registers all MCP
|
||||
* tools for job statistics and queries.
|
||||
*/
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class McpServerConfig {
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider jobStatisticsToolProvider(JobStatisticsTool jobStatisticsTool) {
|
||||
log.info("Registering JobStatisticsTool for MCP server");
|
||||
return MethodToolCallbackProvider.builder().toolObjects(jobStatisticsTool).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider jobQueryToolProvider(JobQueryTool jobQueryTool) {
|
||||
log.info("Registering JobQueryTool for MCP server");
|
||||
return MethodToolCallbackProvider.builder().toolObjects(jobQueryTool).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider taskCompletionToolProvider(TaskCompletionTool taskCompletionTool) {
|
||||
log.info("Registering TaskCompletionTool for MCP server");
|
||||
return MethodToolCallbackProvider.builder().toolObjects(taskCompletionTool).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package de.assecutor.votianlt.mcp.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* DTO for customer revenue results.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CustomerRevenueResult {
|
||||
|
||||
private String customer;
|
||||
private BigDecimal revenue;
|
||||
private long jobCount;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.assecutor.votianlt.mcp.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* DTO for job query results returned by MCP tools.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class JobQueryResult {
|
||||
|
||||
private String jobId;
|
||||
private String jobNumber;
|
||||
private String status;
|
||||
private String statusDisplayName;
|
||||
private String customer;
|
||||
private String pickupCity;
|
||||
private String deliveryCity;
|
||||
private LocalDate pickupDate;
|
||||
private LocalDate deliveryDate;
|
||||
private BigDecimal price;
|
||||
private LocalDateTime createdAt;
|
||||
private String assignedAppUser;
|
||||
private boolean digitalProcessing;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package de.assecutor.votianlt.mcp.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* DTO for job statistics results returned by MCP tools.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class JobStatisticsResult {
|
||||
|
||||
private Map<String, Long> countsByStatus;
|
||||
private long totalJobs;
|
||||
private long completedJobs;
|
||||
private long cancelledJobs;
|
||||
private long inProgressJobs;
|
||||
private double completionRate;
|
||||
private BigDecimal totalRevenue;
|
||||
private LocalDateTime queryTimestamp;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.assecutor.votianlt.mcp.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* DTO for task completion statistics.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TaskCompletionResult {
|
||||
|
||||
private long totalTasks;
|
||||
private long completedTasks;
|
||||
private long pendingTasks;
|
||||
private double completionRate;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package de.assecutor.votianlt.mcp.tools;
|
||||
|
||||
import de.assecutor.votianlt.mcp.dto.JobQueryResult;
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.JobStatus;
|
||||
import de.assecutor.votianlt.service.JobStatisticsService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MCP Tool for querying jobs with various filters.
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class JobQueryTool {
|
||||
|
||||
private final JobStatisticsService statisticsService;
|
||||
|
||||
public JobQueryTool(JobStatisticsService statisticsService) {
|
||||
this.statisticsService = statisticsService;
|
||||
}
|
||||
|
||||
@Tool(description = "Query jobs with optional filters. Returns a list of jobs matching the criteria.")
|
||||
public List<JobQueryResult> queryJobs(
|
||||
@ToolParam(description = "Optional: Job status filter (CREATED, IN_PROGRESS, PICKUP_SCHEDULED, PICKED_UP, IN_TRANSIT, DELIVERED, COMPLETED, CANCELLED)") String status,
|
||||
@ToolParam(description = "Optional: Customer name filter") String customer,
|
||||
@ToolParam(description = "Optional: Pickup city filter") String pickupCity,
|
||||
@ToolParam(description = "Optional: Delivery city filter") String deliveryCity,
|
||||
@ToolParam(description = "Maximum results to return (default 50)") Integer limit) {
|
||||
log.info("MCP Tool: Querying jobs with filters - status: {}, customer: {}, pickupCity: {}, deliveryCity: {}",
|
||||
status, customer, pickupCity, deliveryCity);
|
||||
|
||||
int actualLimit = limit != null ? limit : 50;
|
||||
List<Job> jobs;
|
||||
|
||||
if (status != null && !status.isBlank()) {
|
||||
JobStatus jobStatus = JobStatus.valueOf(status.toUpperCase());
|
||||
jobs = statisticsService.getJobsByStatus(jobStatus);
|
||||
} else if (customer != null && !customer.isBlank()) {
|
||||
jobs = statisticsService.getJobsByCustomer(customer);
|
||||
} else if (pickupCity != null && !pickupCity.isBlank()) {
|
||||
jobs = statisticsService.getJobsByPickupCity(pickupCity);
|
||||
} else if (deliveryCity != null && !deliveryCity.isBlank()) {
|
||||
jobs = statisticsService.getJobsByDeliveryCity(deliveryCity);
|
||||
} else {
|
||||
jobs = statisticsService.getLatestJobs(actualLimit);
|
||||
}
|
||||
|
||||
return jobs.stream().limit(actualLimit).map(this::toQueryResult).toList();
|
||||
}
|
||||
|
||||
@Tool(description = "Get detailed information about a specific job by its job number")
|
||||
public JobQueryResult getJobByNumber(
|
||||
@ToolParam(description = "The job number to look up (e.g., JOB-2024-0001)") String jobNumber) {
|
||||
log.info("MCP Tool: Getting job by number: {}", jobNumber);
|
||||
|
||||
Job job = statisticsService.getJobByNumber(jobNumber);
|
||||
if (job == null) {
|
||||
return null;
|
||||
}
|
||||
return toQueryResult(job);
|
||||
}
|
||||
|
||||
@Tool(description = "Get jobs assigned to a specific mobile app user")
|
||||
public List<JobQueryResult> getJobsByAppUser(@ToolParam(description = "App user identifier") String appUser) {
|
||||
log.info("MCP Tool: Getting jobs for app user: {}", appUser);
|
||||
|
||||
return statisticsService.getJobsByAppUser(appUser).stream().map(this::toQueryResult).toList();
|
||||
}
|
||||
|
||||
@Tool(description = "Get the most recent jobs, sorted by creation date descending")
|
||||
public List<JobQueryResult> getLatestJobs(
|
||||
@ToolParam(description = "Number of jobs to return (default 10)") Integer limit) {
|
||||
log.info("MCP Tool: Getting latest jobs, limit: {}", limit);
|
||||
|
||||
int actualLimit = limit != null ? limit : 10;
|
||||
return statisticsService.getLatestJobs(actualLimit).stream().map(this::toQueryResult).toList();
|
||||
}
|
||||
|
||||
@Tool(description = "Get jobs created within a specific date range")
|
||||
public List<JobQueryResult> getJobsByDateRange(
|
||||
@ToolParam(description = "Start date in ISO format (e.g., 2024-01-01T00:00:00)") String startDate,
|
||||
@ToolParam(description = "End date in ISO format (e.g., 2024-12-31T23:59:59)") String endDate,
|
||||
@ToolParam(description = "Maximum results to return (default 100)") Integer limit) {
|
||||
log.info("MCP Tool: Getting jobs for date range: {} to {}", startDate, endDate);
|
||||
|
||||
LocalDateTime start = LocalDateTime.parse(startDate);
|
||||
LocalDateTime end = LocalDateTime.parse(endDate);
|
||||
int actualLimit = limit != null ? limit : 100;
|
||||
|
||||
return statisticsService.getJobsByDateRange(start, end).stream().limit(actualLimit).map(this::toQueryResult)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private JobQueryResult toQueryResult(Job job) {
|
||||
return JobQueryResult.builder().jobId(job.getIdAsString()).jobNumber(job.getJobNumber())
|
||||
.status(job.getStatus() != null ? job.getStatus().name() : null)
|
||||
.statusDisplayName(job.getStatus() != null ? job.getStatus().getDisplayName() : null)
|
||||
.customer(job.getCustomerSelection()).pickupCity(job.getPickupCity())
|
||||
.deliveryCity(job.getDeliveryCity()).pickupDate(job.getPickupDate()).deliveryDate(job.getDeliveryDate())
|
||||
.price(job.getPrice()).createdAt(job.getCreatedAt()).assignedAppUser(job.getAppUser())
|
||||
.digitalProcessing(job.isDigitalProcessing()).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package de.assecutor.votianlt.mcp.tools;
|
||||
|
||||
import de.assecutor.votianlt.mcp.dto.CustomerRevenueResult;
|
||||
import de.assecutor.votianlt.mcp.dto.JobStatisticsResult;
|
||||
import de.assecutor.votianlt.model.JobStatus;
|
||||
import de.assecutor.votianlt.service.JobStatisticsService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.Month;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* MCP Tool for job statistics queries. Provides various statistics and
|
||||
* aggregations about jobs.
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class JobStatisticsTool {
|
||||
|
||||
private final JobStatisticsService statisticsService;
|
||||
|
||||
public JobStatisticsTool(JobStatisticsService statisticsService) {
|
||||
this.statisticsService = statisticsService;
|
||||
}
|
||||
|
||||
@Tool(description = "Get comprehensive job statistics including counts by status, completion rates, and revenue metrics")
|
||||
public JobStatisticsResult getJobStatistics() {
|
||||
log.info("MCP Tool: Getting job statistics");
|
||||
|
||||
Map<JobStatus, Long> countsByStatus = statisticsService.getJobCountsByStatus();
|
||||
Map<String, Long> statusCounts = countsByStatus.entrySet().stream()
|
||||
.collect(Collectors.toMap(e -> e.getKey().name(), Map.Entry::getValue));
|
||||
|
||||
long completed = countsByStatus.getOrDefault(JobStatus.COMPLETED, 0L);
|
||||
long cancelled = countsByStatus.getOrDefault(JobStatus.CANCELLED, 0L);
|
||||
long inProgress = countsByStatus.getOrDefault(JobStatus.IN_PROGRESS, 0L);
|
||||
|
||||
return JobStatisticsResult.builder().countsByStatus(statusCounts)
|
||||
.totalJobs(statisticsService.getTotalJobCount()).completedJobs(completed).cancelledJobs(cancelled)
|
||||
.inProgressJobs(inProgress).completionRate(statisticsService.getCompletionRate())
|
||||
.totalRevenue(statisticsService.getTotalRevenue()).queryTimestamp(LocalDateTime.now()).build();
|
||||
}
|
||||
|
||||
@Tool(description = "Get job counts grouped by status (CREATED, IN_PROGRESS, PICKUP_SCHEDULED, PICKED_UP, IN_TRANSIT, DELIVERED, COMPLETED, CANCELLED)")
|
||||
public Map<String, Long> getJobCountsByStatus() {
|
||||
log.info("MCP Tool: Getting job counts by status");
|
||||
|
||||
Map<JobStatus, Long> counts = statisticsService.getJobCountsByStatus();
|
||||
return counts.entrySet().stream().collect(Collectors
|
||||
.toMap(e -> e.getKey().name() + " (" + e.getKey().getDisplayName() + ")", Map.Entry::getValue));
|
||||
}
|
||||
|
||||
@Tool(description = "Get the completion rate as a percentage (completed jobs / total jobs * 100)")
|
||||
public String getCompletionRate() {
|
||||
log.info("MCP Tool: Getting completion rate");
|
||||
|
||||
double rate = statisticsService.getCompletionRate();
|
||||
return String.format("%.2f%%", rate);
|
||||
}
|
||||
|
||||
@Tool(description = "Get revenue statistics grouped by customer, sorted by revenue descending")
|
||||
public List<CustomerRevenueResult> getRevenueByCustomer(
|
||||
@ToolParam(description = "Maximum number of customers to return (default 10)") Integer limit) {
|
||||
log.info("MCP Tool: Getting revenue by customer, limit: {}", limit);
|
||||
|
||||
int actualLimit = limit != null ? limit : 10;
|
||||
return statisticsService.getTopCustomersByRevenue(actualLimit).stream().map(entry -> {
|
||||
String customer = entry.getKey();
|
||||
long jobCount = statisticsService.getJobsByCustomer(customer).size();
|
||||
return CustomerRevenueResult.builder().customer(customer).revenue(entry.getValue()).jobCount(jobCount)
|
||||
.build();
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@Tool(description = "Get monthly job trend data for a specific year showing job counts per month")
|
||||
public Map<String, Long> getMonthlyJobTrend(
|
||||
@ToolParam(description = "Year for the trend data (e.g., 2024)") int year) {
|
||||
log.info("MCP Tool: Getting monthly job trend for year: {}", year);
|
||||
|
||||
Map<Month, Long> monthlyData = statisticsService.getMonthlyJobCounts(year);
|
||||
return monthlyData.entrySet().stream()
|
||||
.collect(Collectors.toMap(e -> e.getKey().toString(), Map.Entry::getValue));
|
||||
}
|
||||
|
||||
@Tool(description = "Get total revenue from all jobs")
|
||||
public String getTotalRevenue() {
|
||||
log.info("MCP Tool: Getting total revenue");
|
||||
|
||||
BigDecimal revenue = statisticsService.getTotalRevenue();
|
||||
return String.format("%.2f EUR", revenue);
|
||||
}
|
||||
|
||||
@Tool(description = "Get job count for a specific date range")
|
||||
public long getJobCountByDateRange(
|
||||
@ToolParam(description = "Start date in ISO format (e.g., 2024-01-01T00:00:00)") String startDate,
|
||||
@ToolParam(description = "End date in ISO format (e.g., 2024-12-31T23:59:59)") String endDate) {
|
||||
log.info("MCP Tool: Getting job count for date range: {} to {}", startDate, endDate);
|
||||
|
||||
LocalDateTime start = LocalDateTime.parse(startDate);
|
||||
LocalDateTime end = LocalDateTime.parse(endDate);
|
||||
return statisticsService.getJobCountByDateRange(start, end);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package de.assecutor.votianlt.mcp.tools;
|
||||
|
||||
import de.assecutor.votianlt.mcp.dto.TaskCompletionResult;
|
||||
import de.assecutor.votianlt.service.JobStatisticsService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MCP Tool for task completion statistics and data.
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class TaskCompletionTool {
|
||||
|
||||
private final JobStatisticsService statisticsService;
|
||||
|
||||
public TaskCompletionTool(JobStatisticsService statisticsService) {
|
||||
this.statisticsService = statisticsService;
|
||||
}
|
||||
|
||||
@Tool(description = "Get overall task completion statistics including total, completed, pending tasks and completion rate")
|
||||
public TaskCompletionResult getTaskCompletionStats() {
|
||||
log.info("MCP Tool: Getting task completion statistics");
|
||||
|
||||
Map<String, Long> stats = statisticsService.getTaskCompletionStats();
|
||||
long total = stats.getOrDefault("total", 0L);
|
||||
long completed = stats.getOrDefault("completed", 0L);
|
||||
long pending = stats.getOrDefault("pending", 0L);
|
||||
|
||||
double completionRate = total > 0 ? (double) completed / total * 100.0 : 0.0;
|
||||
|
||||
return TaskCompletionResult.builder().totalTasks(total).completedTasks(completed).pendingTasks(pending)
|
||||
.completionRate(completionRate).build();
|
||||
}
|
||||
|
||||
@Tool(description = "Get a summary of task completion as a formatted string")
|
||||
public String getTaskCompletionSummary() {
|
||||
log.info("MCP Tool: Getting task completion summary");
|
||||
|
||||
Map<String, Long> stats = statisticsService.getTaskCompletionStats();
|
||||
long total = stats.getOrDefault("total", 0L);
|
||||
long completed = stats.getOrDefault("completed", 0L);
|
||||
long pending = stats.getOrDefault("pending", 0L);
|
||||
|
||||
double completionRate = total > 0 ? (double) completed / total * 100.0 : 0.0;
|
||||
|
||||
return String.format("Task Statistics: %d total tasks, %d completed (%.1f%%), %d pending", total, completed,
|
||||
completionRate, pending);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package de.assecutor.votianlt.messaging;
|
||||
|
||||
import de.assecutor.votianlt.controller.MessageController;
|
||||
import de.assecutor.votianlt.dto.AppLoginRequest;
|
||||
import de.assecutor.votianlt.dto.AppLoginResponse;
|
||||
import de.assecutor.votianlt.service.ClientConnectionService;
|
||||
import de.assecutor.votianlt.service.LocationService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.EventListener;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Configuration for the messaging system. Sets up message routing after
|
||||
* application startup.
|
||||
*/
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class MessagingConfig {
|
||||
|
||||
private final WebSocketService webSocketService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public MessagingConfig(WebSocketService webSocketService, ObjectMapper objectMapper) {
|
||||
this.webSocketService = webSocketService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up message routing after application startup.
|
||||
*/
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void setupMessaging(ApplicationReadyEvent event) {
|
||||
try {
|
||||
MessageController messageController = event.getApplicationContext().getBean(MessageController.class);
|
||||
ClientConnectionService clientConnectionService = event.getApplicationContext()
|
||||
.getBean(ClientConnectionService.class);
|
||||
LocationService locationService = event.getApplicationContext().getBean(LocationService.class);
|
||||
|
||||
setupSubscriptions(messageController, clientConnectionService, locationService);
|
||||
|
||||
log.info("[Messaging] Message routing configured");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[Messaging] Failed to initialize: {}", e.getMessage());
|
||||
throw new RuntimeException("Failed to initialize messaging", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup message subscriptions on the WebSocket service.
|
||||
*/
|
||||
private void setupSubscriptions(MessageController messageController,
|
||||
ClientConnectionService clientConnectionService, LocationService locationService) {
|
||||
// Login handler: authenticate and register session
|
||||
webSocketService.registerMessageHandler("login", (wsSessionId, payload) -> {
|
||||
handleLoginMessage(wsSessionId, payload, messageController, clientConnectionService);
|
||||
});
|
||||
|
||||
// Task completion handler
|
||||
webSocketService.registerMessageHandler("task_completed", (appUserId, payload) -> {
|
||||
handlePayload(payload, payloadMap -> {
|
||||
String taskType = payloadMap.get("taskType") != null ? payloadMap.get("taskType").toString() : null;
|
||||
messageController.handleTaskCompleted(payloadMap, taskType);
|
||||
});
|
||||
});
|
||||
|
||||
// Chat message handler
|
||||
webSocketService.registerMessageHandler("message", (appUserId, payload) -> {
|
||||
handlePayload(payload, payloadMap -> {
|
||||
messageController.handleIncomingMessage(appUserId, payloadMap);
|
||||
});
|
||||
});
|
||||
|
||||
// Buffer flushed handler - client is ready to receive pending messages
|
||||
webSocketService.registerMessageHandler("buffer_flushed", (appUserId, payload) -> {
|
||||
handlePayload(payload, payloadMap -> {
|
||||
int messageCount = extractMessageCount(payloadMap);
|
||||
clientConnectionService.onBufferFlushed(appUserId, messageCount);
|
||||
});
|
||||
});
|
||||
|
||||
// Location handler - client sends position updates
|
||||
webSocketService.registerMessageHandler("location", (appUserId, payload) -> {
|
||||
handlePayload(payload, payloadMap -> {
|
||||
locationService.savePosition(appUserId, payloadMap);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle login message. The wsSessionId identifies the pending WebSocket
|
||||
* session. On success, registers the session under the appUserId and sends an
|
||||
* auth response. On failure, sends an error response to the pending session.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void handleLoginMessage(String wsSessionId, byte[] payload, MessageController messageController,
|
||||
ClientConnectionService clientConnectionService) {
|
||||
try {
|
||||
String json = new String(payload, StandardCharsets.UTF_8);
|
||||
Map<String, Object> payloadMap = objectMapper.readValue(json, Map.class);
|
||||
AppLoginRequest loginRequest = objectMapper.convertValue(payloadMap, AppLoginRequest.class);
|
||||
AppLoginResponse response = messageController.handleAppLogin(loginRequest);
|
||||
|
||||
if (response.isSuccess()) {
|
||||
String appUserId = response.getAppUserId();
|
||||
log.info("[Messaging] Login successful for appUserId: {}", appUserId);
|
||||
|
||||
webSocketService.registerAuthenticatedSession(wsSessionId, appUserId);
|
||||
|
||||
// Send success response to the now-authenticated session
|
||||
// locationTrackingEnabled: true = client should send position updates
|
||||
// appUserId: wird an den Client gesendet für Referenz
|
||||
Map<String, Object> authResponse = Map.of("success", true, "message", response.getMessage(),
|
||||
"locationTrackingEnabled", true, "appUserId", appUserId);
|
||||
byte[] responseBytes = objectMapper.writeValueAsBytes(authResponse);
|
||||
log.info("[Messaging] Sending auth response to appUserId: {}", appUserId);
|
||||
webSocketService.sendToClient(appUserId, "auth", responseBytes);
|
||||
|
||||
// Register client - pending messages and jobs will be sent after
|
||||
// client confirms buffer_flushed
|
||||
clientConnectionService.registerClient(appUserId);
|
||||
} else {
|
||||
log.warn("[Messaging] Login failed: {}", response.getMessage());
|
||||
// Send failure response to the pending session
|
||||
Map<String, Object> authResponse = Map.of("success", false, "message", response.getMessage());
|
||||
byte[] responseBytes = objectMapper.writeValueAsBytes(authResponse);
|
||||
webSocketService.sendToSessionById(wsSessionId, "/client/auth", responseBytes);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[Messaging] Login handling error: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract message count from buffer_flushed payload.
|
||||
*/
|
||||
private int extractMessageCount(Map<String, Object> payloadMap) {
|
||||
try {
|
||||
Object countObj = payloadMap.get("messageCount");
|
||||
if (countObj instanceof Number) {
|
||||
return ((Number) countObj).intValue();
|
||||
}
|
||||
return 0;
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse payload bytes to a Map and pass to the consumer.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void handlePayload(byte[] payload, java.util.function.Consumer<Map<String, Object>> handler) {
|
||||
try {
|
||||
String json = new String(payload, StandardCharsets.UTF_8);
|
||||
Map<String, Object> payloadMap = objectMapper.readValue(json, Map.class);
|
||||
handler.accept(payloadMap);
|
||||
} catch (Exception e) {
|
||||
log.error("[Messaging] Error parsing payload: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package de.assecutor.votianlt.messaging;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Publishing helper to send JSON payloads to clients via WebSocket.
|
||||
*/
|
||||
public interface MessagingPublisher {
|
||||
void publishAsJson(String clientId, String messageType, Object payload);
|
||||
}
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
class MessagingPublisherImpl implements MessagingPublisher {
|
||||
|
||||
private final WebSocketService webSocketService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public MessagingPublisherImpl(WebSocketService webSocketService, ObjectMapper objectMapper) {
|
||||
this.webSocketService = webSocketService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishAsJson(String clientId, String messageType, Object payload) {
|
||||
try {
|
||||
// Prüfen ob Client verbunden ist
|
||||
boolean isConnected = webSocketService.isClientConnected(clientId);
|
||||
log.debug("[Messaging] Publishing to {}/{} - connected: {}", clientId, messageType, isConnected);
|
||||
|
||||
if (!isConnected) {
|
||||
log.warn("[Messaging] Client {} is not connected, cannot send {}", clientId, messageType);
|
||||
return;
|
||||
}
|
||||
|
||||
String json = objectMapper.writeValueAsString(payload);
|
||||
byte[] data = json.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
webSocketService.sendToClient(clientId, messageType, data).thenRun(() -> {
|
||||
log.debug("[Messaging] Successfully sent {}/{} to client {}", messageType, clientId);
|
||||
}).exceptionally(ex -> {
|
||||
log.error("[Messaging] Failed to deliver to {}/{}: {}", clientId, messageType, ex.getMessage(), ex);
|
||||
return null;
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[Messaging] Failed to publish to {}/{}: {}", clientId, messageType, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.assecutor.votianlt.messaging;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
|
||||
|
||||
/**
|
||||
* WebSocket configuration that registers the WebSocketService as a handler on
|
||||
* the configured endpoint.
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSocket
|
||||
public class WebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
private final WebSocketService webSocketService;
|
||||
|
||||
@Value("${app.messaging.websocket.path:/ws/messaging}")
|
||||
private String wsPath;
|
||||
|
||||
@Value("${app.messaging.websocket.allowed-origins:*}")
|
||||
private String allowedOrigins;
|
||||
|
||||
public WebSocketConfig(WebSocketService webSocketService) {
|
||||
this.webSocketService = webSocketService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
registry.addHandler(webSocketService, wsPath).setAllowedOrigins(allowedOrigins.split(","))
|
||||
.addInterceptors(new HttpSessionHandshakeInterceptor());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
package de.assecutor.votianlt.messaging;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* WebSocket service for direct bidirectional communication with mobile clients.
|
||||
*
|
||||
* Wire Protocol: Each WebSocket message is a JSON document with a "topic" and
|
||||
* "payload" field:
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* "topic": "/server/login",
|
||||
* "payload": { ... }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* Topic Structure:
|
||||
* <ul>
|
||||
* <li>Server to Client: /client/{messageType}</li>
|
||||
* <li>Client to Server: /server/{messageType}</li>
|
||||
* <li>Login (special): /server/login (unauthenticated)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class WebSocketService extends TextWebSocketHandler {
|
||||
|
||||
@FunctionalInterface
|
||||
public interface MessageHandler {
|
||||
void onMessageReceived(String clientId, byte[] payload);
|
||||
}
|
||||
|
||||
private static final String TOPIC_TO_CLIENT = "/client/%s";
|
||||
private static final long PENDING_SESSION_TIMEOUT_MS = 30_000;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// appUserId -> WebSocketSession
|
||||
private final ConcurrentHashMap<String, WebSocketSession> clientSessions = new ConcurrentHashMap<>();
|
||||
|
||||
// sessionId -> appUserId (reverse lookup for cleanup on disconnect)
|
||||
private final ConcurrentHashMap<String, String> sessionToClient = new ConcurrentHashMap<>();
|
||||
|
||||
// sessionId -> PendingSession (connected but not yet logged in)
|
||||
private final ConcurrentHashMap<String, PendingSession> pendingSessions = new ConcurrentHashMap<>();
|
||||
|
||||
private final Map<String, MessageHandler> messageHandlers = new ConcurrentHashMap<>();
|
||||
private volatile boolean initialized = false;
|
||||
|
||||
private ScheduledExecutorService pendingSessionCleanup;
|
||||
|
||||
public WebSocketService(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Lifecycle
|
||||
// ==========================================
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
pendingSessionCleanup = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "ws-pending-cleanup");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
pendingSessionCleanup.scheduleAtFixedRate(this::cleanupPendingSessions, 30, 30, TimeUnit.SECONDS);
|
||||
|
||||
initialized = true;
|
||||
log.info("[WebSocket] Service initialized on endpoint /ws/messaging");
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
if (pendingSessionCleanup != null) {
|
||||
pendingSessionCleanup.shutdownNow();
|
||||
}
|
||||
|
||||
for (var entry : clientSessions.entrySet()) {
|
||||
try {
|
||||
WebSocketSession session = entry.getValue();
|
||||
if (session.isOpen()) {
|
||||
session.close(CloseStatus.GOING_AWAY);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[WebSocket] Error closing session for client {}: {}", entry.getKey(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
for (var entry : pendingSessions.entrySet()) {
|
||||
try {
|
||||
if (entry.getValue().session.isOpen()) {
|
||||
entry.getValue().session.close(CloseStatus.GOING_AWAY);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
clientSessions.clear();
|
||||
sessionToClient.clear();
|
||||
pendingSessions.clear();
|
||||
messageHandlers.clear();
|
||||
initialized = false;
|
||||
|
||||
log.info("[WebSocket] Service shut down");
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Public API
|
||||
// ==========================================
|
||||
|
||||
public CompletableFuture<Void> sendToClient(String clientId, String messageType, byte[] payload) {
|
||||
WebSocketSession session = clientSessions.get(clientId);
|
||||
if (session == null) {
|
||||
log.warn("[WebSocket] No session found for client {}", clientId);
|
||||
return CompletableFuture.failedFuture(new IOException("No WebSocket session for client: " + clientId));
|
||||
}
|
||||
|
||||
if (!session.isOpen()) {
|
||||
log.warn("[WebSocket] Session for client {} is closed", clientId);
|
||||
// Session aus der Map entfernen
|
||||
clientSessions.remove(clientId);
|
||||
sessionToClient.remove(session.getId());
|
||||
return CompletableFuture.failedFuture(new IOException("WebSocket session closed for client: " + clientId));
|
||||
}
|
||||
|
||||
try {
|
||||
String topic = String.format(TOPIC_TO_CLIENT, messageType);
|
||||
String payloadJson = new String(payload, StandardCharsets.UTF_8);
|
||||
|
||||
ObjectNode wireMessage = objectMapper.createObjectNode();
|
||||
wireMessage.put("topic", topic);
|
||||
wireMessage.set("payload", objectMapper.readTree(payloadJson));
|
||||
|
||||
String wireJson = objectMapper.writeValueAsString(wireMessage);
|
||||
log.info("[WebSocket OUT] {} to client {} (session open: {})", topic, clientId, session.isOpen());
|
||||
log.debug("[WebSocket OUT] {} -> {}", topic, wireJson);
|
||||
|
||||
sendToSession(session, wireJson);
|
||||
log.debug("[WebSocket] Message sent successfully to client {}", clientId);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
} catch (Exception e) {
|
||||
log.error("[WebSocket] Failed to send to client {}: {}", clientId, e.getMessage(), e);
|
||||
return CompletableFuture.failedFuture(new IOException("Failed to send WebSocket message", e));
|
||||
}
|
||||
}
|
||||
|
||||
public void registerMessageHandler(String messageType, MessageHandler handler) {
|
||||
messageHandlers.put(messageType, handler);
|
||||
log.debug("[WebSocket] Registered handler for messageType: {}", messageType);
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
public boolean isClientConnected(String clientId) {
|
||||
WebSocketSession session = clientSessions.get(clientId);
|
||||
return session != null && session.isOpen();
|
||||
}
|
||||
|
||||
public int getConnectedClientCount() {
|
||||
return clientSessions.size();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// WebSocket handler methods
|
||||
// ==========================================
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
pendingSessions.put(session.getId(), new PendingSession(session, Instant.now()));
|
||||
log.info("[WebSocket] New connection: sessionId={}, remote={}", session.getId(), session.getRemoteAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
|
||||
try {
|
||||
String json = message.getPayload();
|
||||
JsonNode wireMessage = objectMapper.readTree(json);
|
||||
|
||||
JsonNode topicNode = wireMessage.get("topic");
|
||||
JsonNode payloadNode = wireMessage.get("payload");
|
||||
|
||||
if (topicNode == null || payloadNode == null) {
|
||||
log.warn("[WebSocket] Invalid message format (missing topic or payload): {}", json);
|
||||
return;
|
||||
}
|
||||
|
||||
String topic = topicNode.asText();
|
||||
byte[] payloadBytes = objectMapper.writeValueAsBytes(payloadNode);
|
||||
|
||||
log.info("[WebSocket IN] {} <- {}", topic, json);
|
||||
|
||||
// Login message (special: unauthenticated)
|
||||
if ("/server/login".equals(topic)) {
|
||||
handleLoginMessage(session, payloadBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular client message: /server/{messageType}
|
||||
if (topic.startsWith("/server/")) {
|
||||
// Verify session is authenticated
|
||||
String appUserId = sessionToClient.get(session.getId());
|
||||
if (appUserId == null) {
|
||||
log.warn("[WebSocket] Unauthenticated session {} tried to send: {}", session.getId(), topic);
|
||||
return;
|
||||
}
|
||||
handleClientMessage(topic, appUserId, payloadBytes);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[WebSocket] Error handling message: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
||||
String sessionId = session.getId();
|
||||
|
||||
// Remove from pending sessions
|
||||
pendingSessions.remove(sessionId);
|
||||
|
||||
// Remove from authenticated sessions
|
||||
String clientId = sessionToClient.remove(sessionId);
|
||||
if (clientId != null) {
|
||||
clientSessions.remove(clientId, session);
|
||||
log.info("[WebSocket] Client disconnected: clientId={}, reason={}", clientId, status);
|
||||
} else {
|
||||
log.info("[WebSocket] Unauthenticated session closed: sessionId={}", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportError(WebSocketSession session, Throwable exception) {
|
||||
log.error("[WebSocket] Transport error for session {}: {}", session.getId(), exception.getMessage());
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Internal message routing
|
||||
// ==========================================
|
||||
|
||||
private void handleLoginMessage(WebSocketSession session, byte[] payloadBytes) {
|
||||
MessageHandler handler = messageHandlers.get("login");
|
||||
if (handler != null) {
|
||||
handler.onMessageReceived(session.getId(), payloadBytes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a pending session as authenticated under the given appUserId. Called
|
||||
* by MessagingConfig after successful login.
|
||||
*/
|
||||
public void registerAuthenticatedSession(String wsSessionId, String appUserId) {
|
||||
PendingSession pending = pendingSessions.get(wsSessionId);
|
||||
if (pending == null) {
|
||||
log.warn("[WebSocket] No pending session for wsSessionId={}", wsSessionId);
|
||||
return;
|
||||
}
|
||||
registerClientSession(appUserId, pending.session());
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a wire-format message directly to a session by its WebSocket sessionId.
|
||||
* Used for sending login responses to pending (not yet authenticated) sessions.
|
||||
*/
|
||||
public void sendToSessionById(String wsSessionId, String topic, byte[] payload) {
|
||||
try {
|
||||
// Check pending sessions first
|
||||
PendingSession pending = pendingSessions.get(wsSessionId);
|
||||
WebSocketSession session = pending != null ? pending.session() : null;
|
||||
|
||||
// Fallback: check authenticated sessions via reverse lookup
|
||||
if (session == null) {
|
||||
String appUserId = sessionToClient.get(wsSessionId);
|
||||
if (appUserId != null) {
|
||||
session = clientSessions.get(appUserId);
|
||||
}
|
||||
}
|
||||
|
||||
if (session == null || !session.isOpen()) {
|
||||
log.warn("[WebSocket] Cannot send to session {}: not found or closed", wsSessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
String payloadJson = new String(payload, StandardCharsets.UTF_8);
|
||||
ObjectNode wireMessage = objectMapper.createObjectNode();
|
||||
wireMessage.put("topic", topic);
|
||||
wireMessage.set("payload", objectMapper.readTree(payloadJson));
|
||||
|
||||
String wireJson = objectMapper.writeValueAsString(wireMessage);
|
||||
log.info("[WebSocket OUT] {} -> {}", topic, wireJson);
|
||||
|
||||
sendToSession(session, wireJson);
|
||||
} catch (Exception e) {
|
||||
log.error("[WebSocket] Error sending to session {}: {}", wsSessionId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleClientMessage(String topic, String appUserId, byte[] payload) {
|
||||
String[] parts = topic.split("/");
|
||||
|
||||
// Handle /server/{messageType} where messageType can contain slashes
|
||||
if (parts.length >= 3) {
|
||||
String messageType = String.join("/", Arrays.copyOfRange(parts, 2, parts.length));
|
||||
|
||||
MessageHandler handler = messageHandlers.get(messageType);
|
||||
if (handler != null) {
|
||||
handler.onMessageReceived(appUserId, payload);
|
||||
} else {
|
||||
log.warn("[WebSocket] No handler registered for messageType: {}", messageType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Session management
|
||||
// ==========================================
|
||||
|
||||
private void registerClientSession(String clientId, WebSocketSession session) {
|
||||
// Close old session if same clientId reconnects
|
||||
WebSocketSession oldSession = clientSessions.put(clientId, session);
|
||||
if (oldSession != null && oldSession.isOpen() && !oldSession.getId().equals(session.getId())) {
|
||||
try {
|
||||
String oldSessionId = oldSession.getId();
|
||||
sessionToClient.remove(oldSessionId);
|
||||
oldSession.close(CloseStatus.NORMAL.withReason("Replaced by new connection"));
|
||||
log.info("[WebSocket] Closed old session for clientId={} (replaced)", clientId);
|
||||
} catch (IOException e) {
|
||||
log.warn("[WebSocket] Error closing old session for client {}: {}", clientId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
sessionToClient.put(session.getId(), clientId);
|
||||
pendingSessions.remove(session.getId());
|
||||
|
||||
log.info("[WebSocket] Client registered: clientId={}, sessionId={}", clientId, session.getId());
|
||||
}
|
||||
|
||||
private void cleanupPendingSessions() {
|
||||
Instant cutoff = Instant.now().minusMillis(PENDING_SESSION_TIMEOUT_MS);
|
||||
pendingSessions.entrySet().removeIf(entry -> {
|
||||
if (entry.getValue().connectedAt.isBefore(cutoff)) {
|
||||
try {
|
||||
WebSocketSession session = entry.getValue().session;
|
||||
if (session.isOpen()) {
|
||||
session.close(CloseStatus.POLICY_VIOLATION.withReason("Login timeout"));
|
||||
}
|
||||
log.info("[WebSocket] Closed pending session (login timeout): sessionId={}", entry.getKey());
|
||||
} catch (IOException e) {
|
||||
log.warn("[WebSocket] Error closing pending session: {}", e.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Utility methods
|
||||
// ==========================================
|
||||
|
||||
private void sendToSession(WebSocketSession session, String message) throws IOException {
|
||||
synchronized (session) {
|
||||
if (session.isOpen()) {
|
||||
session.sendMessage(new TextMessage(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Internal types
|
||||
// ==========================================
|
||||
|
||||
private record PendingSession(WebSocketSession session, Instant connectedAt) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Speichert das Ergebnis einer Adressvalidierung. Wird verwendet, um zu merken,
|
||||
* ob eine Adresse bereits validiert wurde und ob sie gültig ist.
|
||||
*/
|
||||
@Data
|
||||
public class AddressValidationResult {
|
||||
|
||||
private final String addressType; // "pickup" oder "delivery"
|
||||
private final String street;
|
||||
private final String houseNumber;
|
||||
private final String zip;
|
||||
private final String city;
|
||||
|
||||
private boolean valid;
|
||||
private String formattedAddress;
|
||||
private double latitude;
|
||||
private double longitude;
|
||||
private String validationMessage;
|
||||
|
||||
public AddressValidationResult(String addressType, String street, String houseNumber, String zip, String city) {
|
||||
this.addressType = addressType;
|
||||
this.street = street;
|
||||
this.houseNumber = houseNumber;
|
||||
this.zip = zip;
|
||||
this.city = city;
|
||||
this.valid = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen eindeutigen Schlüssel für diese Adresse
|
||||
*/
|
||||
public String getAddressKey() {
|
||||
return String.format("%s|%s|%s|%s|%s", addressType, normalize(street), normalize(houseNumber), normalize(zip),
|
||||
normalize(city));
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value != null ? value.trim().toLowerCase() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob diese Validierung für die angegebenen Adressdaten gilt
|
||||
*/
|
||||
public boolean matches(String street, String houseNumber, String zip, String city) {
|
||||
return normalize(this.street).equals(normalize(street))
|
||||
&& normalize(this.houseNumber).equals(normalize(houseNumber))
|
||||
&& normalize(this.zip).equals(normalize(zip)) && normalize(this.city).equals(normalize(city));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Document(collection = "app_user")
|
||||
public class AppUser {
|
||||
|
||||
@Id
|
||||
@JsonIgnore
|
||||
private ObjectId id;
|
||||
|
||||
@Field("bezeichnung")
|
||||
private String bezeichnung;
|
||||
|
||||
@Field("vorname")
|
||||
private String vorname;
|
||||
|
||||
@Field("nachname")
|
||||
private String nachname;
|
||||
|
||||
@Field("telefon")
|
||||
private String telefon;
|
||||
|
||||
@Field("app_code")
|
||||
private String appCode;
|
||||
|
||||
@Field("email")
|
||||
@org.springframework.data.mongodb.core.index.Indexed(unique = true)
|
||||
private String email;
|
||||
|
||||
@Field("password")
|
||||
private String password;
|
||||
|
||||
// Reset-Token und Zeitstempel
|
||||
@Field("password_code")
|
||||
private String passwordCode;
|
||||
|
||||
@Field("password_timestamp")
|
||||
private LocalDateTime passwordTimestamp;
|
||||
|
||||
@Field("geraet")
|
||||
private String geraet;
|
||||
|
||||
@Field("owner")
|
||||
private ObjectId owner;
|
||||
|
||||
@Field("erstellt_am")
|
||||
private LocalDateTime erstelltAm;
|
||||
|
||||
@Field("erstellt_von")
|
||||
private ObjectId erstelltVon;
|
||||
|
||||
@Field("aktualisiert_am")
|
||||
private LocalDateTime aktualisiertAm;
|
||||
|
||||
@Field("aktualisiert_von")
|
||||
private ObjectId aktualisiertVon;
|
||||
|
||||
public AppUser() {
|
||||
this.erstelltAm = LocalDateTime.now();
|
||||
this.aktualisiertAm = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ObjectId as string for JSON serialization. This ensures that the
|
||||
* app user id is returned as a string when users are retrieved via API.
|
||||
*/
|
||||
@JsonGetter("id")
|
||||
public String getIdAsString() {
|
||||
return id != null ? id.toString() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Barcode entity for storing barcode data from task completions. References the
|
||||
* task ObjectId and stores barcode strings.
|
||||
*/
|
||||
@Data
|
||||
@Document(collection = "barcodes")
|
||||
public class Barcode {
|
||||
|
||||
@Id
|
||||
private ObjectId id;
|
||||
|
||||
private ObjectId taskId;
|
||||
private String barcode;
|
||||
private LocalDateTime createdAt;
|
||||
private String completedBy;
|
||||
|
||||
// Default constructor
|
||||
public Barcode() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// Constructor with parameters
|
||||
public Barcode(ObjectId taskId, String barcode, String completedBy) {
|
||||
this();
|
||||
this.taskId = taskId;
|
||||
this.barcode = barcode;
|
||||
this.completedBy = completedBy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Document(collection = "cargo_items")
|
||||
public class CargoItem {
|
||||
@Id
|
||||
@JsonIgnore
|
||||
private ObjectId id;
|
||||
|
||||
@Field("job_id")
|
||||
private ObjectId jobId;
|
||||
|
||||
@Field("description")
|
||||
private String description;
|
||||
|
||||
@Field("quantity")
|
||||
private Integer quantity;
|
||||
|
||||
@Field("weight_kg")
|
||||
private Double weightKg;
|
||||
|
||||
@Field("length_mm")
|
||||
private Double lengthMm;
|
||||
|
||||
@Field("width_mm")
|
||||
private Double widthMm;
|
||||
|
||||
@Field("height_mm")
|
||||
private Double heightMm;
|
||||
|
||||
/**
|
||||
* Returns the ObjectId as string for JSON serialization. This ensures that the
|
||||
* cargo item id is returned as a string when items are retrieved via API.
|
||||
*/
|
||||
@JsonGetter("id")
|
||||
public String getIdAsString() {
|
||||
return id != null ? id.toString() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Document(collection = "comments")
|
||||
public class Comment {
|
||||
|
||||
@Id
|
||||
private ObjectId id;
|
||||
|
||||
@Field("task_id")
|
||||
private ObjectId taskId;
|
||||
|
||||
@Field("comment_text")
|
||||
private String commentText;
|
||||
|
||||
@Field("completed_by")
|
||||
private String completedBy;
|
||||
|
||||
@Field("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public Comment(ObjectId taskId, String commentText, String completedBy) {
|
||||
this.taskId = taskId;
|
||||
this.commentText = commentText;
|
||||
this.completedBy = completedBy;
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
@Data
|
||||
public class Company {
|
||||
private ObjectId id;
|
||||
|
||||
private String name;
|
||||
private String street;
|
||||
private String houseNumber;
|
||||
private String addressAddition;
|
||||
private String zip;
|
||||
private String city;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
@Data
|
||||
@Document(collection = "customers")
|
||||
public class Customer {
|
||||
@Id
|
||||
private ObjectId id;
|
||||
|
||||
@Field("title")
|
||||
private String title;
|
||||
|
||||
@Field("company_name")
|
||||
private String companyName;
|
||||
|
||||
@Field("firstname")
|
||||
private String firstname;
|
||||
|
||||
@Field("last_name")
|
||||
private String lastName;
|
||||
|
||||
@Field("telephone")
|
||||
private String telephone;
|
||||
|
||||
@Field("fax")
|
||||
private String fax;
|
||||
|
||||
@Field("mail")
|
||||
private String mail;
|
||||
|
||||
@Field("street")
|
||||
private String street;
|
||||
|
||||
@Field("house_number")
|
||||
private String houseNumber;
|
||||
|
||||
@Field("address_addition")
|
||||
private String addressAddition;
|
||||
|
||||
@Field("zip")
|
||||
private String zip;
|
||||
|
||||
@Field("city")
|
||||
private String city;
|
||||
|
||||
@Field("created_by")
|
||||
private ObjectId createdBy;
|
||||
|
||||
@Field("owner")
|
||||
private ObjectId owner;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import de.assecutor.votianlt.model.task.BaseTask;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Embedded delivery station within a Job. Each job can have up to 25 delivery
|
||||
* stations. This is NOT a standalone MongoDB document - it is stored as part of
|
||||
* the Job document.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DeliveryStation {
|
||||
|
||||
@Field("station_id")
|
||||
@JsonIgnore
|
||||
private ObjectId stationId;
|
||||
|
||||
@Field("station_order")
|
||||
private int stationOrder;
|
||||
|
||||
@Field("company")
|
||||
private String company;
|
||||
|
||||
@Field("salutation")
|
||||
private String salutation;
|
||||
|
||||
@Field("first_name")
|
||||
private String firstName;
|
||||
|
||||
@Field("last_name")
|
||||
private String lastName;
|
||||
|
||||
@Field("phone")
|
||||
private String phone;
|
||||
|
||||
@Field("street")
|
||||
private String street;
|
||||
|
||||
@Field("house_number")
|
||||
private String houseNumber;
|
||||
|
||||
@Field("address_addition")
|
||||
private String addressAddition;
|
||||
|
||||
@Field("zip")
|
||||
private String zip;
|
||||
|
||||
@Field("city")
|
||||
private String city;
|
||||
|
||||
@Field("delivery_date")
|
||||
private LocalDate deliveryDate;
|
||||
|
||||
@Field("delivery_time")
|
||||
private LocalTime deliveryTime;
|
||||
|
||||
@Field("tasks")
|
||||
private List<BaseTask> tasks = new ArrayList<>();
|
||||
|
||||
@JsonGetter("stationId")
|
||||
public String getStationIdAsString() {
|
||||
return stationId != null ? stationId.toHexString() : null;
|
||||
}
|
||||
|
||||
public ObjectId ensureStationId() {
|
||||
if (stationId == null) {
|
||||
stationId = new ObjectId();
|
||||
}
|
||||
return stationId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Stores invoice template data for a user. Contains the JSON representation of
|
||||
* the canvas elements.
|
||||
*/
|
||||
@Document(collection = "invoice_templates")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class InvoiceTemplate {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* The user ID this template belongs to
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* Template name (optional, for future use if multiple templates are supported)
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* JSON string containing the template data (canvas elements)
|
||||
*/
|
||||
private String templateData;
|
||||
|
||||
/**
|
||||
* When the template was created
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* When the template was last updated
|
||||
*/
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* Version for optimistic locking
|
||||
*/
|
||||
private Long version;
|
||||
|
||||
public InvoiceTemplate(String userId, String name, String templateData) {
|
||||
this.userId = userId;
|
||||
this.name = name;
|
||||
this.templateData = templateData;
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void updateTemplate(String templateData) {
|
||||
this.templateData = templateData;
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
234
backend/src/main/java/de/assecutor/votianlt/model/Job.java
Normal file
234
backend/src/main/java/de/assecutor/votianlt/model/Job.java
Normal file
@@ -0,0 +1,234 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Data
|
||||
@Document(collection = "jobs")
|
||||
public class Job {
|
||||
@Id
|
||||
@JsonIgnore
|
||||
private ObjectId id;
|
||||
|
||||
// Metadaten
|
||||
@Field("job_number")
|
||||
private String jobNumber; // Eindeutige Auftragsnummer
|
||||
|
||||
@Field("status")
|
||||
private JobStatus status = JobStatus.CREATED; // Status des Auftrags
|
||||
|
||||
@Field("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Field("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Field("created_by")
|
||||
private String createdBy; // Benutzer, der den Auftrag erstellt hat
|
||||
|
||||
@Field("is_draft")
|
||||
private boolean isDraft = false; // Kennzeichnet Entwürfe
|
||||
|
||||
// Auftraggeber/Rechnungsempfänger
|
||||
@Field("customer_selection")
|
||||
private String customerSelection; // Kunde01 | KOTVor K01Nach
|
||||
|
||||
// Abholadresse
|
||||
@Field("pickup_company")
|
||||
private String pickupCompany;
|
||||
|
||||
@Field("pickup_salutation")
|
||||
private String pickupSalutation;
|
||||
|
||||
@Field("pickup_first_name")
|
||||
private String pickupFirstName;
|
||||
|
||||
@Field("pickup_last_name")
|
||||
private String pickupLastName;
|
||||
|
||||
@Field("pickup_phone")
|
||||
private String pickupPhone;
|
||||
|
||||
@Field("pickup_street")
|
||||
private String pickupStreet;
|
||||
|
||||
@Field("pickup_house_number")
|
||||
private String pickupHouseNumber;
|
||||
|
||||
@Field("pickup_address_addition")
|
||||
private String pickupAddressAddition;
|
||||
|
||||
@Field("pickup_zip")
|
||||
private String pickupZip;
|
||||
|
||||
@Field("pickup_city")
|
||||
private String pickupCity;
|
||||
|
||||
// Lieferadresse
|
||||
@Field("delivery_company")
|
||||
private String deliveryCompany;
|
||||
|
||||
@Field("delivery_salutation")
|
||||
private String deliverySalutation;
|
||||
|
||||
@Field("delivery_first_name")
|
||||
private String deliveryFirstName;
|
||||
|
||||
@Field("delivery_last_name")
|
||||
private String deliveryLastName;
|
||||
|
||||
@Field("delivery_phone")
|
||||
private String deliveryPhone;
|
||||
|
||||
@Field("delivery_street")
|
||||
private String deliveryStreet;
|
||||
|
||||
@Field("delivery_house_number")
|
||||
private String deliveryHouseNumber;
|
||||
|
||||
@Field("delivery_address_addition")
|
||||
private String deliveryAddressAddition;
|
||||
|
||||
@Field("delivery_zip")
|
||||
private String deliveryZip;
|
||||
|
||||
@Field("delivery_city")
|
||||
private String deliveryCity;
|
||||
|
||||
// Digitale Abwicklung per App
|
||||
@Field("digital_processing")
|
||||
private boolean digitalProcessing;
|
||||
|
||||
@Field("app_user")
|
||||
private String appUser;
|
||||
|
||||
// Termine
|
||||
@Field("pickup_date")
|
||||
private LocalDate pickupDate;
|
||||
|
||||
@Field("pickup_time")
|
||||
private LocalTime pickupTime;
|
||||
|
||||
@Field("delivery_date")
|
||||
private LocalDate deliveryDate;
|
||||
|
||||
@Field("delivery_time")
|
||||
private LocalTime deliveryTime;
|
||||
|
||||
// Bemerkung
|
||||
@Field("remark")
|
||||
private String remark;
|
||||
|
||||
// Preis (netto)
|
||||
@Field("price")
|
||||
private BigDecimal price;
|
||||
|
||||
// Gefahrene Kilometer für Rechnungsstellung
|
||||
@Field("kilometers_driven")
|
||||
private Integer kilometersDriven;
|
||||
|
||||
// Arbeitszeit in 15-Minuten-Einheiten für Rechnungsstellung
|
||||
@Field("time_in_15min_units")
|
||||
private Integer timeIn15MinUnits;
|
||||
|
||||
// Service-IDs für die Rechnung
|
||||
@Field("service_ids")
|
||||
private List<String> serviceIds;
|
||||
|
||||
// Ausgewählte Leistungen inkl. zugeordneter Lieferstation und Berechnungsbasis
|
||||
@Field("selected_services")
|
||||
private List<JobServiceSelection> selectedServices = new ArrayList<>();
|
||||
|
||||
// Streckeninformation für die Rechnung (in km)
|
||||
@Field("route_distance_km")
|
||||
private Double routeDistanceKm;
|
||||
|
||||
// Fahrtzeit in Sekunden für die Rechnung
|
||||
@Field("route_duration_seconds")
|
||||
private Integer routeDurationSeconds;
|
||||
|
||||
// Referenz auf die erstellte Rechnung
|
||||
@Field("invoice_id")
|
||||
private String invoiceId;
|
||||
|
||||
// Lieferstationen (bis zu 25)
|
||||
@Field("delivery_stations")
|
||||
private List<DeliveryStation> deliveryStations = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Returns the ObjectId as string for JSON serialization. This ensures that the
|
||||
* job id is returned as a string when jobs are retrieved via API.
|
||||
*/
|
||||
@JsonGetter("id")
|
||||
public String getIdAsString() {
|
||||
return id != null ? id.toString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first delivery station's city. Falls back to the flat
|
||||
* deliveryCity field for backward compatibility with old jobs.
|
||||
*/
|
||||
public String getFirstDeliveryCity() {
|
||||
if (deliveryStations != null && !deliveryStations.isEmpty()) {
|
||||
return deliveryStations.get(0).getCity();
|
||||
}
|
||||
return deliveryCity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last delivery station's city for route display.
|
||||
*/
|
||||
public String getLastDeliveryCity() {
|
||||
if (deliveryStations != null && !deliveryStations.isEmpty()) {
|
||||
return deliveryStations.get(deliveryStations.size() - 1).getCity();
|
||||
}
|
||||
return deliveryCity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all delivery cities joined with arrows for display (e.g. "Berlin →
|
||||
* Dresden → München").
|
||||
*/
|
||||
public String getDeliveryCitiesDisplay() {
|
||||
if (deliveryStations != null && !deliveryStations.isEmpty()) {
|
||||
return deliveryStations.stream().map(DeliveryStation::getCity).filter(c -> c != null && !c.isBlank())
|
||||
.collect(Collectors.joining(" \u2192 "));
|
||||
}
|
||||
return deliveryCity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the flat delivery fields from the first delivery station for
|
||||
* backward compatibility. Call this before saving when using delivery stations.
|
||||
*/
|
||||
public void syncFlatDeliveryFieldsFromStations() {
|
||||
if (deliveryStations != null && !deliveryStations.isEmpty()) {
|
||||
DeliveryStation first = deliveryStations.get(0);
|
||||
this.deliveryCompany = first.getCompany();
|
||||
this.deliverySalutation = first.getSalutation();
|
||||
this.deliveryFirstName = first.getFirstName();
|
||||
this.deliveryLastName = first.getLastName();
|
||||
this.deliveryPhone = first.getPhone();
|
||||
this.deliveryStreet = first.getStreet();
|
||||
this.deliveryHouseNumber = first.getHouseNumber();
|
||||
this.deliveryAddressAddition = first.getAddressAddition();
|
||||
this.deliveryZip = first.getZip();
|
||||
this.deliveryCity = first.getCity();
|
||||
this.deliveryDate = first.getDeliveryDate();
|
||||
this.deliveryTime = first.getDeliveryTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Job History entity for tracking all changes made to a job. Each entry
|
||||
* represents a single change or action performed on a job.
|
||||
*/
|
||||
@Data
|
||||
@Document(collection = "job_history")
|
||||
public class JobHistory {
|
||||
|
||||
@Id
|
||||
private ObjectId id;
|
||||
|
||||
/**
|
||||
* Reference to the job this history entry belongs to
|
||||
*/
|
||||
private ObjectId jobId;
|
||||
|
||||
/**
|
||||
* Timestamp when the change occurred
|
||||
*/
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
/**
|
||||
* Reason for the change (e.g., "Status Update", "User Edit", "System Update")
|
||||
*/
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* Description of what was changed (e.g., "Status changed from CREATED to
|
||||
* IN_PROGRESS")
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* User who made the change (can be null for system changes)
|
||||
*/
|
||||
private String changedBy;
|
||||
|
||||
/**
|
||||
* Additional details about the change (optional)
|
||||
*/
|
||||
private String details;
|
||||
|
||||
/**
|
||||
* Type of change (CREATE, UPDATE, STATUS_CHANGE, DELETE, etc.)
|
||||
*/
|
||||
private JobHistoryType changeType;
|
||||
|
||||
/**
|
||||
* Old value (for comparison, stored as JSON string if complex)
|
||||
*/
|
||||
private String oldValue;
|
||||
|
||||
/**
|
||||
* New value (for comparison, stored as JSON string if complex)
|
||||
*/
|
||||
private String newValue;
|
||||
|
||||
// Default constructor
|
||||
public JobHistory() {
|
||||
this.timestamp = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// Constructor for basic history entry
|
||||
public JobHistory(ObjectId jobId, String reason, String description, String changedBy) {
|
||||
this();
|
||||
this.jobId = jobId;
|
||||
this.reason = reason;
|
||||
this.description = description;
|
||||
this.changedBy = changedBy;
|
||||
}
|
||||
|
||||
// Constructor for detailed history entry
|
||||
public JobHistory(ObjectId jobId, String reason, String description, String changedBy, JobHistoryType changeType,
|
||||
String oldValue, String newValue) {
|
||||
this(jobId, reason, description, changedBy);
|
||||
this.changeType = changeType;
|
||||
this.oldValue = oldValue;
|
||||
this.newValue = newValue;
|
||||
}
|
||||
|
||||
// Getter for ID as String
|
||||
public String getIdAsString() {
|
||||
return id != null ? id.toHexString() : null;
|
||||
}
|
||||
|
||||
// Getter for Job ID as String
|
||||
public String getJobIdAsString() {
|
||||
return jobId != null ? jobId.toHexString() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
/**
|
||||
* Enumeration of different types of job history changes
|
||||
*/
|
||||
public enum JobHistoryType {
|
||||
/**
|
||||
* Job was created
|
||||
*/
|
||||
CREATE,
|
||||
|
||||
/**
|
||||
* Job data was updated
|
||||
*/
|
||||
UPDATE,
|
||||
|
||||
/**
|
||||
* Job status was changed
|
||||
*/
|
||||
STATUS_CHANGE,
|
||||
|
||||
/**
|
||||
* Job was assigned to a user
|
||||
*/
|
||||
ASSIGNMENT,
|
||||
|
||||
/**
|
||||
* Task was completed within the job
|
||||
*/
|
||||
TASK_COMPLETED,
|
||||
|
||||
/**
|
||||
* Job was exported or shared
|
||||
*/
|
||||
EXPORT,
|
||||
|
||||
/**
|
||||
* Job was deleted or archived
|
||||
*/
|
||||
DELETE,
|
||||
|
||||
/**
|
||||
* System-generated change
|
||||
*/
|
||||
SYSTEM,
|
||||
|
||||
/**
|
||||
* Comment or note was added
|
||||
*/
|
||||
COMMENT,
|
||||
|
||||
/**
|
||||
* Other type of change
|
||||
*/
|
||||
OTHER
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
@Data
|
||||
public class JobServiceSelection {
|
||||
|
||||
@Field("service_id")
|
||||
private String serviceId;
|
||||
|
||||
@Field("delivery_station_order")
|
||||
private Integer deliveryStationOrder;
|
||||
|
||||
@Field("route_distance_km")
|
||||
private Double routeDistanceKm;
|
||||
|
||||
@Field("route_duration_seconds")
|
||||
private Integer routeDurationSeconds;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
/**
|
||||
* Status-Enum für Aufträge
|
||||
*/
|
||||
public enum JobStatus {
|
||||
CREATED("Erstellt"),
|
||||
IN_PROGRESS("In Bearbeitung"),
|
||||
PICKUP_SCHEDULED("Abholung geplant"),
|
||||
PICKED_UP("Abgeholt"),
|
||||
IN_TRANSIT("Unterwegs"),
|
||||
DELIVERED("Zugestellt"),
|
||||
COMPLETED("Abgeschlossen"),
|
||||
CANCELLED("Storniert");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
JobStatus(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
public enum Language {
|
||||
DE("Deutsch"),
|
||||
EN("English"),
|
||||
FR("Français"),
|
||||
ES("Español"),
|
||||
TR("Türkçe"),
|
||||
PL("Polski"),
|
||||
RU("Русский"),
|
||||
EE("Eesti"),
|
||||
LV("Latviešu"),
|
||||
LT("Lietuvių");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
Language(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public static Language fromString(String text) {
|
||||
for (Language language : Language.values()) {
|
||||
if (language.name().equalsIgnoreCase(text)) {
|
||||
return language;
|
||||
}
|
||||
}
|
||||
return DE; // Default to German
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Represents a GPS position reported by a mobile client.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Document(collection = "location_positions")
|
||||
public class LocationPosition {
|
||||
|
||||
@Id
|
||||
private ObjectId id;
|
||||
|
||||
/**
|
||||
* AppUser ID (clientId) - the user who sent this position
|
||||
*/
|
||||
@Field("app_user_id")
|
||||
@Indexed
|
||||
private String appUserId;
|
||||
|
||||
/**
|
||||
* Latitude in decimal degrees
|
||||
*/
|
||||
@Field("latitude")
|
||||
private Double latitude;
|
||||
|
||||
/**
|
||||
* Longitude in decimal degrees
|
||||
*/
|
||||
@Field("longitude")
|
||||
private Double longitude;
|
||||
|
||||
/**
|
||||
* Accuracy of the position in meters
|
||||
*/
|
||||
@Field("accuracy")
|
||||
private Double accuracy;
|
||||
|
||||
/**
|
||||
* Altitude in meters above sea level (optional)
|
||||
*/
|
||||
@Field("altitude")
|
||||
private Double altitude;
|
||||
|
||||
/**
|
||||
* Speed in meters per second (optional)
|
||||
*/
|
||||
@Field("speed")
|
||||
private Double speed;
|
||||
|
||||
/**
|
||||
* Heading in degrees (0-360) (optional)
|
||||
*/
|
||||
@Field("heading")
|
||||
private Double heading;
|
||||
|
||||
/**
|
||||
* Timestamp when the position was reported (from client)
|
||||
*/
|
||||
@Field("timestamp")
|
||||
private Instant timestamp;
|
||||
|
||||
/**
|
||||
* Timestamp when the position was received by the server
|
||||
*/
|
||||
@Field("received_at")
|
||||
@Indexed(expireAfter = "3600s") // TTL index: auto-delete after 60 minutes
|
||||
private Instant receivedAt;
|
||||
|
||||
public LocationPosition(String appUserId, Double latitude, Double longitude, Double accuracy, Double altitude,
|
||||
Double speed, Double heading, Instant timestamp) {
|
||||
this.appUserId = appUserId;
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
this.accuracy = accuracy;
|
||||
this.altitude = altitude;
|
||||
this.speed = speed;
|
||||
this.heading = heading;
|
||||
this.timestamp = timestamp;
|
||||
this.receivedAt = Instant.now();
|
||||
}
|
||||
}
|
||||
190
backend/src/main/java/de/assecutor/votianlt/model/Message.java
Normal file
190
backend/src/main/java/de/assecutor/votianlt/model/Message.java
Normal file
@@ -0,0 +1,190 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Represents a message that can be sent between the server and clients.
|
||||
* Messages can be either job-related or general messages.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Document(collection = "messages")
|
||||
public class Message {
|
||||
|
||||
@Id
|
||||
@JsonIgnore
|
||||
private ObjectId id;
|
||||
|
||||
/**
|
||||
* Content of the message, either plain text or base64 encoded media
|
||||
*/
|
||||
@Field("content")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* Declares how to interpret the content payload
|
||||
*/
|
||||
@Field("content_type")
|
||||
private MessageContentType contentType = MessageContentType.TEXT;
|
||||
|
||||
/**
|
||||
* AppUser ID (clientId) - the AppUser to whom this message belongs
|
||||
*/
|
||||
@Field("receiver")
|
||||
private String receiver;
|
||||
|
||||
/**
|
||||
* Timestamp when the message was created
|
||||
*/
|
||||
@Field("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* Origin of the message: INCOMING (from client), OUTGOING (to client), or
|
||||
* SERVER (from server)
|
||||
*/
|
||||
@Field("origin")
|
||||
private MessageOrigin origin;
|
||||
|
||||
/**
|
||||
* Type of message: JOB_RELATED or GENERAL
|
||||
*/
|
||||
@Field("message_type")
|
||||
private MessageType messageType;
|
||||
|
||||
/**
|
||||
* Optional reference to a job (only for job-related messages)
|
||||
*/
|
||||
@Field("job_id")
|
||||
private ObjectId jobId;
|
||||
|
||||
/**
|
||||
* Optional job number for easier reference (denormalized)
|
||||
*/
|
||||
@Field("job_number")
|
||||
private String jobNumber;
|
||||
|
||||
/**
|
||||
* Whether the message has been read by the receiver
|
||||
*/
|
||||
@Field("is_read")
|
||||
private boolean isRead;
|
||||
|
||||
/**
|
||||
* Timestamp when the message was read
|
||||
*/
|
||||
@Field("read_at")
|
||||
private LocalDateTime readAt;
|
||||
|
||||
/**
|
||||
* Delivery status: NOTSEND (failed to deliver), SEND (successfully delivered)
|
||||
*/
|
||||
@Field("delivery_status")
|
||||
private MessageDeliveryStatus deliveryStatus;
|
||||
|
||||
/**
|
||||
* Constructor for general messages
|
||||
*/
|
||||
public Message(String content, String receiver, MessageOrigin origin) {
|
||||
this(content, receiver, origin, MessageContentType.TEXT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for general messages with explicit content type
|
||||
*/
|
||||
public Message(String content, String receiver, MessageOrigin origin, MessageContentType contentType) {
|
||||
initializeBaseFields(content, receiver, origin, contentType);
|
||||
this.messageType = MessageType.GENERAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for job-related messages
|
||||
*/
|
||||
public Message(String content, String receiver, MessageOrigin origin, ObjectId jobId, String jobNumber) {
|
||||
this(content, receiver, origin, MessageContentType.TEXT, jobId, jobNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for job-related messages with explicit content type
|
||||
*/
|
||||
public Message(String content, String receiver, MessageOrigin origin, MessageContentType contentType,
|
||||
ObjectId jobId, String jobNumber) {
|
||||
initializeBaseFields(content, receiver, origin, contentType);
|
||||
this.messageType = MessageType.JOB_RELATED;
|
||||
this.jobId = jobId;
|
||||
this.jobNumber = jobNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the message as read
|
||||
*/
|
||||
public void markAsRead() {
|
||||
this.isRead = true;
|
||||
this.readAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the message as sent (successfully delivered)
|
||||
*/
|
||||
public void markAsSent() {
|
||||
this.deliveryStatus = MessageDeliveryStatus.SEND;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the message as not sent (delivery failed)
|
||||
*/
|
||||
public void markAsNotSent() {
|
||||
this.deliveryStatus = MessageDeliveryStatus.NOTSEND;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if message was successfully delivered
|
||||
*/
|
||||
public boolean isDelivered() {
|
||||
return deliveryStatus == MessageDeliveryStatus.SEND;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ObjectId as string for JSON serialization
|
||||
*/
|
||||
@JsonGetter("id")
|
||||
public String getIdAsString() {
|
||||
return id != null ? id.toString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the job ObjectId as string for JSON serialization
|
||||
*/
|
||||
@JsonGetter("jobId")
|
||||
public String getJobIdAsString() {
|
||||
return jobId != null ? jobId.toString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure callers always receive a non-null content type to simplify rendering
|
||||
*/
|
||||
public MessageContentType getContentType() {
|
||||
return contentType != null ? contentType : MessageContentType.TEXT;
|
||||
}
|
||||
|
||||
private void initializeBaseFields(String content, String receiver, MessageOrigin origin,
|
||||
MessageContentType contentType) {
|
||||
this.content = content;
|
||||
this.receiver = receiver;
|
||||
this.origin = origin;
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.isRead = false;
|
||||
this.contentType = contentType != null ? contentType : MessageContentType.TEXT;
|
||||
// Server messages start as NOTSEND until confirmed delivered
|
||||
this.deliveryStatus = (origin == MessageOrigin.SERVER) ? MessageDeliveryStatus.NOTSEND : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
/**
|
||||
* Supported content variants for chat messages.
|
||||
*/
|
||||
public enum MessageContentType {
|
||||
TEXT, IMAGE
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
/**
|
||||
* Delivery status for messages sent to clients. Tracks whether a message was
|
||||
* successfully delivered via WebSocket.
|
||||
*/
|
||||
public enum MessageDeliveryStatus {
|
||||
NOTSEND("Nicht gesendet"), SEND("Gesendet");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
MessageDeliveryStatus(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
/**
|
||||
* Enum representing the origin of a message
|
||||
*/
|
||||
public enum MessageOrigin {
|
||||
/**
|
||||
* Message received from a client (app user)
|
||||
*/
|
||||
CLIENT,
|
||||
|
||||
/**
|
||||
* Message sent from the server
|
||||
*/
|
||||
SERVER
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
/**
|
||||
* Enum representing the type of message
|
||||
*/
|
||||
public enum MessageType {
|
||||
/**
|
||||
* General message not related to a specific job
|
||||
*/
|
||||
GENERAL,
|
||||
|
||||
/**
|
||||
* Message related to a specific job
|
||||
*/
|
||||
JOB_RELATED
|
||||
}
|
||||
38
backend/src/main/java/de/assecutor/votianlt/model/Photo.java
Normal file
38
backend/src/main/java/de/assecutor/votianlt/model/Photo.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Photo entity for storing photo data from task completions. References the job
|
||||
* ObjectId and stores base64 encoded photos.
|
||||
*/
|
||||
@Data
|
||||
@Document(collection = "photos")
|
||||
public class Photo {
|
||||
|
||||
@Id
|
||||
private ObjectId id;
|
||||
|
||||
private ObjectId taskId;
|
||||
private String photo; // base64 encoded photos
|
||||
private LocalDateTime createdAt;
|
||||
private String completedBy;
|
||||
|
||||
// Default constructor
|
||||
public Photo() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// Constructor with parameters
|
||||
public Photo(ObjectId taskId, String photo, String completedBy) {
|
||||
this();
|
||||
this.taskId = taskId;
|
||||
this.photo = photo;
|
||||
this.completedBy = completedBy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
@Document(collection = "price_table")
|
||||
public class PriceTable {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
private String monthlyBasePackage;
|
||||
private String appUsageLicense;
|
||||
private String revenueParticipation;
|
||||
private String statisticalEvaluation;
|
||||
|
||||
public PriceTable() {
|
||||
}
|
||||
|
||||
public PriceTable(String monthlyBasePackage, String appUsageLicense, String revenueParticipation,
|
||||
String statisticalEvaluation) {
|
||||
this.monthlyBasePackage = monthlyBasePackage;
|
||||
this.appUsageLicense = appUsageLicense;
|
||||
this.revenueParticipation = revenueParticipation;
|
||||
this.statisticalEvaluation = statisticalEvaluation;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getMonthlyBasePackage() {
|
||||
return monthlyBasePackage;
|
||||
}
|
||||
|
||||
public void setMonthlyBasePackage(String monthlyBasePackage) {
|
||||
this.monthlyBasePackage = monthlyBasePackage;
|
||||
}
|
||||
|
||||
public String getAppUsageLicense() {
|
||||
return appUsageLicense;
|
||||
}
|
||||
|
||||
public void setAppUsageLicense(String appUsageLicense) {
|
||||
this.appUsageLicense = appUsageLicense;
|
||||
}
|
||||
|
||||
public String getRevenueParticipation() {
|
||||
return revenueParticipation;
|
||||
}
|
||||
|
||||
public void setRevenueParticipation(String revenueParticipation) {
|
||||
this.revenueParticipation = revenueParticipation;
|
||||
}
|
||||
|
||||
public String getStatisticalEvaluation() {
|
||||
return statisticalEvaluation;
|
||||
}
|
||||
|
||||
public void setStatisticalEvaluation(String statisticalEvaluation) {
|
||||
this.statisticalEvaluation = statisticalEvaluation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Speichert das Ergebnis einer Routenberechnung zwischen zwei Adressen.
|
||||
*/
|
||||
@Data
|
||||
public class RouteCalculationResult {
|
||||
|
||||
private boolean valid;
|
||||
private double distanceKm;
|
||||
private int durationSeconds;
|
||||
private String formattedDistance;
|
||||
private String formattedDuration;
|
||||
private String routeMessage;
|
||||
|
||||
public RouteCalculationResult() {
|
||||
this.valid = false;
|
||||
this.distanceKm = 0.0;
|
||||
this.durationSeconds = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Dauer in Minuten zurück
|
||||
*/
|
||||
public int getDurationMinutes() {
|
||||
return durationSeconds / 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Dauer formatiert zurück (z.B. "1 Std. 30 Min." oder "45 Min.")
|
||||
*/
|
||||
public String getFormattedDurationLong() {
|
||||
int hours = durationSeconds / 3600;
|
||||
int minutes = (durationSeconds % 3600) / 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return String.format("%d Std. %d Min.", hours, minutes);
|
||||
} else {
|
||||
return String.format("%d Min.", minutes);
|
||||
}
|
||||
}
|
||||
}
|
||||
136
backend/src/main/java/de/assecutor/votianlt/model/Service.java
Normal file
136
backend/src/main/java/de/assecutor/votianlt/model/Service.java
Normal file
@@ -0,0 +1,136 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Document(collection = "services")
|
||||
public class Service {
|
||||
public static final BigDecimal FIXED_VAT_RATE = new BigDecimal("0.19");
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
private String userId;
|
||||
private String name;
|
||||
private CalculationBasis calculationBasis;
|
||||
private BigDecimal price; // For FLAT_RATE services
|
||||
private BigDecimal pricePerKilometer; // For DISTANCE services - price per kilometer
|
||||
private BigDecimal pricePer15Minutes; // For TIME services - price per 15 minutes
|
||||
private boolean mandatory;
|
||||
|
||||
public enum CalculationBasis {
|
||||
DISTANCE, TIME, FLAT_RATE
|
||||
}
|
||||
|
||||
public Service() {
|
||||
}
|
||||
|
||||
public Service(String userId, String name, CalculationBasis calculationBasis, BigDecimal price,
|
||||
BigDecimal vatRate) {
|
||||
this(userId, name, calculationBasis, price, vatRate, false);
|
||||
}
|
||||
|
||||
public Service(String userId, String name, CalculationBasis calculationBasis, BigDecimal price, BigDecimal vatRate,
|
||||
boolean mandatory) {
|
||||
this.userId = userId;
|
||||
this.name = name;
|
||||
this.calculationBasis = calculationBasis;
|
||||
this.mandatory = mandatory;
|
||||
|
||||
// Set the appropriate price field based on calculation basis
|
||||
switch (calculationBasis) {
|
||||
case DISTANCE:
|
||||
this.pricePerKilometer = price;
|
||||
break;
|
||||
case TIME:
|
||||
this.pricePer15Minutes = price;
|
||||
break;
|
||||
case FLAT_RATE:
|
||||
this.price = price;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public CalculationBasis getCalculationBasis() {
|
||||
return calculationBasis;
|
||||
}
|
||||
|
||||
public void setCalculationBasis(CalculationBasis calculationBasis) {
|
||||
this.calculationBasis = calculationBasis;
|
||||
}
|
||||
|
||||
public BigDecimal getVatRate() {
|
||||
return FIXED_VAT_RATE;
|
||||
}
|
||||
|
||||
public void setVatRate(BigDecimal vatRate) {
|
||||
}
|
||||
|
||||
public BigDecimal getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public void setPrice(BigDecimal price) {
|
||||
this.price = price;
|
||||
}
|
||||
|
||||
public BigDecimal getPricePerKilometer() {
|
||||
return pricePerKilometer;
|
||||
}
|
||||
|
||||
public void setPricePerKilometer(BigDecimal pricePerKilometer) {
|
||||
this.pricePerKilometer = pricePerKilometer;
|
||||
}
|
||||
|
||||
public BigDecimal getPricePer15Minutes() {
|
||||
return pricePer15Minutes;
|
||||
}
|
||||
|
||||
public void setPricePer15Minutes(BigDecimal pricePer15Minutes) {
|
||||
this.pricePer15Minutes = pricePer15Minutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate price based on calculation basis
|
||||
*/
|
||||
public BigDecimal getEffectivePrice() {
|
||||
return switch (calculationBasis) {
|
||||
case DISTANCE -> pricePerKilometer;
|
||||
case TIME -> pricePer15Minutes;
|
||||
case FLAT_RATE -> price;
|
||||
};
|
||||
}
|
||||
|
||||
public boolean isMandatory() {
|
||||
return mandatory;
|
||||
}
|
||||
|
||||
public void setMandatory(boolean mandatory) {
|
||||
this.mandatory = mandatory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Signature entity for storing signature SVG data from task completions.
|
||||
* References the task ObjectId and stores SVG signature strings.
|
||||
*/
|
||||
@Data
|
||||
@Document(collection = "signatures")
|
||||
public class Signature {
|
||||
|
||||
@Id
|
||||
private ObjectId id;
|
||||
|
||||
private ObjectId taskId;
|
||||
private String signatureSvg;
|
||||
private LocalDateTime createdAt;
|
||||
private String completedBy;
|
||||
|
||||
// Default constructor
|
||||
public Signature() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// Constructor with parameters
|
||||
public Signature(ObjectId taskId, String signatureSvg, String completedBy) {
|
||||
this();
|
||||
this.taskId = taskId;
|
||||
this.signatureSvg = signatureSvg;
|
||||
this.completedBy = completedBy;
|
||||
}
|
||||
}
|
||||
121
backend/src/main/java/de/assecutor/votianlt/model/TaskEntry.java
Normal file
121
backend/src/main/java/de/assecutor/votianlt/model/TaskEntry.java
Normal file
@@ -0,0 +1,121 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Document(collection = "tasks")
|
||||
public class TaskEntry {
|
||||
@Id
|
||||
@JsonIgnore
|
||||
private ObjectId id;
|
||||
|
||||
@Field("station_id")
|
||||
@JsonIgnore
|
||||
private ObjectId stationId;
|
||||
|
||||
@Field("job_id")
|
||||
@JsonIgnore
|
||||
private ObjectId jobId;
|
||||
|
||||
@Field("task_type")
|
||||
private TaskType taskType = TaskType.CONFIRMATION;
|
||||
|
||||
// Task-specific configuration data
|
||||
@Field("configuration")
|
||||
private TaskConfiguration configuration;
|
||||
|
||||
// Completion tracking
|
||||
@Field("completed")
|
||||
private boolean completed = false;
|
||||
|
||||
@Field("completed_at")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
@Field("completed_by")
|
||||
private String completedBy;
|
||||
|
||||
/**
|
||||
* Returns the ObjectId as string for JSON serialization. This ensures that the
|
||||
* task id is returned as a string when jobs are retrieved via API.
|
||||
*/
|
||||
@JsonGetter("id")
|
||||
public String getIdAsString() {
|
||||
return id != null ? id.toString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the station ObjectId as string for JSON serialization.
|
||||
*/
|
||||
@JsonGetter("stationId")
|
||||
public String getStationIdAsString() {
|
||||
return stationId != null ? stationId.toHexString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the legacy job ObjectId as string for internal fallback handling.
|
||||
*/
|
||||
public String getJobIdAsString() {
|
||||
return jobId != null ? jobId.toString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum for different task types
|
||||
*/
|
||||
public enum TaskType {
|
||||
CONFIRMATION(
|
||||
"Bestätigung"),
|
||||
SIGNATURE("Unterschrift"),
|
||||
TODOLIST("To-Do Liste"),
|
||||
PHOTO("Foto"),
|
||||
BARCODE("Barcode");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
TaskType(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration data for different task types
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class TaskConfiguration {
|
||||
// For CONFIRMATION: button text
|
||||
private String buttonText;
|
||||
|
||||
// For TODOLIST: list of todo items
|
||||
private List<String> todoItems;
|
||||
|
||||
// For PHOTO: min and max photo count
|
||||
private Integer minPhotoCount;
|
||||
private Integer maxPhotoCount;
|
||||
|
||||
// For BARCODE: min and max barcode count
|
||||
private Integer minBarcodeCount;
|
||||
private Integer maxBarcodeCount;
|
||||
|
||||
// Generic configuration map for future extensions
|
||||
private Map<String, Object> additionalConfig;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import de.assecutor.votianlt.model.task.BaseTask;
|
||||
import lombok.Data;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Document(collection = "task_templates")
|
||||
public class TaskTemplate {
|
||||
|
||||
@Id
|
||||
private ObjectId id;
|
||||
|
||||
@Indexed
|
||||
private ObjectId userId;
|
||||
|
||||
private String templateName;
|
||||
private List<BaseTask> tasks;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public TaskTemplate() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void updateTimestamp() {
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MongoDB document for caching LLM translations. Stores the original text, all
|
||||
* translations keyed by language code, and the insertion timestamp.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Document(collection = "translation_cache")
|
||||
public class TranslationCacheEntry {
|
||||
|
||||
@Id
|
||||
private ObjectId id;
|
||||
|
||||
/**
|
||||
* The original text that was translated.
|
||||
*/
|
||||
@Indexed(unique = true)
|
||||
@Field("source_text")
|
||||
private String sourceText;
|
||||
|
||||
/**
|
||||
* Translations keyed by language code (e.g. "de", "en", "fr").
|
||||
*/
|
||||
@Field("translations")
|
||||
private Map<String, String> translations;
|
||||
|
||||
/**
|
||||
* When this entry was inserted into the cache.
|
||||
*/
|
||||
@Indexed
|
||||
@Field("inserted_at")
|
||||
private LocalDateTime insertedAt;
|
||||
|
||||
public TranslationCacheEntry(String sourceText, Map<String, String> translations) {
|
||||
this.sourceText = sourceText;
|
||||
this.translations = translations;
|
||||
this.insertedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
71
backend/src/main/java/de/assecutor/votianlt/model/User.java
Normal file
71
backend/src/main/java/de/assecutor/votianlt/model/User.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
|
||||
@Data
|
||||
@Document(collection = "users")
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
private ObjectId id;
|
||||
|
||||
private int usrId;
|
||||
private String title;
|
||||
private String name; // Nachname
|
||||
private String firstname; // Vorname
|
||||
|
||||
// Firmen-/Adressdaten
|
||||
private String company; // Firma
|
||||
private String companyAddition; // Firmenzusatz
|
||||
private String street; // Straße
|
||||
private String houseNumber; // Hausnr
|
||||
private String addressAddition; // Adresszusatz (optional)
|
||||
private String zip; // Postleitzahl
|
||||
private String city; // Stadt
|
||||
|
||||
// Abweichende Rechnungsadresse
|
||||
private boolean diffInvoiceAddress; // Checkbox für abweichende Rechnungsadresse
|
||||
private String invCompany; // Rechnungsadresse: Firma
|
||||
private String invCompanyAddition; // Rechnungsadresse: Firmenzusatz
|
||||
private String invFirstname; // Rechnungsadresse: Vorname
|
||||
private String invLastname; // Rechnungsadresse: Nachname
|
||||
private String invStreet; // Rechnungsadresse: Straße
|
||||
private String invHouseNumber; // Rechnungsadresse: Hausnr
|
||||
private String invAddressAddition; // Rechnungsadresse: Adresszusatz
|
||||
private String invZip; // Rechnungsadresse: Postleitzahl
|
||||
private String invCity; // Rechnungsadresse: Stadt
|
||||
|
||||
@Indexed(unique = true)
|
||||
private String email;
|
||||
|
||||
private String phone;
|
||||
private String phone2;
|
||||
private String fax;
|
||||
private String password;
|
||||
private byte isActivated;
|
||||
private byte isEmailConfirmed;
|
||||
private String passwordCode;
|
||||
private LocalDateTime passwordTimestamp;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
private Set<String> roles;
|
||||
|
||||
// Digitale Abwicklung und App-Nutzer Ortung
|
||||
private boolean digitalProcessingEnabled = true; // Digitale Abwicklung per App
|
||||
private boolean locationTrackingEnabled = true; // App-Nutzer orten
|
||||
|
||||
// 2-Faktor-Authentifizierung (standardmäßig aktiviert für neue Nutzer)
|
||||
private boolean twoFactorEnabled = true;
|
||||
|
||||
// Spracheinstellung (standardmäßig Deutsch)
|
||||
@Field("language")
|
||||
private Language language = Language.DE;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.Data;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Document(collection = "user_invoice_data")
|
||||
public class UserInvoiceData {
|
||||
|
||||
@Id
|
||||
private ObjectId id;
|
||||
|
||||
@Indexed
|
||||
private ObjectId userId;
|
||||
|
||||
private boolean billingEnabled;
|
||||
private String prefix;
|
||||
private String ustId;
|
||||
private String taxNumber;
|
||||
private String bankName;
|
||||
private String iban;
|
||||
private String taxRate;
|
||||
private String introText;
|
||||
private String paymentTerms;
|
||||
|
||||
private long nextInvoiceNumber = 0;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public UserInvoiceData() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void updateTimestamp() {
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
package de.assecutor.votianlt.model.invoices;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import java.time.LocalDate;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Document(collection = "customerInvoices")
|
||||
public class CustomerInvoice {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
// Pflichtangaben nach §14 UStG (German VAT law)
|
||||
private String invoiceNumber; // Fortlaufende Rechnungsnummer
|
||||
private LocalDate invoiceDate; // Rechnungsdatum
|
||||
private LocalDate deliveryDate; // Leistungsdatum
|
||||
|
||||
// Rechnungssteller (Sender)
|
||||
private String senderName;
|
||||
private String senderAddress;
|
||||
private String senderPostcode;
|
||||
private String senderCity;
|
||||
private String senderCountry;
|
||||
private String senderTaxNumber; // Steuernummer
|
||||
private String senderVatId; // USt-IdNr.
|
||||
private String senderPhone;
|
||||
private String senderEmail;
|
||||
private String senderWebsite;
|
||||
|
||||
// Rechnungsempfänger (Recipient)
|
||||
private String recipientName;
|
||||
private String recipientCompany;
|
||||
private String recipientAddress;
|
||||
private String recipientPostcode;
|
||||
private String recipientCity;
|
||||
private String recipientCountry;
|
||||
private String recipientVatId; // USt-IdNr. des Empfängers (falls vorhanden)
|
||||
|
||||
// Rechnungsdetails
|
||||
private String description; // Beschreibung der Leistung
|
||||
private List<CustomerInvoiceItem> items;
|
||||
|
||||
// Beträge
|
||||
private BigDecimal netAmount; // Nettobetrag
|
||||
private BigDecimal vatRate; // Steuersatz (z.B. 19% = 0.19)
|
||||
private BigDecimal vatAmount; // Steuerbetrag
|
||||
private BigDecimal totalAmount; // Bruttobetrag
|
||||
|
||||
// Zahlungsdetails
|
||||
private String paymentTerms; // Zahlungsbedingungen
|
||||
private LocalDate paymentDueDate; // Fälligkeitsdatum
|
||||
private String bankAccount; // Bankverbindung
|
||||
private String iban;
|
||||
private String bic;
|
||||
|
||||
// Zusätzliche rechtliche Angaben
|
||||
private String legalNotes; // Rechtliche Hinweise
|
||||
private String reverseChargeNote; // Hinweis auf Reverse Charge (falls zutreffend)
|
||||
|
||||
// Verknüpfung mit Auftrag und Benutzer
|
||||
private String jobId; // Referenz auf den Auftrag
|
||||
private String userId; // Referenz auf den Benutzer (Rechnungsersteller)
|
||||
|
||||
// Gespeicherte PDF-Daten (Base64-kodiert)
|
||||
private byte[] pdfData;
|
||||
|
||||
// Constructors
|
||||
public CustomerInvoice() {
|
||||
}
|
||||
|
||||
public CustomerInvoice(String invoiceNumber, LocalDate invoiceDate, String description) {
|
||||
this.invoiceNumber = invoiceNumber;
|
||||
this.invoiceDate = invoiceDate;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getInvoiceNumber() {
|
||||
return invoiceNumber;
|
||||
}
|
||||
|
||||
public void setInvoiceNumber(String invoiceNumber) {
|
||||
this.invoiceNumber = invoiceNumber;
|
||||
}
|
||||
|
||||
public LocalDate getInvoiceDate() {
|
||||
return invoiceDate;
|
||||
}
|
||||
|
||||
public void setInvoiceDate(LocalDate invoiceDate) {
|
||||
this.invoiceDate = invoiceDate;
|
||||
}
|
||||
|
||||
public LocalDate getDeliveryDate() {
|
||||
return deliveryDate;
|
||||
}
|
||||
|
||||
public void setDeliveryDate(LocalDate deliveryDate) {
|
||||
this.deliveryDate = deliveryDate;
|
||||
}
|
||||
|
||||
public String getSenderName() {
|
||||
return senderName;
|
||||
}
|
||||
|
||||
public void setSenderName(String senderName) {
|
||||
this.senderName = senderName;
|
||||
}
|
||||
|
||||
public String getSenderAddress() {
|
||||
return senderAddress;
|
||||
}
|
||||
|
||||
public void setSenderAddress(String senderAddress) {
|
||||
this.senderAddress = senderAddress;
|
||||
}
|
||||
|
||||
public String getSenderPostcode() {
|
||||
return senderPostcode;
|
||||
}
|
||||
|
||||
public void setSenderPostcode(String senderPostcode) {
|
||||
this.senderPostcode = senderPostcode;
|
||||
}
|
||||
|
||||
public String getSenderCity() {
|
||||
return senderCity;
|
||||
}
|
||||
|
||||
public void setSenderCity(String senderCity) {
|
||||
this.senderCity = senderCity;
|
||||
}
|
||||
|
||||
public String getSenderCountry() {
|
||||
return senderCountry;
|
||||
}
|
||||
|
||||
public void setSenderCountry(String senderCountry) {
|
||||
this.senderCountry = senderCountry;
|
||||
}
|
||||
|
||||
public String getSenderTaxNumber() {
|
||||
return senderTaxNumber;
|
||||
}
|
||||
|
||||
public void setSenderTaxNumber(String senderTaxNumber) {
|
||||
this.senderTaxNumber = senderTaxNumber;
|
||||
}
|
||||
|
||||
public String getSenderVatId() {
|
||||
return senderVatId;
|
||||
}
|
||||
|
||||
public void setSenderVatId(String senderVatId) {
|
||||
this.senderVatId = senderVatId;
|
||||
}
|
||||
|
||||
public String getSenderPhone() {
|
||||
return senderPhone;
|
||||
}
|
||||
|
||||
public void setSenderPhone(String senderPhone) {
|
||||
this.senderPhone = senderPhone;
|
||||
}
|
||||
|
||||
public String getSenderEmail() {
|
||||
return senderEmail;
|
||||
}
|
||||
|
||||
public void setSenderEmail(String senderEmail) {
|
||||
this.senderEmail = senderEmail;
|
||||
}
|
||||
|
||||
public String getSenderWebsite() {
|
||||
return senderWebsite;
|
||||
}
|
||||
|
||||
public void setSenderWebsite(String senderWebsite) {
|
||||
this.senderWebsite = senderWebsite;
|
||||
}
|
||||
|
||||
public String getRecipientName() {
|
||||
return recipientName;
|
||||
}
|
||||
|
||||
public void setRecipientName(String recipientName) {
|
||||
this.recipientName = recipientName;
|
||||
}
|
||||
|
||||
public String getRecipientCompany() {
|
||||
return recipientCompany;
|
||||
}
|
||||
|
||||
public void setRecipientCompany(String recipientCompany) {
|
||||
this.recipientCompany = recipientCompany;
|
||||
}
|
||||
|
||||
public String getRecipientAddress() {
|
||||
return recipientAddress;
|
||||
}
|
||||
|
||||
public void setRecipientAddress(String recipientAddress) {
|
||||
this.recipientAddress = recipientAddress;
|
||||
}
|
||||
|
||||
public String getRecipientPostcode() {
|
||||
return recipientPostcode;
|
||||
}
|
||||
|
||||
public void setRecipientPostcode(String recipientPostcode) {
|
||||
this.recipientPostcode = recipientPostcode;
|
||||
}
|
||||
|
||||
public String getRecipientCity() {
|
||||
return recipientCity;
|
||||
}
|
||||
|
||||
public void setRecipientCity(String recipientCity) {
|
||||
this.recipientCity = recipientCity;
|
||||
}
|
||||
|
||||
public String getRecipientCountry() {
|
||||
return recipientCountry;
|
||||
}
|
||||
|
||||
public void setRecipientCountry(String recipientCountry) {
|
||||
this.recipientCountry = recipientCountry;
|
||||
}
|
||||
|
||||
public String getRecipientVatId() {
|
||||
return recipientVatId;
|
||||
}
|
||||
|
||||
public void setRecipientVatId(String recipientVatId) {
|
||||
this.recipientVatId = recipientVatId;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public List<CustomerInvoiceItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void setItems(List<CustomerInvoiceItem> items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
public BigDecimal getNetAmount() {
|
||||
return netAmount;
|
||||
}
|
||||
|
||||
public void setNetAmount(BigDecimal netAmount) {
|
||||
this.netAmount = netAmount;
|
||||
}
|
||||
|
||||
public BigDecimal getVatRate() {
|
||||
return vatRate;
|
||||
}
|
||||
|
||||
public void setVatRate(BigDecimal vatRate) {
|
||||
this.vatRate = vatRate;
|
||||
}
|
||||
|
||||
public BigDecimal getVatAmount() {
|
||||
return vatAmount;
|
||||
}
|
||||
|
||||
public void setVatAmount(BigDecimal vatAmount) {
|
||||
this.vatAmount = vatAmount;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalAmount() {
|
||||
return totalAmount;
|
||||
}
|
||||
|
||||
public void setTotalAmount(BigDecimal totalAmount) {
|
||||
this.totalAmount = totalAmount;
|
||||
}
|
||||
|
||||
public String getPaymentTerms() {
|
||||
return paymentTerms;
|
||||
}
|
||||
|
||||
public void setPaymentTerms(String paymentTerms) {
|
||||
this.paymentTerms = paymentTerms;
|
||||
}
|
||||
|
||||
public LocalDate getPaymentDueDate() {
|
||||
return paymentDueDate;
|
||||
}
|
||||
|
||||
public void setPaymentDueDate(LocalDate paymentDueDate) {
|
||||
this.paymentDueDate = paymentDueDate;
|
||||
}
|
||||
|
||||
public String getBankAccount() {
|
||||
return bankAccount;
|
||||
}
|
||||
|
||||
public void setBankAccount(String bankAccount) {
|
||||
this.bankAccount = bankAccount;
|
||||
}
|
||||
|
||||
public String getIban() {
|
||||
return iban;
|
||||
}
|
||||
|
||||
public void setIban(String iban) {
|
||||
this.iban = iban;
|
||||
}
|
||||
|
||||
public String getBic() {
|
||||
return bic;
|
||||
}
|
||||
|
||||
public void setBic(String bic) {
|
||||
this.bic = bic;
|
||||
}
|
||||
|
||||
public String getLegalNotes() {
|
||||
return legalNotes;
|
||||
}
|
||||
|
||||
public void setLegalNotes(String legalNotes) {
|
||||
this.legalNotes = legalNotes;
|
||||
}
|
||||
|
||||
public String getReverseChargeNote() {
|
||||
return reverseChargeNote;
|
||||
}
|
||||
|
||||
public void setReverseChargeNote(String reverseChargeNote) {
|
||||
this.reverseChargeNote = reverseChargeNote;
|
||||
}
|
||||
|
||||
public String getJobId() {
|
||||
return jobId;
|
||||
}
|
||||
|
||||
public void setJobId(String jobId) {
|
||||
this.jobId = jobId;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public byte[] getPdfData() {
|
||||
return pdfData;
|
||||
}
|
||||
|
||||
public void setPdfData(byte[] pdfData) {
|
||||
this.pdfData = pdfData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
package de.assecutor.votianlt.model.invoices;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.math.BigDecimal;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class CustomerInvoiceData {
|
||||
|
||||
private String invoiceNumber;
|
||||
private LocalDate invoiceDate;
|
||||
private LocalDate deliveryDate;
|
||||
private String description;
|
||||
|
||||
// Rechnungssteller
|
||||
private String senderName;
|
||||
private String senderAddress;
|
||||
private String senderPostcode;
|
||||
private String senderCity;
|
||||
private String senderCountry;
|
||||
private String senderTaxNumber;
|
||||
private String senderVatId;
|
||||
private String senderPhone;
|
||||
private String senderEmail;
|
||||
private String senderWebsite;
|
||||
|
||||
// Rechnungsempfänger
|
||||
private String recipientName;
|
||||
private String recipientCompany;
|
||||
private String recipientAddress;
|
||||
private String recipientPostcode;
|
||||
private String recipientCity;
|
||||
private String recipientCountry;
|
||||
private String recipientVatId;
|
||||
|
||||
private List<CustomerInvoiceItem> items;
|
||||
|
||||
private BigDecimal netAmount;
|
||||
private BigDecimal vatRate;
|
||||
private BigDecimal vatAmount;
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
// Zahlungsdetails
|
||||
private String paymentTerms;
|
||||
private LocalDate paymentDueDate;
|
||||
private String bankAccount;
|
||||
private String iban;
|
||||
private String bic;
|
||||
|
||||
// Rechtliche Angaben
|
||||
private String legalNotes;
|
||||
private String reverseChargeNote;
|
||||
|
||||
// Number formatter for German locale
|
||||
private static final NumberFormat CURRENCY_FORMAT = NumberFormat.getCurrencyInstance(Locale.GERMANY);
|
||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy");
|
||||
|
||||
// Constructors
|
||||
public CustomerInvoiceData() {
|
||||
}
|
||||
|
||||
public CustomerInvoiceData(String invoiceNumber, LocalDate invoiceDate, String description) {
|
||||
this.invoiceNumber = invoiceNumber;
|
||||
this.invoiceDate = invoiceDate;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public String getInvoiceNumber() {
|
||||
return invoiceNumber;
|
||||
}
|
||||
|
||||
public void setInvoiceNumber(String invoiceNumber) {
|
||||
this.invoiceNumber = invoiceNumber;
|
||||
}
|
||||
|
||||
public LocalDate getInvoiceDate() {
|
||||
return invoiceDate;
|
||||
}
|
||||
|
||||
public void setInvoiceDate(LocalDate invoiceDate) {
|
||||
this.invoiceDate = invoiceDate;
|
||||
}
|
||||
|
||||
public LocalDate getDeliveryDate() {
|
||||
return deliveryDate;
|
||||
}
|
||||
|
||||
public void setDeliveryDate(LocalDate deliveryDate) {
|
||||
this.deliveryDate = deliveryDate;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getSenderName() {
|
||||
return senderName;
|
||||
}
|
||||
|
||||
public void setSenderName(String senderName) {
|
||||
this.senderName = senderName;
|
||||
}
|
||||
|
||||
public String getSenderAddress() {
|
||||
return senderAddress;
|
||||
}
|
||||
|
||||
public void setSenderAddress(String senderAddress) {
|
||||
this.senderAddress = senderAddress;
|
||||
}
|
||||
|
||||
public String getSenderPostcode() {
|
||||
return senderPostcode;
|
||||
}
|
||||
|
||||
public void setSenderPostcode(String senderPostcode) {
|
||||
this.senderPostcode = senderPostcode;
|
||||
}
|
||||
|
||||
public String getSenderCity() {
|
||||
return senderCity;
|
||||
}
|
||||
|
||||
public void setSenderCity(String senderCity) {
|
||||
this.senderCity = senderCity;
|
||||
}
|
||||
|
||||
public String getSenderCountry() {
|
||||
return senderCountry;
|
||||
}
|
||||
|
||||
public void setSenderCountry(String senderCountry) {
|
||||
this.senderCountry = senderCountry;
|
||||
}
|
||||
|
||||
public String getSenderTaxNumber() {
|
||||
return senderTaxNumber;
|
||||
}
|
||||
|
||||
public void setSenderTaxNumber(String senderTaxNumber) {
|
||||
this.senderTaxNumber = senderTaxNumber;
|
||||
}
|
||||
|
||||
public String getSenderVatId() {
|
||||
return senderVatId;
|
||||
}
|
||||
|
||||
public void setSenderVatId(String senderVatId) {
|
||||
this.senderVatId = senderVatId;
|
||||
}
|
||||
|
||||
public String getSenderPhone() {
|
||||
return senderPhone;
|
||||
}
|
||||
|
||||
public void setSenderPhone(String senderPhone) {
|
||||
this.senderPhone = senderPhone;
|
||||
}
|
||||
|
||||
public String getSenderEmail() {
|
||||
return senderEmail;
|
||||
}
|
||||
|
||||
public void setSenderEmail(String senderEmail) {
|
||||
this.senderEmail = senderEmail;
|
||||
}
|
||||
|
||||
public String getSenderWebsite() {
|
||||
return senderWebsite;
|
||||
}
|
||||
|
||||
public void setSenderWebsite(String senderWebsite) {
|
||||
this.senderWebsite = senderWebsite;
|
||||
}
|
||||
|
||||
public String getRecipientName() {
|
||||
return recipientName;
|
||||
}
|
||||
|
||||
public void setRecipientName(String recipientName) {
|
||||
this.recipientName = recipientName;
|
||||
}
|
||||
|
||||
public String getRecipientCompany() {
|
||||
return recipientCompany;
|
||||
}
|
||||
|
||||
public void setRecipientCompany(String recipientCompany) {
|
||||
this.recipientCompany = recipientCompany;
|
||||
}
|
||||
|
||||
public String getRecipientAddress() {
|
||||
return recipientAddress;
|
||||
}
|
||||
|
||||
public void setRecipientAddress(String recipientAddress) {
|
||||
this.recipientAddress = recipientAddress;
|
||||
}
|
||||
|
||||
public String getRecipientPostcode() {
|
||||
return recipientPostcode;
|
||||
}
|
||||
|
||||
public void setRecipientPostcode(String recipientPostcode) {
|
||||
this.recipientPostcode = recipientPostcode;
|
||||
}
|
||||
|
||||
public String getRecipientCity() {
|
||||
return recipientCity;
|
||||
}
|
||||
|
||||
public void setRecipientCity(String recipientCity) {
|
||||
this.recipientCity = recipientCity;
|
||||
}
|
||||
|
||||
public String getRecipientCountry() {
|
||||
return recipientCountry;
|
||||
}
|
||||
|
||||
public void setRecipientCountry(String recipientCountry) {
|
||||
this.recipientCountry = recipientCountry;
|
||||
}
|
||||
|
||||
public String getRecipientVatId() {
|
||||
return recipientVatId;
|
||||
}
|
||||
|
||||
public void setRecipientVatId(String recipientVatId) {
|
||||
this.recipientVatId = recipientVatId;
|
||||
}
|
||||
|
||||
public List<CustomerInvoiceItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void setItems(List<CustomerInvoiceItem> items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
public BigDecimal getNetAmount() {
|
||||
return netAmount;
|
||||
}
|
||||
|
||||
public void setNetAmount(BigDecimal netAmount) {
|
||||
this.netAmount = netAmount;
|
||||
}
|
||||
|
||||
public BigDecimal getVatRate() {
|
||||
return vatRate;
|
||||
}
|
||||
|
||||
public void setVatRate(BigDecimal vatRate) {
|
||||
this.vatRate = vatRate;
|
||||
}
|
||||
|
||||
public BigDecimal getVatAmount() {
|
||||
return vatAmount;
|
||||
}
|
||||
|
||||
public void setVatAmount(BigDecimal vatAmount) {
|
||||
this.vatAmount = vatAmount;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalAmount() {
|
||||
return totalAmount;
|
||||
}
|
||||
|
||||
public void setTotalAmount(BigDecimal totalAmount) {
|
||||
this.totalAmount = totalAmount;
|
||||
}
|
||||
|
||||
public String getPaymentTerms() {
|
||||
return paymentTerms;
|
||||
}
|
||||
|
||||
public void setPaymentTerms(String paymentTerms) {
|
||||
this.paymentTerms = paymentTerms;
|
||||
}
|
||||
|
||||
public LocalDate getPaymentDueDate() {
|
||||
return paymentDueDate;
|
||||
}
|
||||
|
||||
public void setPaymentDueDate(LocalDate paymentDueDate) {
|
||||
this.paymentDueDate = paymentDueDate;
|
||||
}
|
||||
|
||||
public String getBankAccount() {
|
||||
return bankAccount;
|
||||
}
|
||||
|
||||
public void setBankAccount(String bankAccount) {
|
||||
this.bankAccount = bankAccount;
|
||||
}
|
||||
|
||||
public String getIban() {
|
||||
return iban;
|
||||
}
|
||||
|
||||
public void setIban(String iban) {
|
||||
this.iban = iban;
|
||||
}
|
||||
|
||||
public String getBic() {
|
||||
return bic;
|
||||
}
|
||||
|
||||
public void setBic(String bic) {
|
||||
this.bic = bic;
|
||||
}
|
||||
|
||||
public String getLegalNotes() {
|
||||
return legalNotes;
|
||||
}
|
||||
|
||||
public void setLegalNotes(String legalNotes) {
|
||||
this.legalNotes = legalNotes;
|
||||
}
|
||||
|
||||
public String getReverseChargeNote() {
|
||||
return reverseChargeNote;
|
||||
}
|
||||
|
||||
public void setReverseChargeNote(String reverseChargeNote) {
|
||||
this.reverseChargeNote = reverseChargeNote;
|
||||
}
|
||||
|
||||
// Formatting methods for PDF generation
|
||||
public String getFormattedInvoiceDate() {
|
||||
return invoiceDate != null ? invoiceDate.format(DATE_FORMAT) : "";
|
||||
}
|
||||
|
||||
public String getFormattedDeliveryDate() {
|
||||
return deliveryDate != null ? deliveryDate.format(DATE_FORMAT) : "";
|
||||
}
|
||||
|
||||
public String getFormattedPaymentDueDate() {
|
||||
return paymentDueDate != null ? paymentDueDate.format(DATE_FORMAT) : "";
|
||||
}
|
||||
|
||||
public String getFormattedNetAmount() {
|
||||
return netAmount != null ? CURRENCY_FORMAT.format(netAmount) : "0,00 €";
|
||||
}
|
||||
|
||||
public String getFormattedVatAmount() {
|
||||
return vatAmount != null ? CURRENCY_FORMAT.format(vatAmount) : "0,00 €";
|
||||
}
|
||||
|
||||
public String getFormattedTotalAmount() {
|
||||
return totalAmount != null ? CURRENCY_FORMAT.format(totalAmount) : "0,00 €";
|
||||
}
|
||||
|
||||
public String getFormattedVatRate() {
|
||||
if (vatRate != null) {
|
||||
return String.format("%.0f%%", vatRate.multiply(new BigDecimal("100")));
|
||||
}
|
||||
return "19%";
|
||||
}
|
||||
|
||||
// Legacy methods for backward compatibility
|
||||
public String getDate() {
|
||||
return getFormattedInvoiceDate();
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getRecipientDepartment() {
|
||||
return recipientCompany;
|
||||
}
|
||||
|
||||
public String getRecipientStreet() {
|
||||
return recipientAddress;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user