Compare commits
10 Commits
d32b79d219
...
5f5d5995c5
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f5d5995c5 | |||
| d688bd8ce6 | |||
| e949d0c46a | |||
| b70c45b1d5 | |||
| 3d77a0bf64 | |||
| a2f6daed1c | |||
| 08ece158df | |||
| d8ee804019 | |||
| c2bc4e4900 | |||
| e532780e2a |
@@ -1,10 +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 target/*.jar app.jar
|
||||
COPY ${JAR_FILE} app.jar
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java", "-jar", "/app.jar", "--spring.profiles.active=production"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
docker buildx build --platform linux/amd64 -t appcreationgmbh/votianlt:0.8.0 --push .
|
||||
|
||||
docker buildx build --platform linux/amd64 -t repository.assecutor.de/votianlt:0.9.10 --push .
|
||||
docker buildx build --platform linux/amd64 -t registry.assecutor.de/votianlt:0.9.10 --push .
|
||||
|
||||
adsg
|
||||
G8m0T3vz
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
> Gilt für alle Views unter `src/main/java/de/assecutor/votianlt/pages/view/`
|
||||
> Theme-Datei: `src/main/frontend/themes/votian-modern/styles.css`
|
||||
> Stand: UI-Änderungen bis 23.03.2026 berücksichtigt (`Landing-Hero-CTA/Demo-Button`, `ViewToolbar`-Migrationen, Landing-/Dashboard-Updates, TabSheet-Dialoge, Message-/Statistics-Layouts)
|
||||
|
||||
---
|
||||
|
||||
@@ -159,8 +160,8 @@ Jede View muss **genau eine** der folgenden Root-Klassen tragen:
|
||||
| `statistics-chat-view` | Statistik/Chat | `VerticalLayout` |
|
||||
| `dashboard-view` | Dashboard-Seiten | `VerticalLayout` |
|
||||
| `dashboard-home-view` | Haupt-Dashboard (transparenter Hintergrund) | `VerticalLayout` |
|
||||
| `landing-view` | Startseite (unauthentifiziert, weißer Hintergrund) | `Main` |
|
||||
| `login-view` | Login-Seite (weißer Hintergrund, fullscreen) | `Main` |
|
||||
| `landing-view` | Startseite (Shell-Gradient im Body, weiße Inhalts-Panels) | `Main` |
|
||||
| `login-view` | Login-Seite (Shell-Gradient, einspaltig, fullscreen) | `Main` |
|
||||
|
||||
### Kritische Java-Regeln
|
||||
|
||||
@@ -243,6 +244,11 @@ Einheitliche Toolbar für alle Seiten-Überschriften:
|
||||
add(new ViewToolbar(getTranslation("my.view.title")));
|
||||
// Mit Aktionsbutton:
|
||||
add(new ViewToolbar(getTranslation("my.view.title"), myButton));
|
||||
// Mit gruppierten Sekundäraktionen:
|
||||
add(new ViewToolbar(
|
||||
getTranslation("my.view.title"),
|
||||
ViewToolbar.group(backButton, exportButton),
|
||||
addButton));
|
||||
```
|
||||
|
||||
```css
|
||||
@@ -251,13 +257,27 @@ add(new ViewToolbar(getTranslation("my.view.title"), myButton));
|
||||
padding: 0.75rem 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.view-toolbar-title-row {
|
||||
gap: 0.8rem;
|
||||
}
|
||||
.view-toolbar-title {
|
||||
color: var(--app-text-strong);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
.view-toolbar-actions,
|
||||
.view-toolbar-group {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
```
|
||||
|
||||
**Regeln**:
|
||||
- `ViewToolbar` ist Standard für neue Header und ersetzt ad-hoc `HorizontalLayout`+`H2`/`H1`-Konstrukte.
|
||||
- Bereits migriert: `EditProfileView`, `ImprintView`, `StatisticsView`, `UserMessagesView`, `MessageDetailsView`.
|
||||
- Titel im Toolbar-Header bleiben einzeilig (`white-space: nowrap` wird im Component-Code gesetzt).
|
||||
- Ausnahme: Für komplexe Header-Zeilen wie in `MessageDetailsView` darf der Inhalt einer `ViewToolbar` ersetzt werden, die Basiskomponente bleibt aber `ViewToolbar`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Daten-Grids
|
||||
@@ -289,6 +309,7 @@ add(panel);
|
||||
- Hintergrund: `rgba(255,255,255,0.88)`, `border-radius: 24px`
|
||||
- Header: Gedämpftes Grau, Uppercase, `font-weight: 800`, `font-size: 0.76rem`
|
||||
- Selektierte Zeile: `--app-accent-soft` Hintergrund
|
||||
- Auch eingebettete Grids in Formular-/Summary-Bereichen werden so gekapselt; kein nacktes `Grid` direkt in einen Content-Abschnitt hängen.
|
||||
|
||||
---
|
||||
|
||||
@@ -377,6 +398,17 @@ body:has(.dashboard-home-view) {
|
||||
}
|
||||
```
|
||||
|
||||
**Aktuelles Strukturmuster**:
|
||||
- Das eingeloggte Dashboard besteht aus Hero + Systemsektion; App-Overview und Footer bleiben auf der öffentlichen Landing Page.
|
||||
- Dashboard-Feature-Karten dürfen vollständig klickbar sein, indem `feature-card` in `RouterLink.feature-card-link` gewrappt wird.
|
||||
|
||||
```java
|
||||
RouterLink link = new RouterLink();
|
||||
link.setRoute(ShowJobsView.class);
|
||||
link.add(card);
|
||||
link.addClassName("feature-card-link");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Sidebar / Navigation
|
||||
@@ -406,8 +438,21 @@ header.addClickListener(event -> UI.getCurrent().navigate("dashboard"));
|
||||
transform: translateX(4px);
|
||||
background: rgba(255,255,255,0.14);
|
||||
}
|
||||
.app-nav-row-root {
|
||||
width: 20.5rem;
|
||||
}
|
||||
.app-nav-row-management-child {
|
||||
width: 18.25rem;
|
||||
}
|
||||
.app-nav-row-user-child {
|
||||
width: 17rem;
|
||||
}
|
||||
```
|
||||
|
||||
**Zusatzregeln**:
|
||||
- Root-Einträge und Kind-Einträge bekommen unterschiedliche Breitenklassen entsprechend ihrer Ebene im `TreeGrid`.
|
||||
- Drawer-Labels werden im Renderer mit `white-space: nowrap` versehen; Zeilenumbrüche in der Navigation sind nicht vorgesehen.
|
||||
|
||||
### User Menu
|
||||
|
||||
```css
|
||||
@@ -433,7 +478,7 @@ header.addClickListener(event -> UI.getCurrent().navigate("dashboard"));
|
||||
| `simple-card` | 26px | rgba(255,255,255,0.88) | Einfache Karten |
|
||||
| `detail-card` | 24px | rgba(255,255,255,0.88) | Detail-Informationen |
|
||||
| `hero-panel` | 34px | Dunkelblau-Gradient | Hero-Bereiche |
|
||||
| `feature-card` | 24px | Linear-Gradient weiß/blau | Feature-Kacheln |
|
||||
| `feature-card` | 24px | Linear-Gradient weiß/blau | Feature-Kacheln, optional klickbar via `feature-card-link` |
|
||||
| `message-card` | 22px | Linear-Gradient weiß | Nachrichten |
|
||||
| `station-tile` | 24px | Linear-Gradient weiß | Station-Kacheln |
|
||||
| `job-task-card` | 22px | Linear-Gradient weiß | Aufgaben-Karten |
|
||||
@@ -561,6 +606,49 @@ header.addClickListener(event -> UI.getCurrent().navigate("dashboard"));
|
||||
|
||||
## 17. Message & Chat Components
|
||||
|
||||
### Header & Thread Layout
|
||||
|
||||
Message-Views verwenden `ViewToolbar` als erstes sichtbares Header-Element. Eigene `.message-thread-header`-Layouts sollen für neue Implementierungen nicht mehr aufgebaut werden.
|
||||
|
||||
```java
|
||||
addClassName("message-hub-view");
|
||||
|
||||
VerticalLayout contentLayout = new VerticalLayout();
|
||||
contentLayout.setPadding(true);
|
||||
contentLayout.setSpacing(true);
|
||||
contentLayout.setWidthFull();
|
||||
contentLayout.setHeightFull();
|
||||
contentLayout.addClassName("message-thread-layout");
|
||||
|
||||
contentLayout.add(new ViewToolbar(getTranslation("usermessages.title.with", clientName)));
|
||||
```
|
||||
|
||||
```css
|
||||
.message-thread-layout {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.message-thread-scroller {
|
||||
border: 1px solid var(--app-border-strong);
|
||||
background: linear-gradient(180deg, rgba(249,250,251,0.95), rgba(243,244,246,0.88));
|
||||
border-radius: 24px;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
.message-thread {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
.message-thread-input {
|
||||
border: 1px solid var(--app-border-strong);
|
||||
background: rgba(255,255,255,0.82);
|
||||
border-radius: 22px;
|
||||
padding: 0.85rem 1rem;
|
||||
}
|
||||
```
|
||||
|
||||
### Message Card
|
||||
|
||||
```css
|
||||
@@ -588,8 +676,8 @@ header.addClickListener(event -> UI.getCurrent().navigate("dashboard"));
|
||||
box-shadow: var(--app-shadow-sm);
|
||||
}
|
||||
.message-bubble.client {
|
||||
background: rgba(255,255,255,0.96);
|
||||
border: 1px solid var(--app-border-strong);
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.98), rgba(241,245,249,0.92));
|
||||
border: 1px solid rgba(148,163,184,0.25);
|
||||
}
|
||||
.message-bubble.server {
|
||||
background: linear-gradient(135deg, rgba(191,219,254,0.8), rgba(167,243,208,0.74));
|
||||
@@ -619,7 +707,8 @@ header.addClickListener(event -> UI.getCurrent().navigate("dashboard"));
|
||||
## 18. Dialoge
|
||||
|
||||
```css
|
||||
vaadin-dialog-overlay::part(content) {
|
||||
vaadin-dialog-overlay::part(content),
|
||||
vaadin-confirm-dialog-overlay::part(content) {
|
||||
border-radius: 28px;
|
||||
border: 1px solid var(--app-border-strong);
|
||||
background: rgba(255,255,255,0.94);
|
||||
@@ -639,13 +728,76 @@ vaadin-dialog-overlay::part(content) {
|
||||
|
||||
```css
|
||||
.dialog-form-panel,
|
||||
.dialog-content-panel {
|
||||
.dialog-content-panel,
|
||||
.dialog-cargo-card {
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 1rem;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.dialog-task-card {
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
|
||||
}
|
||||
```
|
||||
|
||||
### Station Dialoge mit TabSheet und innerer Karte
|
||||
|
||||
`PickupStationDialog` und `DeliveryStationDialog` verwenden nicht mehr die Standard-Overlay-Karte, sondern ein transparentes Overlay mit eigener weißer Innenkarte.
|
||||
|
||||
```java
|
||||
getElement().setAttribute("theme", "no-inner-card");
|
||||
|
||||
TabSheet tabSheet = new TabSheet();
|
||||
tabSheet.setWidthFull();
|
||||
tabSheet.setSizeFull();
|
||||
|
||||
Div frame = new Div();
|
||||
frame.getStyle().set("border", "10px solid transparent");
|
||||
frame.getStyle().set("display", "flex");
|
||||
frame.getStyle().set("flex-direction", "column");
|
||||
frame.getStyle().set("flex", "1");
|
||||
frame.setSizeFull();
|
||||
|
||||
Div whiteCard = new Div();
|
||||
whiteCard.getStyle().set("background", "white");
|
||||
whiteCard.getStyle().set("border-radius", "24px");
|
||||
whiteCard.getStyle().set("flex", "1");
|
||||
whiteCard.getStyle().set("overflow", "auto");
|
||||
whiteCard.setSizeFull();
|
||||
whiteCard.add(tabSheet);
|
||||
|
||||
frame.add(whiteCard);
|
||||
add(frame);
|
||||
```
|
||||
|
||||
```css
|
||||
vaadin-dialog-overlay[theme~="no-inner-card"]::part(content) {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
backdrop-filter: none;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
vaadin-tabsheet::part(content) {
|
||||
border-radius: 0 0 24px 24px;
|
||||
background: rgba(255,255,255,0.86);
|
||||
}
|
||||
|
||||
[part="tabs-container"] {
|
||||
background: white;
|
||||
border-radius: 24px 24px 0 0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
```
|
||||
|
||||
**Tab-Konventionen**:
|
||||
- `PickupStationDialog`: Adresse, Termine, Fracht
|
||||
- `DeliveryStationDialog`: Adresse, Aufgaben
|
||||
- Fehlerindikatoren sitzen direkt am jeweiligen `Tab`
|
||||
|
||||
---
|
||||
|
||||
## 19. Detail Cards
|
||||
@@ -758,12 +910,24 @@ loginShell.addClassName("login-shell");
|
||||
.landing-view {
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
background: #ffffff !important;
|
||||
background: transparent !important;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
body:has(.landing-view) {
|
||||
background: #ffffff;
|
||||
background: var(--app-shell-background);
|
||||
}
|
||||
.landing-view .surface-panel {
|
||||
border: 1px solid var(--app-border-strong);
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.landing-view .landing-header,
|
||||
.landing-view .section-panel,
|
||||
.landing-view .app-overview-panel,
|
||||
.landing-view .footer-panel {
|
||||
background: var(--app-surface-solid);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -786,13 +950,58 @@ body:has(.landing-view) {
|
||||
}
|
||||
```
|
||||
|
||||
### Anonymer Header
|
||||
|
||||
- Im anonymen Zustand enthält die Header-Navigation nur `Login`, `Registrieren` und die Sprachwahl.
|
||||
- Öffentliche Primäraktionen gehören nicht in die Header-Leiste, sondern in das Hero-Panel.
|
||||
- Der `Demo`-Einstieg wird deshalb nicht als zusätzlicher Header-Button geführt.
|
||||
|
||||
```java
|
||||
private Component createAnonymousNavigation() {
|
||||
HorizontalLayout navButtons = new HorizontalLayout();
|
||||
navButtons.setSpacing(true);
|
||||
navButtons.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
navButtons.addClassName("landing-nav");
|
||||
|
||||
Button loginBtn = new Button(getTranslation("start.button.login"), event -> login());
|
||||
loginBtn.addThemeVariants(ButtonVariant.LUMO_CONTRAST);
|
||||
loginBtn.addClassName("landing-nav-button");
|
||||
|
||||
Button registerBtn = new Button(getTranslation("start.button.register"), event -> register());
|
||||
registerBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
registerBtn.addClassName("landing-nav-button");
|
||||
|
||||
Button languageBtn = createLanguageSelector();
|
||||
|
||||
navButtons.add(loginBtn, registerBtn, languageBtn);
|
||||
return navButtons;
|
||||
}
|
||||
```
|
||||
|
||||
### App-Section vs. Footer
|
||||
|
||||
CTA-Text und Slogan liegen auf der Landing Page jetzt im `app-overview-panel`, nicht mehr im Footer.
|
||||
|
||||
```css
|
||||
.app-cta {
|
||||
margin-top: 0.75rem;
|
||||
color: var(--app-accent-strong);
|
||||
font-weight: 700;
|
||||
max-width: 820px;
|
||||
}
|
||||
.app-slogan {
|
||||
color: var(--app-accent-strong);
|
||||
font-style: italic;
|
||||
}
|
||||
```
|
||||
|
||||
### Hero Panel
|
||||
|
||||
```css
|
||||
.hero-panel {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 340px;
|
||||
min-height: 520px;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
border-radius: 34px;
|
||||
@@ -810,6 +1019,68 @@ body:has(.landing-view) {
|
||||
color: rgba(226,232,240,0.92);
|
||||
font-size: clamp(1rem, 2vw, 1.15rem);
|
||||
}
|
||||
.hero-actions {
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
.hero-action-group {
|
||||
gap: 0.45rem;
|
||||
align-items: center;
|
||||
}
|
||||
.hero-choice-hint {
|
||||
max-width: 420px;
|
||||
margin: 0;
|
||||
color: rgba(226,232,240,0.82);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.55;
|
||||
text-align: center;
|
||||
}
|
||||
```
|
||||
|
||||
### Hero-CTA-Regeln
|
||||
|
||||
- Öffentliche CTAs werden in der Hero-Kachel vertikal gestapelt und nicht über Header und Hero verteilt.
|
||||
- Reihenfolge auf der Landing Page: zuerst `Demo`, danach `Jetzt kostenlos testen`.
|
||||
- Beide CTA-Buttons verwenden dieselbe Primärdarstellung: `LUMO_PRIMARY`, `LUMO_LARGE`, `setWidthFull()`, Pill-Optik.
|
||||
- Jeder CTA bekommt direkt darunter einen eigenen Erklärungstext.
|
||||
- Keine kombinierten Erklärungstexte für mehrere Buttons verwenden; Hinweise immer 1:1 an den jeweiligen CTA binden.
|
||||
|
||||
```java
|
||||
Button demoButton = new Button(getTranslation("start.button.demo"), event -> loginDemo());
|
||||
demoButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE);
|
||||
demoButton.addClassNames("hero-cta", "hero-demo-cta");
|
||||
demoButton.setWidthFull();
|
||||
|
||||
Button ctaButton = new Button(getTranslation("cta.freetest"), event -> register());
|
||||
ctaButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE);
|
||||
ctaButton.addClassName("hero-cta");
|
||||
ctaButton.setWidthFull();
|
||||
|
||||
Paragraph demoHint = new Paragraph(getTranslation("start.hero.demo.hint"));
|
||||
demoHint.addClassName("hero-choice-hint");
|
||||
|
||||
Paragraph trialHint = new Paragraph(getTranslation("start.hero.trial.hint"));
|
||||
trialHint.addClassName("hero-choice-hint");
|
||||
|
||||
VerticalLayout demoAction = new VerticalLayout(demoButton, demoHint);
|
||||
demoAction.addClassName("hero-action-group");
|
||||
|
||||
VerticalLayout trialAction = new VerticalLayout(ctaButton, trialHint);
|
||||
trialAction.addClassName("hero-action-group");
|
||||
|
||||
VerticalLayout heroActions = new VerticalLayout();
|
||||
heroActions.setPadding(false);
|
||||
heroActions.setSpacing(false);
|
||||
heroActions.setWidthFull();
|
||||
heroActions.setMaxWidth("420px");
|
||||
heroActions.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
heroActions.addClassName("hero-actions");
|
||||
heroActions.add(demoAction, trialAction);
|
||||
```
|
||||
|
||||
```properties
|
||||
start.hero.demo.hint=Demo startet sofort mit vorbereiteten Beispieldaten.
|
||||
start.hero.trial.hint="Jetzt kostenlos testen" erstellt Ihren eigenen Account für den kostenlosen Probemonat.
|
||||
```
|
||||
|
||||
---
|
||||
@@ -1136,3 +1407,4 @@ Beim Erstellen einer neuen View folgende Punkte prüfen:
|
||||
- [ ] Schmale Formular-Container haben `setWidth("Xpx")` **und** `setMaxWidth("100%")`
|
||||
- [ ] Inline-Styles nur für dynamische Werte verwenden, alles andere per CSS-Klassen
|
||||
- [ ] Sekundäre Buttons (`Abbrechen`, `Zurück`, `Export`) haben `LUMO_TERTIARY`, Hauptaktionen haben `LUMO_PRIMARY`
|
||||
- [ ] Tabbed Dialoge verwenden das `no-inner-card`-Pattern mit innerer weißer Karte statt zusätzlicher Overlay-Dekoration
|
||||
@@ -1,6 +1,79 @@
|
||||
# 1. Login (mit deinen Credentials)
|
||||
echo "G8m0T3vz" | docker login registry.assecutor.org -u adsg --password-stdin
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Dann ganz normal pushen
|
||||
docker buildx build --platform linux/amd64 -t registry.assecutor.org/votianlt:0.9.9 --push .
|
||||
set -euo pipefail
|
||||
|
||||
readonly SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly REGISTRY_IMAGE="registry.assecutor.org/votianlt"
|
||||
|
||||
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 "./mvnw" ]] || fail "'./mvnw' wurde nicht gefunden oder ist nicht ausführbar."
|
||||
|
||||
local version
|
||||
version="$(
|
||||
./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} ..."
|
||||
./mvnw -Pproduction -DskipTests -Drevision="${VERSION}" package
|
||||
|
||||
JAR_FILE="target/votianlt-${VERSION}.jar"
|
||||
[[ -f "${JAR_FILE}" ]] || fail "Release-JAR wurde nicht gefunden: ${JAR_FILE}"
|
||||
|
||||
echo "Pushe Image ${REGISTRY_IMAGE}:${VERSION} ..."
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg "JAR_FILE=${JAR_FILE}" \
|
||||
-t "${REGISTRY_IMAGE}:${VERSION}" \
|
||||
--push \
|
||||
.
|
||||
|
||||
echo "Fertig: ${REGISTRY_IMAGE}:${VERSION}"
|
||||
|
||||
3
pom.xml
3
pom.xml
@@ -6,11 +6,12 @@
|
||||
|
||||
<groupId>de.assecutor.votianlt</groupId>
|
||||
<artifactId>votianlt</artifactId>
|
||||
<version>0.9.12</version>
|
||||
<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>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -165,6 +165,7 @@ window.initProfileInvoiceGenerator = function() {
|
||||
var w = (el.width || 100) * zoomFactor;
|
||||
var h = (el.height || 30) * zoomFactor;
|
||||
var fontSize = (el.fontSize || 14) * zoomFactor;
|
||||
var textAlign = el.textAlign || 'left';
|
||||
|
||||
if (el.type === 'line') {
|
||||
ctx.strokeStyle = el.color || '#333333';
|
||||
@@ -269,9 +270,17 @@ window.initProfileInvoiceGenerator = function() {
|
||||
var fontWeight = (el.isStatic && !el.isCustomer) ? '' : (el.fontStyle || '');
|
||||
ctx.font = (fontWeight ? fontWeight + ' ' : '') + fontSize + 'px Arial';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.textAlign = textAlign;
|
||||
|
||||
var textX = x;
|
||||
if (textAlign === 'center') {
|
||||
textX = x + (w / 2);
|
||||
} else if (textAlign === 'right') {
|
||||
textX = x + w;
|
||||
}
|
||||
|
||||
lines.forEach(function(line) {
|
||||
ctx.fillText(line, x, ty);
|
||||
ctx.fillText(line, textX, ty);
|
||||
ty += lineHeight;
|
||||
});
|
||||
}
|
||||
@@ -1113,6 +1122,7 @@ window.initProfileInvoiceGenerator = function() {
|
||||
heightPercent: toPercentY(el.height),
|
||||
fontSize: el.fontSize,
|
||||
fontStyle: el.fontStyle,
|
||||
textAlign: el.textAlign,
|
||||
color: el.color,
|
||||
isStatic: el.isStatic,
|
||||
isCustomer: el.isCustomer,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
[part="tabs-container"] {
|
||||
background: white;
|
||||
border-radius: 24px 24px 0 0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
@@ -273,7 +273,6 @@ vaadin-vertical-layout.admin-form-view {
|
||||
.view-toolbar,
|
||||
.form-card,
|
||||
.message-section,
|
||||
.message-thread-layout,
|
||||
.statistics-header,
|
||||
.statistics-input-panel {
|
||||
padding: clamp(1rem, 1.8vw, 1.5rem);
|
||||
@@ -362,7 +361,7 @@ vaadin-vertical-layout.admin-form-view {
|
||||
.hero-panel {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 340px;
|
||||
min-height: 520px;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
border-radius: 34px;
|
||||
@@ -431,6 +430,30 @@ vaadin-vertical-layout.admin-form-view {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-action-group {
|
||||
gap: 0.45rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-actions .hero-cta,
|
||||
.hero-actions .hero-demo-cta {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.hero-choice-hint {
|
||||
max-width: 420px;
|
||||
margin: 0;
|
||||
color: rgba(226, 232, 240, 0.82);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.55;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
@@ -1220,8 +1243,8 @@ vaadin-grid-tree-toggle[expanded] .nav-expand-icon {
|
||||
border-radius: 22px !important;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(246, 249, 255, 0.9)) !important;
|
||||
box-shadow: var(--app-shadow-sm) !important;
|
||||
max-width: 100% !important;
|
||||
margin-bottom: 0.85rem !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.message-section {
|
||||
@@ -1265,12 +1288,12 @@ vaadin-grid-tree-toggle[expanded] .nav-expand-icon {
|
||||
}
|
||||
|
||||
.message-thread-layout {
|
||||
width: min(1240px, 100%);
|
||||
margin: 0 auto;
|
||||
border: 1px solid var(--app-border-strong);
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
box-shadow: var(--app-shadow-lg);
|
||||
border-radius: 30px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.message-thread-header {
|
||||
@@ -1281,10 +1304,14 @@ vaadin-grid-tree-toggle[expanded] .nav-expand-icon {
|
||||
padding: 1rem 1.15rem;
|
||||
}
|
||||
|
||||
.message-thread {
|
||||
.message-thread-scroller {
|
||||
border: 1px solid var(--app-border-strong);
|
||||
background: linear-gradient(180deg, rgba(239, 244, 255, 0.72), rgba(248, 250, 252, 0.92));
|
||||
background: linear-gradient(180deg, rgba(249, 250, 251, 0.95), rgba(243, 244, 246, 0.88));
|
||||
border-radius: 24px;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.message-thread {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
|
||||
@@ -1338,8 +1365,8 @@ vaadin-grid-tree-toggle[expanded] .nav-expand-icon {
|
||||
}
|
||||
|
||||
.message-bubble.client {
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border: 1px solid var(--app-border-strong);
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(241, 245, 249, 0.92));
|
||||
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||
}
|
||||
|
||||
.message-bubble.server {
|
||||
@@ -1381,7 +1408,14 @@ vaadin-grid-tree-toggle[expanded] .nav-expand-icon {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.statistics-header,
|
||||
.statistics-header {
|
||||
width: min(1240px, 100%);
|
||||
margin: 0 auto;
|
||||
border: none;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.statistics-input-panel {
|
||||
width: min(1240px, 100%);
|
||||
margin: 0 auto;
|
||||
@@ -1398,6 +1432,7 @@ vaadin-grid-tree-toggle[expanded] .nav-expand-icon {
|
||||
background: linear-gradient(180deg, rgba(238, 244, 255, 0.75), rgba(248, 250, 252, 0.96));
|
||||
border-radius: 30px;
|
||||
box-shadow: var(--app-shadow-lg);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.station-tile.validated {
|
||||
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
@@ -18,15 +19,19 @@ public class DataInitializer implements CommandLineRunner {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final DemoModeService demoModeService;
|
||||
|
||||
public DataInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder) {
|
||||
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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.confirmdialog.ConfirmDialog;
|
||||
import com.vaadin.flow.component.dialog.Dialog;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
@@ -211,12 +212,14 @@ public class DeliveryStationDialog extends Dialog {
|
||||
setWidth("960px");
|
||||
setHeight("80vh");
|
||||
|
||||
// Remove white rounded card from dialog overlay
|
||||
getElement().setAttribute("theme", "no-inner-card");
|
||||
|
||||
// Address form
|
||||
VerticalLayout formLayout = new VerticalLayout();
|
||||
formLayout.setPadding(true);
|
||||
formLayout.setSpacing(true);
|
||||
formLayout.setWidthFull();
|
||||
formLayout.addClassName("dialog-form-panel");
|
||||
|
||||
// Company with autocomplete
|
||||
company = new ComboBox<>(translationHelper.getTranslation("profile.company"));
|
||||
@@ -325,7 +328,27 @@ public class DeliveryStationDialog extends Dialog {
|
||||
createTasksTab(templates, templateSaveCallback));
|
||||
tasksTab.add(tasksTabError);
|
||||
|
||||
add(tabSheet);
|
||||
// Transparent border frame filling the dialog
|
||||
Div frame = new Div();
|
||||
frame.getStyle().set("border", "10px solid transparent");
|
||||
frame.getStyle().set("border-radius", "0");
|
||||
frame.getStyle().set("box-sizing", "border-box");
|
||||
frame.getStyle().set("display", "flex");
|
||||
frame.getStyle().set("flex-direction", "column");
|
||||
frame.getStyle().set("flex", "1");
|
||||
frame.setSizeFull();
|
||||
|
||||
// White card with rounded corners inside the frame
|
||||
Div whiteCard = new Div();
|
||||
whiteCard.getStyle().set("background", "white");
|
||||
whiteCard.getStyle().set("border-radius", "24px");
|
||||
whiteCard.getStyle().set("flex", "1");
|
||||
whiteCard.getStyle().set("overflow", "auto");
|
||||
whiteCard.setSizeFull();
|
||||
whiteCard.add(tabSheet);
|
||||
|
||||
frame.add(whiteCard);
|
||||
add(frame);
|
||||
|
||||
// Footer buttons
|
||||
Button saveButton = new Button(translationHelper.getTranslation("dialog.confirm"), e -> {
|
||||
@@ -336,20 +359,20 @@ public class DeliveryStationDialog extends Dialog {
|
||||
}
|
||||
|
||||
// Warte-Dialog anzeigen
|
||||
Dialog loadingDialog = new Dialog();
|
||||
Dialog loadingDialog = DialogStylingHelper
|
||||
.createStyledDialog(translationHelper.getTranslation("addjob.validation.dialog.title"), "420px");
|
||||
loadingDialog.setCloseOnOutsideClick(false);
|
||||
loadingDialog.setCloseOnEsc(false);
|
||||
loadingDialog.setHeaderTitle(translationHelper.getTranslation("addjob.validation.dialog.title"));
|
||||
VerticalLayout loadingContent = new VerticalLayout();
|
||||
VerticalLayout loadingContent = DialogStylingHelper.createContentLayout("320px",
|
||||
FlexComponent.Alignment.CENTER);
|
||||
loadingContent.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||
loadingContent.setPadding(true);
|
||||
loadingContent.setSpacing(true);
|
||||
loadingContent.getStyle().set("text-align", "center");
|
||||
Span loadingText = new Span(translationHelper.getTranslation("addjob.validation.dialog.loading"));
|
||||
ProgressBar progressBar = new ProgressBar();
|
||||
progressBar.setIndeterminate(true);
|
||||
progressBar.setWidthFull();
|
||||
loadingContent.add(loadingText, progressBar);
|
||||
loadingDialog.add(loadingContent);
|
||||
loadingDialog.add(DialogStylingHelper.wrapContent(loadingContent));
|
||||
loadingDialog.open();
|
||||
|
||||
// Adresse asynchron bei Google validieren
|
||||
@@ -695,7 +718,6 @@ public class DeliveryStationDialog extends Dialog {
|
||||
content.setPadding(false);
|
||||
content.setSpacing(true);
|
||||
content.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
|
||||
content.addClassName("dialog-content-panel");
|
||||
|
||||
// Task title with template selection
|
||||
H3 tasksTitle = new H3(translationHelper.getTranslation("addjob.tasks.title"));
|
||||
@@ -1252,13 +1274,11 @@ public class DeliveryStationDialog extends Dialog {
|
||||
return;
|
||||
}
|
||||
|
||||
Dialog dialog = new Dialog();
|
||||
dialog.setHeaderTitle(translationHelper.getTranslation("addjob.tasks.template.save.title"));
|
||||
dialog.setWidth("400px");
|
||||
Dialog dialog = DialogStylingHelper
|
||||
.createStyledDialog(translationHelper.getTranslation("addjob.tasks.template.save.title"), "460px");
|
||||
dialog.setCloseOnOutsideClick(false);
|
||||
|
||||
VerticalLayout dialogLayout = new VerticalLayout();
|
||||
dialogLayout.setPadding(false);
|
||||
dialogLayout.setSpacing(true);
|
||||
VerticalLayout dialogLayout = DialogStylingHelper.createContentLayout("340px");
|
||||
|
||||
TextField templateNameField = new TextField(translationHelper.getTranslation("addjob.tasks.template.name"));
|
||||
templateNameField.setPlaceholder(translationHelper.getTranslation("addjob.tasks.template.name.placeholder"));
|
||||
@@ -1288,11 +1308,9 @@ public class DeliveryStationDialog extends Dialog {
|
||||
Button cancelButton = new Button(translationHelper.getTranslation("button.cancel"));
|
||||
cancelButton.addClickListener(e -> dialog.close());
|
||||
|
||||
HorizontalLayout buttonLayout = new HorizontalLayout(cancelButton, saveButton);
|
||||
buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
|
||||
|
||||
dialogLayout.add(templateNameField, buttonLayout);
|
||||
dialog.add(dialogLayout);
|
||||
dialogLayout.add(templateNameField);
|
||||
dialog.add(DialogStylingHelper.wrapContent(dialogLayout));
|
||||
dialog.getFooter().add(cancelButton, saveButton);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package de.assecutor.votianlt.pages.base.ui.component;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.dialog.Dialog;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
|
||||
/**
|
||||
* Shared helper for dialogs that should use the station-dialog shell.
|
||||
*/
|
||||
public final class DialogStylingHelper {
|
||||
|
||||
private DialogStylingHelper() {
|
||||
}
|
||||
|
||||
public static Dialog createStyledDialog(String title, String width) {
|
||||
Dialog dialog = new Dialog();
|
||||
apply(dialog, title, width);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public static void apply(Dialog dialog, String title, String width) {
|
||||
if (title != null && !title.isBlank()) {
|
||||
dialog.setHeaderTitle(title);
|
||||
}
|
||||
if (width != null && !width.isBlank()) {
|
||||
dialog.setWidth(width);
|
||||
}
|
||||
dialog.setMaxWidth("95vw");
|
||||
dialog.getElement().setAttribute("theme", "no-inner-card");
|
||||
}
|
||||
|
||||
public static Component wrapContent(Component content) {
|
||||
Div frame = new Div();
|
||||
frame.getStyle().set("border", "10px solid transparent");
|
||||
frame.getStyle().set("border-radius", "0");
|
||||
frame.getStyle().set("box-sizing", "border-box");
|
||||
frame.setWidthFull();
|
||||
|
||||
Div whiteCard = new Div();
|
||||
whiteCard.getStyle().set("background", "white");
|
||||
whiteCard.getStyle().set("border-radius", "24px");
|
||||
whiteCard.getStyle().set("overflow", "auto");
|
||||
whiteCard.setWidthFull();
|
||||
whiteCard.add(content);
|
||||
|
||||
frame.add(whiteCard);
|
||||
return frame;
|
||||
}
|
||||
|
||||
public static VerticalLayout createContentLayout(String maxWidth) {
|
||||
return createContentLayout(maxWidth, FlexComponent.Alignment.STRETCH);
|
||||
}
|
||||
|
||||
public static VerticalLayout createContentLayout(String maxWidth, FlexComponent.Alignment alignment) {
|
||||
VerticalLayout content = new VerticalLayout();
|
||||
content.setPadding(true);
|
||||
content.setSpacing(true);
|
||||
content.setWidthFull();
|
||||
if (maxWidth != null && !maxWidth.isBlank()) {
|
||||
content.setMaxWidth(maxWidth);
|
||||
}
|
||||
content.setDefaultHorizontalComponentAlignment(alignment);
|
||||
content.getStyle().set("margin", "0 auto");
|
||||
return content;
|
||||
}
|
||||
}
|
||||
@@ -485,20 +485,20 @@ public class PickupStationDialog extends Dialog {
|
||||
}
|
||||
|
||||
// Warte-Dialog anzeigen
|
||||
Dialog loadingDialog = new Dialog();
|
||||
Dialog loadingDialog = DialogStylingHelper
|
||||
.createStyledDialog(translationHelper.getTranslation("addjob.validation.dialog.title"), "420px");
|
||||
loadingDialog.setCloseOnOutsideClick(false);
|
||||
loadingDialog.setCloseOnEsc(false);
|
||||
loadingDialog.setHeaderTitle(translationHelper.getTranslation("addjob.validation.dialog.title"));
|
||||
VerticalLayout loadingContent = new VerticalLayout();
|
||||
VerticalLayout loadingContent = DialogStylingHelper.createContentLayout("320px",
|
||||
FlexComponent.Alignment.CENTER);
|
||||
loadingContent.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||
loadingContent.setPadding(true);
|
||||
loadingContent.setSpacing(true);
|
||||
loadingContent.getStyle().set("text-align", "center");
|
||||
Span loadingText = new Span(translationHelper.getTranslation("addjob.validation.dialog.loading"));
|
||||
ProgressBar progressBar = new ProgressBar();
|
||||
progressBar.setIndeterminate(true);
|
||||
progressBar.setWidthFull();
|
||||
loadingContent.add(loadingText, progressBar);
|
||||
loadingDialog.add(loadingContent);
|
||||
loadingDialog.add(DialogStylingHelper.wrapContent(loadingContent));
|
||||
loadingDialog.open();
|
||||
|
||||
// Adresse asynchron bei Google validieren
|
||||
|
||||
@@ -28,11 +28,13 @@ public final class ViewToolbar extends Composite<Header> {
|
||||
var title = new H1(viewTitle);
|
||||
title.addClassNames(FontSize.XLARGE, Margin.NONE, FontWeight.LIGHT);
|
||||
title.addClassName("view-toolbar-title");
|
||||
title.getStyle().set("white-space", "nowrap");
|
||||
toggleAndTitle = new Div(drawerToggle, title);
|
||||
} else {
|
||||
var title = new H1(viewTitle);
|
||||
title.addClassNames(FontSize.XLARGE, Margin.NONE, FontWeight.LIGHT);
|
||||
title.addClassName("view-toolbar-title");
|
||||
title.getStyle().set("white-space", "nowrap");
|
||||
toggleAndTitle = new Div(title);
|
||||
}
|
||||
toggleAndTitle.addClassNames(Display.FLEX, AlignItems.CENTER);
|
||||
|
||||
@@ -198,6 +198,7 @@ public final class MainLayout extends AppLayout {
|
||||
// Label
|
||||
Span label = new Span(item.label());
|
||||
label.getStyle().set("font-size", "var(--lumo-font-size-s)");
|
||||
label.getStyle().set("white-space", "nowrap");
|
||||
label.addClassName("app-nav-label");
|
||||
row.add(label);
|
||||
row.setFlexGrow(1, label);
|
||||
|
||||
@@ -66,6 +66,7 @@ import de.assecutor.votianlt.pages.base.ui.component.DeliveryStationTile;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.StationTile;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.PickupStationDialog;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.DeliveryStationDialog;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
@@ -1185,13 +1186,10 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
}
|
||||
|
||||
private void openAddServiceDialog() {
|
||||
Dialog dialog = new Dialog();
|
||||
dialog.setHeaderTitle(getTranslation("addjob.services.dialog.title"));
|
||||
dialog.setWidth("560px");
|
||||
Dialog dialog = DialogStylingHelper.createStyledDialog(getTranslation("addjob.services.dialog.title"), "720px");
|
||||
dialog.setCloseOnOutsideClick(false);
|
||||
|
||||
VerticalLayout dialogContent = new VerticalLayout();
|
||||
dialogContent.setPadding(true);
|
||||
dialogContent.setSpacing(true);
|
||||
VerticalLayout dialogContent = DialogStylingHelper.createContentLayout("620px");
|
||||
|
||||
// Load available services for current user
|
||||
List<Service> availableServices = serviceRepository
|
||||
@@ -1224,11 +1222,6 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
|
||||
dialogContent.add(serviceCombo, deliveryStationCombo);
|
||||
|
||||
HorizontalLayout buttonLayout = new HorizontalLayout();
|
||||
buttonLayout.setWidthFull();
|
||||
buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
|
||||
buttonLayout.setSpacing(true);
|
||||
|
||||
Button cancelButton = new Button(getTranslation("button.cancel"), e -> dialog.close());
|
||||
cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
|
||||
@@ -1245,10 +1238,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
});
|
||||
addButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
buttonLayout.add(cancelButton, addButton);
|
||||
dialogContent.add(buttonLayout);
|
||||
|
||||
dialog.add(dialogContent);
|
||||
dialog.add(DialogStylingHelper.wrapContent(dialogContent));
|
||||
dialog.getFooter().add(cancelButton, addButton);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
@@ -2275,15 +2266,13 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
}
|
||||
|
||||
private Dialog createRouteLoadingDialog() {
|
||||
Dialog dialog = new Dialog();
|
||||
Dialog dialog = DialogStylingHelper.createStyledDialog(getTranslation("addjob.route.title"), "460px");
|
||||
dialog.setCloseOnOutsideClick(false);
|
||||
dialog.setCloseOnEsc(false);
|
||||
dialog.setHeaderTitle(getTranslation("addjob.route.title"));
|
||||
|
||||
VerticalLayout content = new VerticalLayout();
|
||||
VerticalLayout content = DialogStylingHelper.createContentLayout("340px", FlexComponent.Alignment.CENTER);
|
||||
content.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||
content.setPadding(true);
|
||||
content.setSpacing(true);
|
||||
content.getStyle().set("text-align", "center");
|
||||
|
||||
Span loadingText = new Span("Strecke zwischen allen Stationen wird berechnet...");
|
||||
ProgressBar progressBar = new ProgressBar();
|
||||
@@ -2291,7 +2280,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
progressBar.setWidthFull();
|
||||
|
||||
content.add(loadingText, progressBar);
|
||||
dialog.add(content);
|
||||
dialog.add(DialogStylingHelper.wrapContent(content));
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@@ -2316,13 +2305,9 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
}
|
||||
|
||||
private void showRouteSummaryDialog(RouteCalculationResult routeResult) {
|
||||
Dialog dialog = new Dialog();
|
||||
dialog.setHeaderTitle(getTranslation("addjob.route.title"));
|
||||
dialog.setWidth("420px");
|
||||
Dialog dialog = DialogStylingHelper.createStyledDialog(getTranslation("addjob.route.title"), "480px");
|
||||
|
||||
VerticalLayout content = new VerticalLayout();
|
||||
content.setPadding(false);
|
||||
content.setSpacing(true);
|
||||
VerticalLayout content = DialogStylingHelper.createContentLayout("360px");
|
||||
content.add(createRouteSummaryRow(getTranslation("addjob.route.distance"), routeResult.getFormattedDistance()));
|
||||
content.add(
|
||||
createRouteSummaryRow(getTranslation("addjob.route.duration"), routeResult.getFormattedDurationLong()));
|
||||
@@ -2330,7 +2315,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
Button closeButton = new Button(getTranslation("dialog.confirm"), event -> dialog.close());
|
||||
closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
dialog.add(content);
|
||||
dialog.add(DialogStylingHelper.wrapContent(content));
|
||||
dialog.getFooter().add(closeButton);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.dialog.Dialog;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
@@ -17,6 +18,7 @@ import com.vaadin.flow.router.HasDynamicTitle;
|
||||
import com.vaadin.flow.router.HasUrlParameter;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.assecutor.votianlt.model.Customer;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper;
|
||||
import de.assecutor.votianlt.pages.service.CustomerService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import org.bson.types.ObjectId;
|
||||
@@ -180,10 +182,13 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
|
||||
|
||||
private void deleteCustomer() {
|
||||
// Show confirmation dialog
|
||||
Dialog confirmDialog = new Dialog();
|
||||
confirmDialog.add(getTranslation("editcustomer.dialog.delete.text"));
|
||||
Dialog confirmDialog = DialogStylingHelper
|
||||
.createStyledDialog(getTranslation("editcustomer.dialog.delete.confirm"), "460px");
|
||||
confirmDialog.setCloseOnOutsideClick(false);
|
||||
|
||||
VerticalLayout dialogContent = DialogStylingHelper.createContentLayout("320px");
|
||||
dialogContent.add(new Span(getTranslation("editcustomer.dialog.delete.text")));
|
||||
|
||||
HorizontalLayout buttonLayout = new HorizontalLayout();
|
||||
Button confirmDeleteButton = new Button(getTranslation("editcustomer.dialog.delete.confirm"), e -> {
|
||||
if (customer != null && customer.getId() != null) {
|
||||
Notification.show(getTranslation("editcustomer.notification.deleted"), 3000,
|
||||
@@ -197,9 +202,8 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
|
||||
Button cancelDeleteButton = new Button(getTranslation("button.cancel"), e -> confirmDialog.close());
|
||||
cancelDeleteButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
|
||||
buttonLayout.add(confirmDeleteButton, cancelDeleteButton);
|
||||
buttonLayout.setSpacing(true);
|
||||
confirmDialog.add(buttonLayout);
|
||||
confirmDialog.add(DialogStylingHelper.wrapContent(dialogContent));
|
||||
confirmDialog.getFooter().add(cancelDeleteButton, confirmDeleteButton);
|
||||
|
||||
confirmDialog.open();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.assecutor.votianlt.pages.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
@@ -52,6 +53,7 @@ import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.dependency.JsModule;
|
||||
import com.vaadin.flow.component.ClientCallable;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper;
|
||||
|
||||
@Route(value = "edit-profile", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||
@RolesAllowed({ "USER", "ADMIN" })
|
||||
@@ -111,6 +113,10 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
formColumn.setHeightFull();
|
||||
formColumn.setPadding(false);
|
||||
formColumn.setSpacing(false);
|
||||
|
||||
ViewToolbar toolbar = new ViewToolbar(getTranslation("page.title.profile.edit"));
|
||||
formColumn.add(toolbar);
|
||||
|
||||
// TabSheet
|
||||
TabSheet tabSheet = new TabSheet();
|
||||
tabSheet.setSizeFull();
|
||||
@@ -1566,16 +1572,14 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
* Open dialog for adding/editing a service
|
||||
*/
|
||||
private void openServiceDialog(Service service) {
|
||||
Dialog dialog = new Dialog();
|
||||
dialog.setHeaderTitle(service == null ? getTranslation("profile.services.dialog.create")
|
||||
: getTranslation("profile.services.dialog.edit"));
|
||||
dialog.setWidth("500px");
|
||||
Dialog dialog = DialogStylingHelper.createStyledDialog(
|
||||
service == null ? getTranslation("profile.services.dialog.create")
|
||||
: getTranslation("profile.services.dialog.edit"),
|
||||
"560px");
|
||||
dialog.setCloseOnOutsideClick(false);
|
||||
|
||||
// Form layout
|
||||
VerticalLayout formLayout = new VerticalLayout();
|
||||
formLayout.setPadding(true);
|
||||
formLayout.setSpacing(true);
|
||||
formLayout.setWidthFull();
|
||||
VerticalLayout formLayout = DialogStylingHelper.createContentLayout("420px");
|
||||
|
||||
// Name field
|
||||
TextField nameField = new TextField(getTranslation("common.name"));
|
||||
@@ -1680,11 +1684,6 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
mandatoryCheckbox);
|
||||
|
||||
// Action buttons
|
||||
HorizontalLayout buttonLayout = new HorizontalLayout();
|
||||
buttonLayout.setWidthFull();
|
||||
buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
|
||||
buttonLayout.setSpacing(true);
|
||||
|
||||
Button cancelButton = new Button(getTranslation("button.cancel"), e -> dialog.close());
|
||||
cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
|
||||
@@ -1711,10 +1710,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
});
|
||||
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
buttonLayout.add(cancelButton, saveButton);
|
||||
formLayout.add(buttonLayout);
|
||||
|
||||
dialog.add(formLayout);
|
||||
dialog.add(DialogStylingHelper.wrapContent(formLayout));
|
||||
dialog.getFooter().add(cancelButton, saveButton);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.assecutor.votianlt.pages.view;
|
||||
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import com.vaadin.flow.router.HasDynamicTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
@@ -13,9 +14,16 @@ public class ImprintView extends VerticalLayout implements HasDynamicTitle {
|
||||
public ImprintView() {
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
setAlignItems(Alignment.CENTER);
|
||||
addClassNames("data-view", "form-shell");
|
||||
setSpacing(false);
|
||||
addClassName("form-page");
|
||||
|
||||
VerticalLayout content = new VerticalLayout();
|
||||
content.setWidthFull();
|
||||
content.setPadding(false);
|
||||
content.setSpacing(false);
|
||||
|
||||
ViewToolbar toolbar = new ViewToolbar(getTranslation("page.title.imprint"));
|
||||
content.add(toolbar);
|
||||
|
||||
try {
|
||||
// Load HTML content from resources
|
||||
@@ -27,15 +35,17 @@ public class ImprintView extends VerticalLayout implements HasDynamicTitle {
|
||||
imprintDiv.addClassNames("form-card", "form-shell");
|
||||
imprintDiv.getElement().setProperty("innerHTML", htmlContent);
|
||||
|
||||
add(imprintDiv);
|
||||
content.add(imprintDiv);
|
||||
|
||||
} catch (Exception e) {
|
||||
// Fallback content in case of error
|
||||
Div errorDiv = new Div();
|
||||
errorDiv.addClassNames("form-card", "form-shell");
|
||||
errorDiv.setText(getTranslation("imprint.error", e.getMessage()));
|
||||
add(errorDiv);
|
||||
content.add(errorDiv);
|
||||
}
|
||||
|
||||
add(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.vaadin.flow.component.upload.Upload;
|
||||
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
|
||||
import com.vaadin.flow.router.HasDynamicTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper;
|
||||
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
|
||||
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
@@ -490,12 +491,11 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
|
||||
colorPreviewLayout.add(colorPreview, colorHexLabel);
|
||||
|
||||
// Color Picker Dialog
|
||||
Dialog colorDialog = new Dialog();
|
||||
colorDialog.setHeaderTitle(getTranslation("invoicegenerator.color.dialog.title"));
|
||||
Dialog colorDialog = DialogStylingHelper
|
||||
.createStyledDialog(getTranslation("invoicegenerator.color.dialog.title"), "460px");
|
||||
colorDialog.setCloseOnOutsideClick(false);
|
||||
|
||||
VerticalLayout dialogLayout = new VerticalLayout();
|
||||
dialogLayout.setSpacing(true);
|
||||
dialogLayout.setPadding(true);
|
||||
VerticalLayout dialogLayout = DialogStylingHelper.createContentLayout("320px");
|
||||
|
||||
// Color Picker im Dialog
|
||||
Input dialogColorPicker = new Input();
|
||||
@@ -521,7 +521,7 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
|
||||
});
|
||||
|
||||
dialogLayout.add(dialogColorPicker, dialogHexField);
|
||||
colorDialog.add(dialogLayout);
|
||||
colorDialog.add(DialogStylingHelper.wrapContent(dialogLayout));
|
||||
|
||||
// Dialog Buttons
|
||||
Button dialogCancelButton = new Button(getTranslation("invoicegenerator.button.cancel"), e -> {
|
||||
|
||||
@@ -37,6 +37,7 @@ import de.assecutor.votianlt.model.task.BarcodeTask;
|
||||
import de.assecutor.votianlt.model.task.CommentTask;
|
||||
import de.assecutor.votianlt.model.AppUser;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.StationTile;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import de.assecutor.votianlt.repository.CargoItemRepository;
|
||||
@@ -521,7 +522,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
|
||||
private List<String> buildDeliverySummaryDetails(List<BaseTask> tasks) {
|
||||
if (tasks == null || tasks.isEmpty()) {
|
||||
return List.of(getTranslation("addjob.tab.tasks") + ": " + getTranslation("jobsummary.tasks.none"));
|
||||
return new ArrayList<>(
|
||||
List.of(getTranslation("addjob.tab.tasks") + ": " + getTranslation("jobsummary.tasks.none")));
|
||||
}
|
||||
|
||||
List<String> summaries = new ArrayList<>();
|
||||
@@ -532,7 +534,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
}
|
||||
|
||||
if (summaries.isEmpty()) {
|
||||
return List.of(getTranslation("addjob.tab.tasks") + ": " + getTranslation("jobsummary.tasks.none"));
|
||||
return new ArrayList<>(
|
||||
List.of(getTranslation("addjob.tab.tasks") + ": " + getTranslation("jobsummary.tasks.none")));
|
||||
}
|
||||
return summaries;
|
||||
}
|
||||
@@ -650,7 +653,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
return sortVisibleTasks(station.getTasks());
|
||||
}
|
||||
|
||||
return List.of();
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
private List<BaseTask> sortVisibleTasks(List<BaseTask> tasks) {
|
||||
@@ -666,9 +669,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
}
|
||||
|
||||
private void showStationTasksDialog(String stationTitle, List<BaseTask> tasks) {
|
||||
Dialog dialog = new Dialog();
|
||||
dialog.setWidth("720px");
|
||||
dialog.setMaxWidth("95vw");
|
||||
Dialog dialog = DialogStylingHelper.createStyledDialog(stationTitle, "720px");
|
||||
dialog.setMaxHeight("85vh");
|
||||
dialog.setResizable(true);
|
||||
dialog.setHeaderTitle(stationTitle);
|
||||
@@ -676,10 +677,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
|
||||
taskCards.clear();
|
||||
|
||||
VerticalLayout dialogContent = new VerticalLayout();
|
||||
dialogContent.setPadding(true);
|
||||
dialogContent.setSpacing(true);
|
||||
dialogContent.setWidthFull();
|
||||
VerticalLayout dialogContent = DialogStylingHelper.createContentLayout("620px");
|
||||
dialogContent.getStyle().set("min-width", "0");
|
||||
dialogContent.addClassName("dialog-content-panel");
|
||||
|
||||
@@ -708,7 +706,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
dialog.getFooter().add(closeButton);
|
||||
|
||||
dialog.add(dialogContent);
|
||||
dialog.add(DialogStylingHelper.wrapContent(dialogContent));
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
@@ -1093,23 +1091,16 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
}
|
||||
|
||||
private void showTaskDetailsDialog(BaseTask task) {
|
||||
Dialog dialog = new Dialog();
|
||||
dialog.setWidth("500px");
|
||||
Dialog dialog = DialogStylingHelper.createStyledDialog("Aufgaben-Details", "560px");
|
||||
dialog.setResizable(true);
|
||||
dialog.setDraggable(true);
|
||||
|
||||
// Reset all task card hover states when dialog closes
|
||||
dialog.addDialogCloseActionListener(e -> resetAllTaskCardHoverStates());
|
||||
|
||||
VerticalLayout dialogContent = new VerticalLayout();
|
||||
dialogContent.setPadding(true);
|
||||
dialogContent.setSpacing(true);
|
||||
VerticalLayout dialogContent = DialogStylingHelper.createContentLayout("420px");
|
||||
dialogContent.addClassName("dialog-content-panel");
|
||||
|
||||
// Header
|
||||
H4 header = new H4("Aufgaben-Details");
|
||||
dialogContent.add(header);
|
||||
|
||||
// Task type and status
|
||||
Span typeSpan = new Span("Typ: " + task.getDisplayName());
|
||||
typeSpan.getStyle().set("font-weight", "bold");
|
||||
@@ -1144,11 +1135,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
});
|
||||
closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
HorizontalLayout buttonLayout = new HorizontalLayout(closeButton);
|
||||
buttonLayout.setJustifyContentMode(HorizontalLayout.JustifyContentMode.END);
|
||||
dialogContent.add(buttonLayout);
|
||||
|
||||
dialog.add(dialogContent);
|
||||
dialog.add(DialogStylingHelper.wrapContent(dialogContent));
|
||||
dialog.getFooter().add(closeButton);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,13 +21,12 @@ import com.vaadin.flow.server.auth.AnonymousAllowed;
|
||||
import de.assecutor.votianlt.security.totp.TwoFactorService;
|
||||
import de.assecutor.votianlt.repository.UserRepository;
|
||||
import de.assecutor.votianlt.model.User;
|
||||
import de.assecutor.votianlt.security.SessionAuthenticationService;
|
||||
import de.assecutor.votianlt.service.DemoModeService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import com.vaadin.flow.server.VaadinSession;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.env.Environment;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
@@ -53,6 +52,9 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private SessionAuthenticationService sessionAuthenticationService;
|
||||
|
||||
@Value("${app.security.two-factor.enabled:false}")
|
||||
private boolean twoFactorEnabledGlobal;
|
||||
|
||||
@@ -140,6 +142,15 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
|
||||
}
|
||||
|
||||
private void handlePasswordLogin(String username, String password) {
|
||||
hideInlineFlash();
|
||||
|
||||
if (DemoModeService.isDemoUsername(username)) {
|
||||
pendingAuth = null;
|
||||
loginForm.setError(true);
|
||||
showInlineFlash(getTranslation("login.demo.only.button"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Prüfe Benutzername/Passwort
|
||||
Authentication auth = authenticationManager
|
||||
@@ -158,13 +169,7 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
|
||||
Notification.show(getTranslation("login.2fa.sent"), 3000, Notification.Position.BOTTOM_CENTER);
|
||||
} else {
|
||||
// 2FA deaktiviert: Direkt anmelden
|
||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
var vaadinSession = VaadinSession.getCurrent();
|
||||
if (vaadinSession != null) {
|
||||
var wrappedSession = vaadinSession.getSession();
|
||||
wrappedSession.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
|
||||
SecurityContextHolder.getContext());
|
||||
}
|
||||
sessionAuthenticationService.storeAuthentication(auth);
|
||||
// Check if user is admin and redirect accordingly
|
||||
if (auth.getAuthorities().stream()
|
||||
.anyMatch(authority -> authority.getAuthority().equals("ROLE_ADMIN"))) {
|
||||
@@ -196,19 +201,12 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
|
||||
return;
|
||||
}
|
||||
// 2FA korrekt: Benutzer nun anmelden
|
||||
SecurityContextHolder.getContext().setAuthentication(pendingAuth);
|
||||
// Persistiere SecurityContext in der HTTP-Session, damit Vaadin/Security ihn in
|
||||
// neuen Requests sieht
|
||||
var vaadinSession = VaadinSession.getCurrent();
|
||||
if (vaadinSession != null) {
|
||||
var wrappedSession = vaadinSession.getSession();
|
||||
wrappedSession.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
|
||||
SecurityContextHolder.getContext());
|
||||
}
|
||||
Authentication authenticatedUser = pendingAuth;
|
||||
sessionAuthenticationService.storeAuthentication(authenticatedUser);
|
||||
this.pendingAuth = null;
|
||||
// Full reload, damit der neue SecurityContext im UI sicher greift
|
||||
// Check if user is admin and redirect accordingly
|
||||
if (SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream()
|
||||
if (authenticatedUser.getAuthorities().stream()
|
||||
.anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"))) {
|
||||
UI.getCurrent().getPage().setLocation("/admin-dashboard");
|
||||
} else {
|
||||
@@ -246,4 +244,14 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
|
||||
public String getPageTitle() {
|
||||
return getTranslation("page.title.login");
|
||||
}
|
||||
|
||||
private void showInlineFlash(String message) {
|
||||
flashBox.setText(message);
|
||||
flashBox.getStyle().set("display", "block");
|
||||
}
|
||||
|
||||
private void hideInlineFlash() {
|
||||
flashBox.setText("");
|
||||
flashBox.getStyle().set("display", "none");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ import com.vaadin.flow.component.UI;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.dialog.Dialog;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.html.H1;
|
||||
import com.vaadin.flow.component.html.Image;
|
||||
import com.vaadin.flow.component.html.Main;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
@@ -160,7 +162,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
|
||||
|
||||
String conversationTitle = resolveConversationTitle(filteredMessages, conversationId);
|
||||
|
||||
HorizontalLayout headerLayout = createHeaderLayout(clientName, conversationTitle);
|
||||
ViewToolbar headerLayout = createHeaderLayout(clientName, conversationTitle);
|
||||
contentLayout.add(headerLayout);
|
||||
|
||||
// Store current messages for rendering and future updates
|
||||
@@ -182,6 +184,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
|
||||
messagesScroller.setHeightFull();
|
||||
messagesScroller.setScrollDirection(Scroller.ScrollDirection.VERTICAL);
|
||||
messagesScroller.getStyle().set("flex", "1 1 auto");
|
||||
messagesScroller.addClassName("message-thread-scroller");
|
||||
|
||||
contentLayout.add(messagesScroller);
|
||||
contentLayout.setFlexGrow(1, messagesScroller);
|
||||
@@ -260,11 +263,9 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
|
||||
}
|
||||
|
||||
private void openImageUploadDialog() {
|
||||
Dialog dialog = new Dialog();
|
||||
dialog.setHeaderTitle("Bild anhängen");
|
||||
Dialog dialog = DialogStylingHelper.createStyledDialog("Bild anhängen", "560px");
|
||||
dialog.setCloseOnEsc(true);
|
||||
dialog.setCloseOnOutsideClick(true);
|
||||
dialog.setWidth("480px");
|
||||
|
||||
MemoryBuffer buffer = new MemoryBuffer();
|
||||
Upload upload = new Upload(buffer);
|
||||
@@ -368,19 +369,11 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
|
||||
sendMessageToParticipant(base64, MessageContentType.IMAGE);
|
||||
});
|
||||
|
||||
VerticalLayout dialogContent = new VerticalLayout(upload, helper, previewWrapper);
|
||||
dialogContent.setSpacing(true);
|
||||
dialogContent.setPadding(false);
|
||||
dialogContent.setAlignItems(FlexComponent.Alignment.STRETCH);
|
||||
dialogContent.setWidthFull();
|
||||
VerticalLayout dialogContent = DialogStylingHelper.createContentLayout("440px");
|
||||
dialogContent.add(upload, helper, previewWrapper);
|
||||
|
||||
HorizontalLayout buttonBar = new HorizontalLayout(cancelButton, confirmButton);
|
||||
buttonBar.setWidthFull();
|
||||
buttonBar.setSpacing(true);
|
||||
buttonBar.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
|
||||
|
||||
dialog.add(dialogContent);
|
||||
dialog.getFooter().add(buttonBar);
|
||||
dialog.add(DialogStylingHelper.wrapContent(dialogContent));
|
||||
dialog.getFooter().add(cancelButton, confirmButton);
|
||||
|
||||
dialog.open();
|
||||
}
|
||||
@@ -668,30 +661,34 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
|
||||
}
|
||||
}
|
||||
|
||||
private HorizontalLayout createHeaderLayout(String clientName, String conversationTitle) {
|
||||
Button backButton = new Button("Zurück", VaadinIcon.ARROW_LEFT.create());
|
||||
backButton.addThemeVariants(com.vaadin.flow.component.button.ButtonVariant.LUMO_TERTIARY);
|
||||
backButton.addClickListener(e -> UI.getCurrent().navigate("user-messages/" + participantKey));
|
||||
|
||||
private ViewToolbar createHeaderLayout(String clientName, String conversationTitle) {
|
||||
// Create title with subtitle in a custom layout
|
||||
VerticalLayout titleLayout = new VerticalLayout();
|
||||
titleLayout.setPadding(false);
|
||||
titleLayout.setSpacing(false);
|
||||
|
||||
H2 title = new H2(clientName);
|
||||
title.getStyle().set("margin", "0");
|
||||
H1 title = new H1(clientName);
|
||||
title.addClassNames(com.vaadin.flow.theme.lumo.LumoUtility.FontSize.XLARGE, com.vaadin.flow.theme.lumo.LumoUtility.Margin.NONE, com.vaadin.flow.theme.lumo.LumoUtility.FontWeight.LIGHT);
|
||||
title.addClassName("view-toolbar-title");
|
||||
|
||||
Span subtitle = new Span(conversationTitle);
|
||||
subtitle.addClassName("message-subtitle");
|
||||
titleLayout.add(title);
|
||||
|
||||
titleLayout.add(title, subtitle);
|
||||
// Use ViewToolbar for consistent header layout with drawer toggle
|
||||
ViewToolbar toolbar = new ViewToolbar("");
|
||||
toolbar.getContent().removeAll();
|
||||
|
||||
HorizontalLayout layout = new HorizontalLayout(backButton, titleLayout);
|
||||
layout.setWidthFull();
|
||||
layout.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER);
|
||||
layout.setSpacing(true);
|
||||
layout.addClassName("message-thread-header");
|
||||
// Build custom header content with drawer toggle
|
||||
com.vaadin.flow.component.applayout.DrawerToggle drawerToggle = new com.vaadin.flow.component.applayout.DrawerToggle();
|
||||
drawerToggle.addClassNames(com.vaadin.flow.theme.lumo.LumoUtility.Margin.NONE);
|
||||
drawerToggle.addClassName("view-toolbar-toggle");
|
||||
|
||||
return layout;
|
||||
com.vaadin.flow.component.html.Div titleRow = new com.vaadin.flow.component.html.Div(drawerToggle, titleLayout);
|
||||
titleRow.addClassNames(com.vaadin.flow.theme.lumo.LumoUtility.Display.FLEX, com.vaadin.flow.theme.lumo.LumoUtility.AlignItems.CENTER);
|
||||
titleRow.addClassName("view-toolbar-title-row");
|
||||
|
||||
toolbar.getContent().add(titleRow);
|
||||
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
private HorizontalLayout createMessageInputArea() {
|
||||
@@ -709,9 +706,10 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has
|
||||
attachButton.getElement().setAttribute("aria-label", "Bild anhängen");
|
||||
attachButton.addClickListener(event -> openImageUploadDialog());
|
||||
|
||||
Button sendButton = new Button("Senden", VaadinIcon.PAPERPLANE.create());
|
||||
Button sendButton = new Button(VaadinIcon.PAPERPLANE.create());
|
||||
sendButton.addThemeVariants(com.vaadin.flow.component.button.ButtonVariant.LUMO_PRIMARY);
|
||||
sendButton.getStyle().set("height", "60px");
|
||||
sendButton.getStyle().set("min-width", "90px");
|
||||
|
||||
sendButton.addClickListener(e -> {
|
||||
String message = messageInput.getValue();
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.vaadin.flow.component.contextmenu.ContextMenu;
|
||||
import com.vaadin.flow.component.html.*;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
@@ -18,8 +20,12 @@ import com.vaadin.flow.router.BeforeEnterEvent;
|
||||
import com.vaadin.flow.router.BeforeEnterObserver;
|
||||
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
||||
import de.assecutor.votianlt.model.Language;
|
||||
import de.assecutor.votianlt.security.SessionAuthenticationService;
|
||||
import de.assecutor.votianlt.security.SecurityService;
|
||||
import de.assecutor.votianlt.service.DemoModeService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -28,10 +34,18 @@ import java.util.Locale;
|
||||
public class StartView extends VerticalLayout implements BeforeEnterObserver, HasDynamicTitle {
|
||||
|
||||
private final SecurityService securityService;
|
||||
private final DemoModeService demoModeService;
|
||||
private final SessionAuthenticationService sessionAuthenticationService;
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final String appVersion;
|
||||
|
||||
public StartView(SecurityService securityService, @Value("${app.version:unknown}") String appVersion) {
|
||||
public StartView(SecurityService securityService, DemoModeService demoModeService,
|
||||
SessionAuthenticationService sessionAuthenticationService, AuthenticationManager authenticationManager,
|
||||
@Value("${app.version:unknown}") String appVersion) {
|
||||
this.securityService = securityService;
|
||||
this.demoModeService = demoModeService;
|
||||
this.sessionAuthenticationService = sessionAuthenticationService;
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.appVersion = appVersion;
|
||||
addClassName("landing-view");
|
||||
setSizeFull();
|
||||
@@ -278,11 +292,48 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
|
||||
Paragraph heroDescription = new Paragraph(getTranslation("start.hero.description"));
|
||||
heroDescription.addClassName("hero-panel-text");
|
||||
|
||||
Button demoButton = new Button(getTranslation("start.button.demo"), event -> loginDemo());
|
||||
demoButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE);
|
||||
demoButton.addClassNames("hero-cta", "hero-demo-cta");
|
||||
demoButton.setWidthFull();
|
||||
|
||||
Button ctaButton = new Button(getTranslation("cta.freetest"), event -> register());
|
||||
ctaButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE);
|
||||
ctaButton.addClassName("hero-cta");
|
||||
ctaButton.setWidthFull();
|
||||
|
||||
heroSection.add(heroIcon, heroTitle, heroDescription, ctaButton);
|
||||
Paragraph demoHint = new Paragraph(getTranslation("start.hero.demo.hint"));
|
||||
demoHint.addClassName("hero-choice-hint");
|
||||
|
||||
Paragraph trialHint = new Paragraph(getTranslation("start.hero.trial.hint"));
|
||||
trialHint.addClassName("hero-choice-hint");
|
||||
|
||||
VerticalLayout demoAction = new VerticalLayout();
|
||||
demoAction.setPadding(false);
|
||||
demoAction.setSpacing(false);
|
||||
demoAction.setWidthFull();
|
||||
demoAction.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
demoAction.addClassName("hero-action-group");
|
||||
demoAction.add(demoButton, demoHint);
|
||||
|
||||
VerticalLayout trialAction = new VerticalLayout();
|
||||
trialAction.setPadding(false);
|
||||
trialAction.setSpacing(false);
|
||||
trialAction.setWidthFull();
|
||||
trialAction.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
trialAction.addClassName("hero-action-group");
|
||||
trialAction.add(ctaButton, trialHint);
|
||||
|
||||
VerticalLayout heroActions = new VerticalLayout();
|
||||
heroActions.setPadding(false);
|
||||
heroActions.setSpacing(false);
|
||||
heroActions.setWidthFull();
|
||||
heroActions.setMaxWidth("420px");
|
||||
heroActions.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
heroActions.addClassName("hero-actions");
|
||||
heroActions.add(demoAction, trialAction);
|
||||
|
||||
heroSection.add(heroIcon, heroTitle, heroDescription, heroActions);
|
||||
return heroSection;
|
||||
}
|
||||
|
||||
@@ -407,6 +458,38 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
|
||||
UI.getCurrent().navigate("login");
|
||||
}
|
||||
|
||||
private void loginDemo() {
|
||||
String sessionId = sessionAuthenticationService.getCurrentSessionId().orElse(null);
|
||||
if (sessionId == null) {
|
||||
showDemoNotification(getTranslation("demo.start.error"), NotificationVariant.LUMO_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean prepared = false;
|
||||
try {
|
||||
prepared = demoModeService.tryPrepareDemoSession(sessionId);
|
||||
if (!prepared) {
|
||||
showDemoNotification(getTranslation("demo.session.active"), NotificationVariant.LUMO_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
var auth = authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(DemoModeService.DEMO_USERNAME, DemoModeService.DEMO_PASSWORD));
|
||||
sessionAuthenticationService.storeAuthentication(auth);
|
||||
UI.getCurrent().getPage().setLocation("/dashboard");
|
||||
} catch (Exception ex) {
|
||||
if (prepared) {
|
||||
demoModeService.cleanupAndReleaseIfOwned(sessionId);
|
||||
}
|
||||
showDemoNotification(getTranslation("demo.start.error"), NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void showDemoNotification(String message, NotificationVariant variant) {
|
||||
Notification notification = Notification.show(message, 4000, Notification.Position.TOP_CENTER);
|
||||
notification.addThemeVariants(variant);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPageTitle() {
|
||||
return getTranslation("page.title.welcome");
|
||||
|
||||
@@ -7,7 +7,7 @@ import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.dependency.JavaScript;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
@@ -56,7 +56,7 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle {
|
||||
addClassName("statistics-chat-view");
|
||||
|
||||
// Header
|
||||
HorizontalLayout header = createHeader();
|
||||
ViewToolbar header = createHeader();
|
||||
add(header);
|
||||
|
||||
// Chat Container mit Scroll
|
||||
@@ -81,25 +81,8 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle {
|
||||
add(inputArea);
|
||||
}
|
||||
|
||||
private HorizontalLayout createHeader() {
|
||||
HorizontalLayout header = new HorizontalLayout();
|
||||
header.setWidthFull();
|
||||
header.setPadding(true);
|
||||
header.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||
header.addClassName("statistics-header");
|
||||
|
||||
Icon aiIcon = VaadinIcon.MAGIC.create();
|
||||
aiIcon.getStyle().set("color", "var(--lumo-primary-color)");
|
||||
|
||||
H2 title = new H2(getTranslation("statistics.title"));
|
||||
title.getStyle().set("margin", "0").set("font-size", "var(--lumo-font-size-xl)");
|
||||
|
||||
Span subtitle = new Span(getTranslation("statistics.subtitle"));
|
||||
subtitle.getStyle().set("color", "var(--lumo-secondary-text-color)").set("font-size", "var(--lumo-font-size-s)")
|
||||
.set("margin-left", "var(--lumo-space-m)");
|
||||
|
||||
header.add(aiIcon, title, subtitle);
|
||||
return header;
|
||||
private ViewToolbar createHeader() {
|
||||
return new ViewToolbar(getTranslation("statistics.title"));
|
||||
}
|
||||
|
||||
private HorizontalLayout createInputArea() {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package de.assecutor.votianlt.pages.view;
|
||||
|
||||
import com.vaadin.flow.component.UI;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.Main;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.BeforeEvent;
|
||||
@@ -83,46 +80,25 @@ public class UserMessagesView extends Main implements HasUrlParameter<String>, H
|
||||
String clientName = client != null ? client.getVorname() + " " + client.getNachname()
|
||||
: Optional.ofNullable(participantKey).orElse(getTranslation("usermessages.unknown.participant"));
|
||||
|
||||
HorizontalLayout headerLayout = createHeaderLayout(clientName);
|
||||
ViewToolbar headerLayout = createHeaderLayout(clientName);
|
||||
contentLayout.add(headerLayout);
|
||||
|
||||
List<Message> conversation = messageService.getMessagesForAppUserAscending(participantKey);
|
||||
Map<MessageType, List<Message>> messagesByType = conversation.stream().collect(Collectors
|
||||
.groupingBy(message -> Optional.ofNullable(message.getMessageType()).orElse(MessageType.GENERAL)));
|
||||
|
||||
VerticalLayout generalSection = createGeneralMessagesSection(messagesByType.get(MessageType.GENERAL));
|
||||
|
||||
VerticalLayout jobSection = createJobMessagesSection(messagesByType.get(MessageType.JOB_RELATED));
|
||||
|
||||
contentLayout.add(generalSection, jobSection);
|
||||
addGeneralMessagesTo(contentLayout, messagesByType.get(MessageType.GENERAL));
|
||||
addJobMessagesTo(contentLayout, messagesByType.get(MessageType.JOB_RELATED));
|
||||
}
|
||||
|
||||
private HorizontalLayout createHeaderLayout(String clientName) {
|
||||
Button backButton = new Button(getTranslation("button.back"), VaadinIcon.ARROW_LEFT.create());
|
||||
backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
backButton.addClickListener(e -> UI.getCurrent().navigate("messages"));
|
||||
|
||||
H2 title = new H2(getTranslation("usermessages.title.with", clientName));
|
||||
|
||||
HorizontalLayout layout = new HorizontalLayout(backButton, title);
|
||||
layout.setWidthFull();
|
||||
layout.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER);
|
||||
layout.setSpacing(true);
|
||||
layout.addClassName("message-thread-header");
|
||||
|
||||
return layout;
|
||||
private ViewToolbar createHeaderLayout(String clientName) {
|
||||
return new ViewToolbar(getTranslation("usermessages.title.with", clientName));
|
||||
}
|
||||
|
||||
private VerticalLayout createGeneralMessagesSection(List<Message> generalMessages) {
|
||||
VerticalLayout section = new VerticalLayout();
|
||||
section.setPadding(true);
|
||||
section.setSpacing(true);
|
||||
section.setWidthFull();
|
||||
section.addClassName("message-section");
|
||||
|
||||
private void addGeneralMessagesTo(VerticalLayout target, List<Message> generalMessages) {
|
||||
H3 title = new H3(getTranslation("usermessages.general.title"));
|
||||
title.addClassName("message-section-title");
|
||||
section.add(title);
|
||||
target.add(title);
|
||||
|
||||
List<Message> sortedMessages = new ArrayList<>();
|
||||
if (generalMessages != null) {
|
||||
@@ -139,26 +115,18 @@ public class UserMessagesView extends Main implements HasUrlParameter<String>, H
|
||||
LocalDateTime lastMessageTime = latest != null ? latest.getCreatedAt() : null;
|
||||
String preview = resolvePreview(latest);
|
||||
|
||||
section.add(createMessageCard(getTranslation("usermessages.general.conversation"), preview, lastMessageTime,
|
||||
target.add(createMessageCard(getTranslation("usermessages.general.conversation"), preview, lastMessageTime,
|
||||
messageCount, unreadCount, "general"));
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
private VerticalLayout createJobMessagesSection(List<Message> jobMessages) {
|
||||
VerticalLayout section = new VerticalLayout();
|
||||
section.setPadding(true);
|
||||
section.setSpacing(true);
|
||||
section.setWidthFull();
|
||||
section.addClassName("message-section");
|
||||
|
||||
private void addJobMessagesTo(VerticalLayout target, List<Message> jobMessages) {
|
||||
H3 title = new H3(getTranslation("usermessages.job.title"));
|
||||
title.addClassName("message-section-title");
|
||||
section.add(title);
|
||||
target.add(title);
|
||||
|
||||
if (jobMessages == null || jobMessages.isEmpty()) {
|
||||
section.add(new Span(getTranslation("usermessages.no.job.messages")));
|
||||
return section;
|
||||
target.add(new Span(getTranslation("usermessages.no.job.messages")));
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, List<Message>> messagesByJob = jobMessages.stream()
|
||||
@@ -171,11 +139,9 @@ public class UserMessagesView extends Main implements HasUrlParameter<String>, H
|
||||
.filter(message -> message.getOrigin() == MessageOrigin.CLIENT && !message.isRead()).count();
|
||||
|
||||
String conversationTitle = getTranslation("usermessages.job.conversation", jobKey);
|
||||
section.add(createMessageCard(conversationTitle, resolvePreview(latest), latest.getCreatedAt(),
|
||||
target.add(createMessageCard(conversationTitle, resolvePreview(latest), latest.getCreatedAt(),
|
||||
messages.size(), unreadCount, "job-" + sanitizeConversationId(jobKey)));
|
||||
});
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
private String resolvePreview(Message message) {
|
||||
@@ -236,7 +202,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String>, H
|
||||
// Add all elements to card
|
||||
VerticalLayout cardContent = new VerticalLayout(titleRow, preview, metaRow);
|
||||
cardContent.setWidthFull();
|
||||
cardContent.setPadding(false);
|
||||
cardContent.setPadding(true);
|
||||
cardContent.setSpacing(false);
|
||||
card.add(cardContent);
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package de.assecutor.votianlt.security;
|
||||
|
||||
import com.vaadin.flow.spring.security.AuthenticationContext;
|
||||
import de.assecutor.votianlt.service.DemoModeService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
@@ -12,12 +14,15 @@ import org.springframework.stereotype.Component;
|
||||
import java.util.Optional;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class SecurityService {
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final DemoModeService demoModeService;
|
||||
|
||||
public SecurityService(AuthenticationContext authenticationContext) {
|
||||
public SecurityService(AuthenticationContext authenticationContext, DemoModeService demoModeService) {
|
||||
this.authenticationContext = authenticationContext;
|
||||
this.demoModeService = demoModeService;
|
||||
}
|
||||
|
||||
public Optional<UserDetails> getAuthenticatedUser() {
|
||||
@@ -99,6 +104,14 @@ public class SecurityService {
|
||||
}
|
||||
|
||||
public void logout() {
|
||||
try {
|
||||
String authenticatedUsername = getAuthenticatedUser().map(UserDetails::getUsername).orElse(null);
|
||||
if (DemoModeService.isDemoUsername(authenticatedUsername)) {
|
||||
demoModeService.cleanupCurrentSessionIfOwned();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("Demo logout cleanup failed: {}", ex.getMessage(), ex);
|
||||
}
|
||||
authenticationContext.logout();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.assecutor.votianlt.security;
|
||||
|
||||
import com.vaadin.flow.server.VaadinSession;
|
||||
import java.util.Optional;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class SessionAuthenticationService {
|
||||
|
||||
public void storeAuthentication(Authentication authentication) {
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
VaadinSession vaadinSession = VaadinSession.getCurrent();
|
||||
if (vaadinSession == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var wrappedSession = vaadinSession.getSession();
|
||||
if (wrappedSession == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
wrappedSession.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
|
||||
SecurityContextHolder.getContext());
|
||||
}
|
||||
|
||||
public Optional<String> getCurrentSessionId() {
|
||||
VaadinSession vaadinSession = VaadinSession.getCurrent();
|
||||
if (vaadinSession == null || vaadinSession.getSession() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(vaadinSession.getSession().getId());
|
||||
}
|
||||
}
|
||||
@@ -358,6 +358,7 @@ public class CustomerInvoiceService {
|
||||
|
||||
int fontSize = element.has("fontSize") ? element.get("fontSize").asInt(14) : 14;
|
||||
String color = element.has("color") ? element.get("color").asText("#333333") : "#333333";
|
||||
String textAlign = element.has("textAlign") ? element.get("textAlign").asText("left") : "left";
|
||||
|
||||
// Convert percentages to mm (A4 is 210mm x 297mm)
|
||||
double mmX = xPercent / 100.0 * 210.0;
|
||||
@@ -392,6 +393,17 @@ public class CustomerInvoiceService {
|
||||
} else {
|
||||
// Vertically center content for other elements
|
||||
htmlBuilder.append("display:flex;align-items:center;");
|
||||
switch (textAlign) {
|
||||
case "center":
|
||||
htmlBuilder.append("justify-content:center;text-align:center;");
|
||||
break;
|
||||
case "right":
|
||||
htmlBuilder.append("justify-content:flex-end;text-align:right;");
|
||||
break;
|
||||
default:
|
||||
htmlBuilder.append("justify-content:flex-start;text-align:left;");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
htmlBuilder.append("'");
|
||||
@@ -649,6 +661,7 @@ public class CustomerInvoiceService {
|
||||
|
||||
int fontSize = element.has("fontSize") ? element.get("fontSize").asInt(14) : 14;
|
||||
String color = element.has("color") ? element.get("color").asText("#333333") : "#333333";
|
||||
String textAlign = element.has("textAlign") ? element.get("textAlign").asText("left") : "left";
|
||||
|
||||
// Convert percentages to mm (A4 is 210mm x 297mm)
|
||||
double mmX = xPercent / 100.0 * 210.0;
|
||||
@@ -682,6 +695,17 @@ public class CustomerInvoiceService {
|
||||
htmlBuilder.append("display:block;overflow:visible;padding:0;");
|
||||
} else {
|
||||
htmlBuilder.append("display:flex;align-items:center;");
|
||||
switch (textAlign) {
|
||||
case "center":
|
||||
htmlBuilder.append("justify-content:center;text-align:center;");
|
||||
break;
|
||||
case "right":
|
||||
htmlBuilder.append("justify-content:flex-end;text-align:right;");
|
||||
break;
|
||||
default:
|
||||
htmlBuilder.append("justify-content:flex-start;text-align:left;");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
htmlBuilder.append("'");
|
||||
|
||||
547
src/main/java/de/assecutor/votianlt/service/DemoModeService.java
Normal file
547
src/main/java/de/assecutor/votianlt/service/DemoModeService.java
Normal file
@@ -0,0 +1,547 @@
|
||||
package de.assecutor.votianlt.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.assecutor.votianlt.model.AppUser;
|
||||
import de.assecutor.votianlt.model.Barcode;
|
||||
import de.assecutor.votianlt.model.Comment;
|
||||
import de.assecutor.votianlt.model.Customer;
|
||||
import de.assecutor.votianlt.model.DeliveryStation;
|
||||
import de.assecutor.votianlt.model.InvoiceTemplate;
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.JobServiceSelection;
|
||||
import de.assecutor.votianlt.model.JobStatus;
|
||||
import de.assecutor.votianlt.model.Language;
|
||||
import de.assecutor.votianlt.model.LocationPosition;
|
||||
import de.assecutor.votianlt.model.Photo;
|
||||
import de.assecutor.votianlt.model.Service;
|
||||
import de.assecutor.votianlt.model.Signature;
|
||||
import de.assecutor.votianlt.model.TaskTemplate;
|
||||
import de.assecutor.votianlt.model.User;
|
||||
import de.assecutor.votianlt.model.invoices.CustomerInvoice;
|
||||
import de.assecutor.votianlt.model.task.BaseTask;
|
||||
import de.assecutor.votianlt.pages.domain.CustomerRepository;
|
||||
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||
import de.assecutor.votianlt.repository.BarcodeRepository;
|
||||
import de.assecutor.votianlt.repository.CargoItemRepository;
|
||||
import de.assecutor.votianlt.repository.CommentRepository;
|
||||
import de.assecutor.votianlt.repository.CustomerInvoiceRepository;
|
||||
import de.assecutor.votianlt.repository.InvoiceTemplateRepository;
|
||||
import de.assecutor.votianlt.repository.JobHistoryRepository;
|
||||
import de.assecutor.votianlt.repository.JobRepository;
|
||||
import de.assecutor.votianlt.repository.LocationPositionRepository;
|
||||
import de.assecutor.votianlt.repository.MessageRepository;
|
||||
import de.assecutor.votianlt.repository.PhotoRepository;
|
||||
import de.assecutor.votianlt.repository.ServiceRepository;
|
||||
import de.assecutor.votianlt.repository.SignatureRepository;
|
||||
import de.assecutor.votianlt.repository.TaskRepository;
|
||||
import de.assecutor.votianlt.repository.TaskTemplateRepository;
|
||||
import de.assecutor.votianlt.repository.UserInvoiceDataRepository;
|
||||
import de.assecutor.votianlt.repository.UserRepository;
|
||||
import de.assecutor.votianlt.security.SessionAuthenticationService;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@org.springframework.stereotype.Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
@Slf4j
|
||||
public class DemoModeService {
|
||||
|
||||
public static final String DEMO_USERNAME = "demo";
|
||||
public static final String DEMO_PASSWORD = "demo";
|
||||
private static final String DEMO_FIRST_NAME = "Demo";
|
||||
private static final String DEMO_LAST_NAME = "Benutzer";
|
||||
private static final String DEMO_INVOICE_TEMPLATE_RESOURCE = "templates/demo_invoice_template_default.json";
|
||||
|
||||
private final DemoSessionRegistry demoSessionRegistry;
|
||||
private final SessionAuthenticationService sessionAuthenticationService;
|
||||
private final UserRepository userRepository;
|
||||
private final CustomerRepository customerRepository;
|
||||
private final JobRepository jobRepository;
|
||||
private final CargoItemRepository cargoItemRepository;
|
||||
private final TaskRepository taskRepository;
|
||||
private final JobHistoryRepository jobHistoryRepository;
|
||||
private final CustomerInvoiceRepository customerInvoiceRepository;
|
||||
private final ServiceRepository serviceRepository;
|
||||
private final AppUserRepository appUserRepository;
|
||||
private final TaskTemplateRepository taskTemplateRepository;
|
||||
private final InvoiceTemplateRepository invoiceTemplateRepository;
|
||||
private final UserInvoiceDataRepository userInvoiceDataRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
private final LocationPositionRepository locationPositionRepository;
|
||||
private final PhotoRepository photoRepository;
|
||||
private final SignatureRepository signatureRepository;
|
||||
private final BarcodeRepository barcodeRepository;
|
||||
private final CommentRepository commentRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public static boolean isDemoUsername(String username) {
|
||||
return username != null && DEMO_USERNAME.equalsIgnoreCase(username.trim());
|
||||
}
|
||||
|
||||
public void ensureDemoUser() {
|
||||
User demoUser = userRepository.findByEmail(DEMO_USERNAME).orElseGet(User::new);
|
||||
applyDemoUserDefaults(demoUser);
|
||||
userRepository.save(demoUser);
|
||||
}
|
||||
|
||||
public boolean tryPrepareDemoSession(String sessionId) {
|
||||
if (!demoSessionRegistry.acquire(sessionId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
ensureDemoUser();
|
||||
cleanupDemoOwnedData();
|
||||
resetDemoUserProfile();
|
||||
seedDemoData();
|
||||
return true;
|
||||
} catch (RuntimeException ex) {
|
||||
demoSessionRegistry.release(sessionId);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanupCurrentSessionIfOwned() {
|
||||
sessionAuthenticationService.getCurrentSessionId().ifPresent(this::cleanupAndReleaseIfOwned);
|
||||
}
|
||||
|
||||
public void cleanupAndReleaseIfOwned(String sessionId) {
|
||||
if (!demoSessionRegistry.isHeldBy(sessionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
cleanupDemoOwnedData();
|
||||
resetDemoUserProfile();
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("Demo cleanup failed for session {}: {}", sessionId, ex.getMessage(), ex);
|
||||
} finally {
|
||||
demoSessionRegistry.release(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
public void resetDemoUserProfile() {
|
||||
User demoUser = getDemoUser();
|
||||
applyDemoUserDefaults(demoUser);
|
||||
userRepository.save(demoUser);
|
||||
}
|
||||
|
||||
public void cleanupDemoOwnedData() {
|
||||
Optional<User> demoUserOptional = userRepository.findByEmail(DEMO_USERNAME);
|
||||
if (demoUserOptional.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
User demoUser = demoUserOptional.get();
|
||||
ObjectId demoUserId = demoUser.getId();
|
||||
if (demoUserId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String demoUserIdHex = demoUserId.toHexString();
|
||||
|
||||
List<AppUser> demoAppUsers = appUserRepository.findByErstelltVon(demoUserId);
|
||||
cleanupDemoAppUserData(demoAppUsers);
|
||||
|
||||
List<Job> demoJobs = jobRepository.findByCreatedBy(demoUserIdHex);
|
||||
cleanupDemoJobData(demoJobs);
|
||||
|
||||
List<Customer> demoCustomers = customerRepository.findByOwner(demoUserId);
|
||||
if (!demoCustomers.isEmpty()) {
|
||||
customerRepository.deleteAll(demoCustomers);
|
||||
}
|
||||
|
||||
serviceRepository.deleteByUserId(demoUserIdHex);
|
||||
|
||||
List<TaskTemplate> demoTaskTemplates = taskTemplateRepository.findByUserIdOrderByTemplateNameAsc(demoUserId);
|
||||
if (!demoTaskTemplates.isEmpty()) {
|
||||
taskTemplateRepository.deleteAll(demoTaskTemplates);
|
||||
}
|
||||
|
||||
invoiceTemplateRepository.deleteByUserId(demoUserIdHex);
|
||||
userInvoiceDataRepository.deleteByUserId(demoUserId);
|
||||
|
||||
List<CustomerInvoice> demoInvoices = customerInvoiceRepository.findByUserId(demoUserIdHex);
|
||||
if (!demoInvoices.isEmpty()) {
|
||||
customerInvoiceRepository.deleteAll(demoInvoices);
|
||||
}
|
||||
|
||||
if (!demoAppUsers.isEmpty()) {
|
||||
appUserRepository.deleteAll(demoAppUsers);
|
||||
}
|
||||
}
|
||||
|
||||
public void seedDemoData() {
|
||||
User demoUser = getDemoUser();
|
||||
List<Service> services = seedDemoServices(demoUser);
|
||||
List<Customer> customers = seedDemoCustomers(demoUser);
|
||||
seedDemoJobs(demoUser, customers, services);
|
||||
seedDemoInvoiceTemplate(demoUser);
|
||||
}
|
||||
|
||||
private void cleanupDemoAppUserData(List<AppUser> demoAppUsers) {
|
||||
for (AppUser appUser : demoAppUsers) {
|
||||
String appUserId = appUser.getIdAsString();
|
||||
if (appUserId == null || appUserId.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var messages = messageRepository.findByReceiverOrderByCreatedAtAsc(appUserId);
|
||||
if (!messages.isEmpty()) {
|
||||
messageRepository.deleteAll(messages);
|
||||
}
|
||||
|
||||
var positions = locationPositionRepository.findByAppUserIdOrderByTimestampDesc(appUserId);
|
||||
if (!positions.isEmpty()) {
|
||||
locationPositionRepository.deleteAll(positions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupDemoJobData(List<Job> demoJobs) {
|
||||
if (demoJobs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<ObjectId> stationIds = new LinkedHashSet<>();
|
||||
Set<ObjectId> jobIds = new LinkedHashSet<>();
|
||||
Set<String> jobIdStrings = new LinkedHashSet<>();
|
||||
for (Job job : demoJobs) {
|
||||
if (job == null || job.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
jobIds.add(job.getId());
|
||||
jobIdStrings.add(job.getIdAsString());
|
||||
if (job.getDeliveryStations() == null) {
|
||||
continue;
|
||||
}
|
||||
for (DeliveryStation station : job.getDeliveryStations()) {
|
||||
if (station != null && station.getStationId() != null) {
|
||||
stationIds.add(station.getStationId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<ObjectId, BaseTask> tasksById = new LinkedHashMap<>();
|
||||
if (!stationIds.isEmpty()) {
|
||||
for (BaseTask task : taskRepository.findByStationIdIn(new ArrayList<>(stationIds))) {
|
||||
if (task != null && task.getId() != null) {
|
||||
tasksById.put(task.getId(), task);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (ObjectId jobId : jobIds) {
|
||||
for (BaseTask task : taskRepository.findByJobIdOrderByTaskOrderAsc(jobId)) {
|
||||
if (task != null && task.getId() != null) {
|
||||
tasksById.put(task.getId(), task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanupTaskArtifacts(tasksById.keySet());
|
||||
|
||||
if (!tasksById.isEmpty()) {
|
||||
taskRepository.deleteAll(tasksById.values());
|
||||
}
|
||||
|
||||
for (ObjectId jobId : jobIds) {
|
||||
var cargoItems = cargoItemRepository.findByJobId(jobId);
|
||||
if (!cargoItems.isEmpty()) {
|
||||
cargoItemRepository.deleteAll(cargoItems);
|
||||
}
|
||||
jobHistoryRepository.deleteByJobId(jobId);
|
||||
}
|
||||
|
||||
List<CustomerInvoice> invoicesToDelete = new ArrayList<>();
|
||||
for (String jobId : jobIdStrings) {
|
||||
customerInvoiceRepository.findByJobId(jobId).ifPresent(invoicesToDelete::add);
|
||||
}
|
||||
if (!invoicesToDelete.isEmpty()) {
|
||||
customerInvoiceRepository.deleteAll(dedupeInvoices(invoicesToDelete));
|
||||
}
|
||||
|
||||
jobRepository.deleteAll(demoJobs);
|
||||
}
|
||||
|
||||
private void cleanupTaskArtifacts(Collection<ObjectId> taskIds) {
|
||||
for (ObjectId taskId : taskIds) {
|
||||
if (taskId == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Photo> photos = photoRepository.findByTaskId(taskId);
|
||||
if (!photos.isEmpty()) {
|
||||
photoRepository.deleteAll(photos);
|
||||
}
|
||||
|
||||
List<Signature> signatures = signatureRepository.findByTaskId(taskId);
|
||||
if (!signatures.isEmpty()) {
|
||||
signatureRepository.deleteAll(signatures);
|
||||
}
|
||||
|
||||
List<Barcode> barcodes = barcodeRepository.findByTaskId(taskId);
|
||||
if (!barcodes.isEmpty()) {
|
||||
barcodeRepository.deleteAll(barcodes);
|
||||
}
|
||||
|
||||
List<Comment> comments = commentRepository.findByTaskIdOrderByCreatedAtDesc(taskId);
|
||||
if (!comments.isEmpty()) {
|
||||
commentRepository.deleteAll(comments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<CustomerInvoice> dedupeInvoices(List<CustomerInvoice> invoices) {
|
||||
Map<String, CustomerInvoice> unique = new LinkedHashMap<>();
|
||||
for (CustomerInvoice invoice : invoices) {
|
||||
if (invoice == null || invoice.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
unique.putIfAbsent(invoice.getId(), invoice);
|
||||
}
|
||||
return new ArrayList<>(unique.values());
|
||||
}
|
||||
|
||||
private List<Customer> seedDemoCustomers(User demoUser) {
|
||||
ObjectId demoUserId = demoUser.getId();
|
||||
|
||||
Customer firstCustomer = new Customer();
|
||||
firstCustomer.setTitle("Frau");
|
||||
firstCustomer.setCompanyName("Demo Bau GmbH");
|
||||
firstCustomer.setFirstname("Anna");
|
||||
firstCustomer.setLastName("Sommer");
|
||||
firstCustomer.setTelephone("030 1234567");
|
||||
firstCustomer.setMail("anna.sommer@demo.invalid");
|
||||
firstCustomer.setStreet("Musterstrasse");
|
||||
firstCustomer.setHouseNumber("12");
|
||||
firstCustomer.setZip("10115");
|
||||
firstCustomer.setCity("Berlin");
|
||||
firstCustomer.setCreatedBy(demoUserId);
|
||||
firstCustomer.setOwner(demoUserId);
|
||||
|
||||
Customer secondCustomer = new Customer();
|
||||
secondCustomer.setTitle("Herr");
|
||||
secondCustomer.setCompanyName("Nordhandel AG");
|
||||
secondCustomer.setFirstname("Lukas");
|
||||
secondCustomer.setLastName("Becker");
|
||||
secondCustomer.setTelephone("040 7654321");
|
||||
secondCustomer.setMail("lukas.becker@demo.invalid");
|
||||
secondCustomer.setStreet("Hafenallee");
|
||||
secondCustomer.setHouseNumber("8");
|
||||
secondCustomer.setZip("20095");
|
||||
secondCustomer.setCity("Hamburg");
|
||||
secondCustomer.setCreatedBy(demoUserId);
|
||||
secondCustomer.setOwner(demoUserId);
|
||||
|
||||
return customerRepository.saveAll(List.of(firstCustomer, secondCustomer));
|
||||
}
|
||||
|
||||
private List<Service> seedDemoServices(User demoUser) {
|
||||
String demoUserId = demoUser.getId().toHexString();
|
||||
|
||||
Service activeService = createFlatRateService(demoUserId, "Expresszustellung", "149.00");
|
||||
Service completedService = createFlatRateService(demoUserId, "Konsolidierungszustellung", "219.00");
|
||||
|
||||
return serviceRepository.saveAll(List.of(activeService, completedService));
|
||||
}
|
||||
|
||||
private void seedDemoJobs(User demoUser, List<Customer> customers, List<Service> services) {
|
||||
Map<String, Customer> customersByCompany = new LinkedHashMap<>();
|
||||
for (Customer customer : customers) {
|
||||
customersByCompany.put(customer.getCompanyName(), customer);
|
||||
}
|
||||
Map<String, Service> servicesByName = new LinkedHashMap<>();
|
||||
for (Service service : services) {
|
||||
servicesByName.put(service.getName(), service);
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String demoUserId = demoUser.getId().toHexString();
|
||||
|
||||
Job activeJob = new Job();
|
||||
activeJob.setJobNumber("DEMO-AKTIV-001");
|
||||
activeJob.setStatus(JobStatus.IN_PROGRESS);
|
||||
activeJob.setCreatedAt(now.minusDays(2));
|
||||
activeJob.setUpdatedAt(now.minusHours(3));
|
||||
activeJob.setCreatedBy(demoUserId);
|
||||
activeJob.setDraft(false);
|
||||
activeJob.setDigitalProcessing(false);
|
||||
activeJob.setPickupDate(LocalDate.now().minusDays(1));
|
||||
activeJob.setPickupTime(LocalTime.of(9, 30));
|
||||
activeJob.setRemark("Demo-Auftrag aktiv");
|
||||
activeJob.setPrice(new BigDecimal("149.00"));
|
||||
applyCustomerToPickup(activeJob, customersByCompany.get("Demo Bau GmbH"));
|
||||
activeJob.setCustomerSelection("Demo Bau GmbH | Anna Sommer");
|
||||
activeJob.setDeliveryStations(new ArrayList<>(List.of(
|
||||
createDeliveryStation("Potsdam Expresslager", "Herr", "Timo", "Kranz", "0331 444555",
|
||||
"Lange Bruecke", "4", null, "14467", "Potsdam", LocalDate.now(), LocalTime.of(14, 0), 0))));
|
||||
activeJob.setServiceIds(new ArrayList<>(List.of(servicesByName.get("Expresszustellung").getId())));
|
||||
activeJob.setSelectedServices(new ArrayList<>(List.of(
|
||||
createJobServiceSelection(servicesByName.get("Expresszustellung"), 0))));
|
||||
activeJob.syncFlatDeliveryFieldsFromStations();
|
||||
|
||||
Job completedJob = new Job();
|
||||
completedJob.setJobNumber("DEMO-ERLEDIGT-001");
|
||||
completedJob.setStatus(JobStatus.COMPLETED);
|
||||
completedJob.setCreatedAt(now.minusDays(8));
|
||||
completedJob.setUpdatedAt(now.minusDays(5));
|
||||
completedJob.setCreatedBy(demoUserId);
|
||||
completedJob.setDraft(false);
|
||||
completedJob.setDigitalProcessing(false);
|
||||
completedJob.setPickupDate(LocalDate.now().minusDays(7));
|
||||
completedJob.setPickupTime(LocalTime.of(8, 15));
|
||||
completedJob.setRemark("Demo-Auftrag abgeschlossen");
|
||||
completedJob.setPrice(new BigDecimal("219.00"));
|
||||
applyCustomerToPickup(completedJob, customersByCompany.get("Nordhandel AG"));
|
||||
completedJob.setCustomerSelection("Nordhandel AG | Lukas Becker");
|
||||
completedJob.setDeliveryStations(new ArrayList<>(List.of(
|
||||
createDeliveryStation("Bremen Konsolidierung", "Frau", "Mara", "Jensen", "0421 987654", "Kontorweg",
|
||||
"19", null, "28195", "Bremen", LocalDate.now().minusDays(6), LocalTime.of(16, 30), 0))));
|
||||
completedJob.setServiceIds(new ArrayList<>(List.of(servicesByName.get("Konsolidierungszustellung").getId())));
|
||||
completedJob.setSelectedServices(new ArrayList<>(List.of(
|
||||
createJobServiceSelection(servicesByName.get("Konsolidierungszustellung"), 0))));
|
||||
completedJob.syncFlatDeliveryFieldsFromStations();
|
||||
|
||||
jobRepository.saveAll(List.of(activeJob, completedJob));
|
||||
}
|
||||
|
||||
private Service createFlatRateService(String userId, String name, String price) {
|
||||
Service service = new Service();
|
||||
service.setId(new ObjectId().toHexString());
|
||||
service.setUserId(userId);
|
||||
service.setName(name);
|
||||
service.setCalculationBasis(Service.CalculationBasis.FLAT_RATE);
|
||||
service.setPrice(new BigDecimal(price));
|
||||
service.setMandatory(false);
|
||||
return service;
|
||||
}
|
||||
|
||||
private JobServiceSelection createJobServiceSelection(Service service, int deliveryStationOrder) {
|
||||
JobServiceSelection selection = new JobServiceSelection();
|
||||
selection.setServiceId(service.getId());
|
||||
selection.setDeliveryStationOrder(deliveryStationOrder);
|
||||
return selection;
|
||||
}
|
||||
|
||||
private void seedDemoInvoiceTemplate(User demoUser) {
|
||||
InvoiceTemplate template = new InvoiceTemplate(demoUser.getId().toHexString(), "Einfach",
|
||||
createDemoInvoiceTemplateData());
|
||||
invoiceTemplateRepository.save(template);
|
||||
}
|
||||
|
||||
private String createDemoInvoiceTemplateData() {
|
||||
try {
|
||||
String templateJson = new String(
|
||||
new ClassPathResource(DEMO_INVOICE_TEMPLATE_RESOURCE).getInputStream().readAllBytes(),
|
||||
StandardCharsets.UTF_8);
|
||||
return new ObjectMapper().writeValueAsString(templateJson);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("Could not load demo invoice template", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyCustomerToPickup(Job job, Customer customer) {
|
||||
if (job == null || customer == null) {
|
||||
return;
|
||||
}
|
||||
job.setPickupCompany(customer.getCompanyName());
|
||||
job.setPickupSalutation(customer.getTitle());
|
||||
job.setPickupFirstName(customer.getFirstname());
|
||||
job.setPickupLastName(customer.getLastName());
|
||||
job.setPickupPhone(customer.getTelephone());
|
||||
job.setPickupStreet(customer.getStreet());
|
||||
job.setPickupHouseNumber(customer.getHouseNumber());
|
||||
job.setPickupAddressAddition(customer.getAddressAddition());
|
||||
job.setPickupZip(customer.getZip());
|
||||
job.setPickupCity(customer.getCity());
|
||||
}
|
||||
|
||||
private DeliveryStation createDeliveryStation(String company, String salutation, String firstName, String lastName,
|
||||
String phone, String street, String houseNumber, String addressAddition, String zip, String city,
|
||||
LocalDate deliveryDate, LocalTime deliveryTime, int stationOrder) {
|
||||
DeliveryStation station = new DeliveryStation();
|
||||
station.ensureStationId();
|
||||
station.setStationOrder(stationOrder);
|
||||
station.setCompany(company);
|
||||
station.setSalutation(salutation);
|
||||
station.setFirstName(firstName);
|
||||
station.setLastName(lastName);
|
||||
station.setPhone(phone);
|
||||
station.setStreet(street);
|
||||
station.setHouseNumber(houseNumber);
|
||||
station.setAddressAddition(addressAddition);
|
||||
station.setZip(zip);
|
||||
station.setCity(city);
|
||||
station.setDeliveryDate(deliveryDate);
|
||||
station.setDeliveryTime(deliveryTime);
|
||||
station.setTasks(new ArrayList<>());
|
||||
return station;
|
||||
}
|
||||
|
||||
private User getDemoUser() {
|
||||
return userRepository.findByEmail(DEMO_USERNAME)
|
||||
.orElseThrow(() -> new IllegalStateException("Demo user does not exist"));
|
||||
}
|
||||
|
||||
private void applyDemoUserDefaults(User demoUser) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (demoUser.getCreatedAt() == null) {
|
||||
demoUser.setCreatedAt(now);
|
||||
}
|
||||
|
||||
demoUser.setEmail(DEMO_USERNAME);
|
||||
demoUser.setPassword(passwordEncoder.encode(DEMO_PASSWORD));
|
||||
demoUser.setFirstname(DEMO_FIRST_NAME);
|
||||
demoUser.setName(DEMO_LAST_NAME);
|
||||
demoUser.setTitle(null);
|
||||
demoUser.setCompany(null);
|
||||
demoUser.setCompanyAddition(null);
|
||||
demoUser.setStreet(null);
|
||||
demoUser.setHouseNumber(null);
|
||||
demoUser.setAddressAddition(null);
|
||||
demoUser.setZip(null);
|
||||
demoUser.setCity(null);
|
||||
demoUser.setDiffInvoiceAddress(false);
|
||||
demoUser.setInvCompany(null);
|
||||
demoUser.setInvCompanyAddition(null);
|
||||
demoUser.setInvFirstname(null);
|
||||
demoUser.setInvLastname(null);
|
||||
demoUser.setInvStreet(null);
|
||||
demoUser.setInvHouseNumber(null);
|
||||
demoUser.setInvAddressAddition(null);
|
||||
demoUser.setInvZip(null);
|
||||
demoUser.setInvCity(null);
|
||||
demoUser.setPhone(null);
|
||||
demoUser.setPhone2(null);
|
||||
demoUser.setFax(null);
|
||||
demoUser.setPasswordCode(null);
|
||||
demoUser.setPasswordTimestamp(null);
|
||||
demoUser.setIsActivated((byte) 1);
|
||||
demoUser.setIsEmailConfirmed((byte) 1);
|
||||
demoUser.setRoles(Set.of("USER"));
|
||||
demoUser.setDigitalProcessingEnabled(true);
|
||||
demoUser.setLocationTrackingEnabled(true);
|
||||
demoUser.setTwoFactorEnabled(false);
|
||||
demoUser.setLanguage(Language.DE);
|
||||
demoUser.setUpdatedAt(now);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.assecutor.votianlt.service;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DemoSessionRegistry {
|
||||
|
||||
private final AtomicReference<String> activeSessionId = new AtomicReference<>();
|
||||
|
||||
public synchronized boolean acquire(String sessionId) {
|
||||
if (sessionId == null || sessionId.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String current = activeSessionId.get();
|
||||
if (current == null || Objects.equals(current, sessionId)) {
|
||||
activeSessionId.set(sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isHeldBy(String sessionId) {
|
||||
return sessionId != null && sessionId.equals(activeSessionId.get());
|
||||
}
|
||||
|
||||
public synchronized void release(String sessionId) {
|
||||
if (isHeldBy(sessionId)) {
|
||||
activeSessionId.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<String> getActiveSessionId() {
|
||||
return Optional.ofNullable(activeSessionId.get());
|
||||
}
|
||||
}
|
||||
@@ -806,11 +806,17 @@ register.notification.failed=Registrierung fehlgeschlagen: {0}
|
||||
# Start Page
|
||||
start.title=VotianLT - Ihr digitaler Transportpartner
|
||||
start.button.login=Anmelden
|
||||
start.button.demo=Demo
|
||||
start.button.register=Registrieren
|
||||
login.demo.only.button=Der Demo-Zugang ist nur über den Demo-Button auf der Startseite verfügbar.
|
||||
demo.session.active=Der Demo-Modus wird bereits von einem anderen Nutzer verwendet. Bitte versuchen Sie es später erneut.
|
||||
demo.start.error=Der Demo-Modus konnte nicht gestartet werden.
|
||||
start.button.createorder=Auftragserstellung
|
||||
start.button.notifications=Benachrichtigungen
|
||||
start.button.nonotifications=Keine neuen Benachrichtigungen
|
||||
start.hero.description=Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe - volldigital und aus einem Guss. Konzentrieren Sie sich auf Ihr Geschäft, wir kümmern uns um die Büroarbeit.
|
||||
start.hero.demo.hint=Demo startet sofort mit vorbereiteten Beispieldaten.
|
||||
start.hero.trial.hint="Jetzt kostenlos testen" erstellt Ihren eigenen Account für den kostenlosen Probemonat.
|
||||
start.system.title=Das System
|
||||
start.system.intro=Für Solo-Selbstständige und Kleinunternehmer im Transportgewerbe ist von entscheidender Bedeutung, dass sie sich in erster Linie auf ihr eigentliches Geschäft konzentrieren können: Kunden gewinnen und Waren von A nach B liefern.
|
||||
start.feature.setup.title=Einrichtungsassistent
|
||||
|
||||
@@ -728,11 +728,17 @@ register.notification.success=Registreerimine \u00f5nnestus. Palun logige sisse.
|
||||
register.notification.failed=Registreerimine eba\u00f5nnestus: {0}
|
||||
start.title=VotianLT - Teie digitaalne transpordipartner
|
||||
start.button.login=Logi sisse
|
||||
start.button.demo=Demo
|
||||
start.button.register=Registreeru
|
||||
login.demo.only.button=Demo ligipääs on võimalik ainult avalehe Demo nupu kaudu.
|
||||
demo.session.active=Demorežiimi kasutab juba teine kasutaja. Palun proovige hiljem uuesti.
|
||||
demo.start.error=Demorežiimi ei õnnestunud käivitada.
|
||||
start.button.createorder=Tellimuse loomine
|
||||
start.button.notifications=Teavitused
|
||||
start.button.nonotifications=Uusi teavitusi pole
|
||||
start.hero.description=\u00dcksikettev\u00f5tjatele ja v\u00e4ikeettev\u00f5tjatele transpordisektoris \u2013 t\u00e4ielikult digitaalne ja terviklik. Keskenduge oma \u00e4rile, meie hoolitseme kontoritöö eest.
|
||||
start.hero.demo.hint=Demo k\u00e4ivitub kohe ettevalmistatud n\u00e4idisandmetega.
|
||||
start.hero.trial.hint="Proovi kohe tasuta" loob sinu isikliku konto tasuta proovikuuks.
|
||||
start.system.title=S\u00fcsteem
|
||||
start.system.intro=\u00dcksikettev\u00f5tjate ja v\u00e4ikeettev\u00f5tjate jaoks transpordisektoris on otsustava t\u00e4htsusega, et nad saaksid keskenduda oma p\u00f5hitegevusele: klientide v\u00f5itmine ja kaupade kohaletoimetamine punktist A punkti B.
|
||||
start.feature.setup.title=Seadistusabi
|
||||
|
||||
@@ -806,11 +806,17 @@ register.notification.failed=Registration failed: {0}
|
||||
# Start Page
|
||||
start.title=VotianLT - Your Digital Transport Partner
|
||||
start.button.login=Log In
|
||||
start.button.demo=Demo
|
||||
start.button.register=Register
|
||||
login.demo.only.button=Demo access is only available through the Demo button on the start page.
|
||||
demo.session.active=Demo mode is already being used by another user. Please try again later.
|
||||
demo.start.error=Demo mode could not be started.
|
||||
start.button.createorder=Create Job
|
||||
start.button.notifications=Notifications
|
||||
start.button.nonotifications=No new notifications
|
||||
start.hero.description=For solo self-employed and small business owners in the transport industry - fully digital and all-in-one. Focus on your business, we take care of the paperwork.
|
||||
start.hero.demo.hint=Demo starts immediately with prepared sample data.
|
||||
start.hero.trial.hint="Try now for free" creates your own account for the free trial month.
|
||||
start.system.title=The System
|
||||
start.system.intro=For solo self-employed and small business owners in the transport industry, it is crucial that they can primarily focus on their actual business: winning customers and delivering goods from A to B.
|
||||
start.feature.setup.title=Setup Wizard
|
||||
|
||||
@@ -806,11 +806,17 @@ register.notification.failed=El registro ha fallado: {0}
|
||||
# Start Page
|
||||
start.title=VotianLT - Su socio digital de transporte
|
||||
start.button.login=Iniciar sesi\u00f3n
|
||||
start.button.demo=Demo
|
||||
start.button.register=Registrarse
|
||||
login.demo.only.button=El acceso demo solo est\u00e1 disponible mediante el bot\u00f3n Demo de la p\u00e1gina de inicio.
|
||||
demo.session.active=Otro usuario ya est\u00e1 utilizando el modo demo. Por favor, int\u00e9ntelo de nuevo m\u00e1s tarde.
|
||||
demo.start.error=No se pudo iniciar el modo demo.
|
||||
start.button.createorder=Crear pedido
|
||||
start.button.notifications=Notificaciones
|
||||
start.button.nonotifications=No hay nuevas notificaciones
|
||||
start.hero.description=Para aut\u00f3nomos y peque\u00f1os empresarios del sector del transporte - totalmente digital y de una sola pieza. Conc\u00e9ntrese en su negocio, nosotros nos encargamos del trabajo de oficina.
|
||||
start.hero.demo.hint=La demo se inicia inmediatamente con datos de ejemplo preparados.
|
||||
start.hero.trial.hint="Probar gratis ahora" crea su propia cuenta para el mes de prueba gratuito.
|
||||
start.system.title=El sistema
|
||||
start.system.intro=Para aut\u00f3nomos y peque\u00f1os empresarios del sector del transporte es de vital importancia poder concentrarse en su negocio principal: captar clientes y entregar mercanc\u00edas de A a B.
|
||||
start.feature.setup.title=Asistente de configuraci\u00f3n
|
||||
|
||||
@@ -806,11 +806,17 @@ register.notification.failed=\u00c9chec de l'inscription : {0}
|
||||
# Start Page
|
||||
start.title=VotianLT - Votre partenaire de transport num\u00e9rique
|
||||
start.button.login=Se connecter
|
||||
start.button.demo=Demo
|
||||
start.button.register=S'inscrire
|
||||
login.demo.only.button=L'acc\u00e8s d\u00e9mo est disponible uniquement via le bouton Demo de la page d'accueil.
|
||||
demo.session.active=Le mode d\u00e9mo est d\u00e9j\u00e0 utilis\u00e9 par un autre utilisateur. Veuillez r\u00e9essayer plus tard.
|
||||
demo.start.error=Impossible de d\u00e9marrer le mode d\u00e9mo.
|
||||
start.button.createorder=Cr\u00e9ation de mission
|
||||
start.button.notifications=Notifications
|
||||
start.button.nonotifications=Aucune nouvelle notification
|
||||
start.hero.description=Pour les travailleurs ind\u00e9pendants et les petites entreprises du secteur du transport - enti\u00e8rement num\u00e9rique et int\u00e9gr\u00e9. Concentrez-vous sur votre activit\u00e9, nous nous occupons de la paperasse.
|
||||
start.hero.demo.hint=La d\u00e9mo d\u00e9marre imm\u00e9diatement avec des donn\u00e9es d'exemple pr\u00e9par\u00e9es.
|
||||
start.hero.trial.hint="Tester gratuitement maintenant" cr\u00e9e votre propre compte pour le mois d'essai gratuit.
|
||||
start.system.title=Le syst\u00e8me
|
||||
start.system.intro=Pour les travailleurs ind\u00e9pendants et les petites entreprises du secteur du transport, il est essentiel de pouvoir se concentrer en priorit\u00e9 sur leur activit\u00e9 principale : gagner des clients et livrer des marchandises de A \u00e0 B.
|
||||
start.feature.setup.title=Assistant de configuration
|
||||
|
||||
@@ -806,11 +806,17 @@ register.notification.failed=Registracija nepavyko: {0}
|
||||
# Start Page
|
||||
start.title=VotianLT - Jūsų skaitmeninis transporto partneris
|
||||
start.button.login=Prisijungti
|
||||
start.button.demo=Demo
|
||||
start.button.register=Registruotis
|
||||
login.demo.only.button=Demo prieiga galima tik per prad\u017eios puslapio mygtuk\u0105 „Demo“.
|
||||
demo.session.active=Demo re\u017eimu jau naudojasi kitas vartotojas. Bandykite dar kart\u0105 v\u0117liau.
|
||||
demo.start.error=Nepavyko paleisti demo re\u017eimo.
|
||||
start.button.createorder=Užsakymo kūrimas
|
||||
start.button.notifications=Pranešimai
|
||||
start.button.nonotifications=Nėra naujų pranešimų
|
||||
start.hero.description=Individualiems verslininkams ir smulkaus verslo savininkams transporto sektoriuje – visiškai skaitmeninis ir vientisas sprendimas. Sutelkite dėmesį į savo verslą, mes pasirūpinsime biuro darbu.
|
||||
start.hero.demo.hint=Demo režimas iš karto paleidžiamas su paruoštais pavyzdiniais duomenimis.
|
||||
start.hero.trial.hint=„Išbandyti nemokamai dabar“ sukuria jūsų paskyrą nemokamam bandomajam mėnesiui.
|
||||
start.system.title=Sistema
|
||||
start.system.intro=Individualiems verslininkams ir smulkaus verslo savininkams transporto sektoriuje labai svarbu, kad jie galėtų visų pirma sutelkti dėmesį į savo pagrindinį verslą: klientų pritraukimą ir prekių pristatymą iš taško A į tašką B.
|
||||
start.feature.setup.title=Sąrankos vedlys
|
||||
|
||||
@@ -806,11 +806,17 @@ register.notification.failed=Reģistrācija neizdevās: {0}
|
||||
# Start Page
|
||||
start.title=VotianLT - Jūsu digitālais transporta partneris
|
||||
start.button.login=Pieteikties
|
||||
start.button.demo=Demo
|
||||
start.button.register=Reģistrēties
|
||||
login.demo.only.button=Demonstrācijas piekļuve ir pieejama tikai caur sākumlapas pogu "Demo".
|
||||
demo.session.active=Demonstrācijas režīmu jau izmanto cits lietotājs. Lūdzu, mēģiniet vēlreiz vēlāk.
|
||||
demo.start.error=Neizdevās palaist demonstrācijas režīmu.
|
||||
start.button.createorder=Izveidot uzdevumu
|
||||
start.button.notifications=Paziņojumi
|
||||
start.button.nonotifications=Nav jaunu paziņojumu
|
||||
start.hero.description=Individuālajiem uzņēmējiem un mazajiem uzņēmumiem transporta nozarē \u2013 pilnībā digitāli un no viena avota. Koncentrējieties uz savu biznesu, mēs parūpēsimies par biroja darbu.
|
||||
start.hero.demo.hint=Demo režīms tiek palaists uzreiz ar sagatavotiem parauga datiem.
|
||||
start.hero.trial.hint=“Izmēģināt bez maksas tagad” izveido jūsu kontu bezmaksas izmēģinājuma mēnesim.
|
||||
start.system.title=Sistēma
|
||||
start.system.intro=Individuālajiem uzņēmējiem un mazajiem uzņēmumiem transporta nozarē ir būtiski svarīgi, lai viņi varētu koncentrēties uz savu pamatdarbību: klientu piesaisti un preču piegādi no A uz B.
|
||||
start.feature.setup.title=Iestatīšanas palīgs
|
||||
|
||||
@@ -806,11 +806,17 @@ register.notification.failed=Rejestracja nie powiod\u0142a si\u0119: {0}
|
||||
# Start Page
|
||||
start.title=VotianLT - Tw\u00f3j cyfrowy partner transportowy
|
||||
start.button.login=Zaloguj si\u0119
|
||||
start.button.demo=Demo
|
||||
start.button.register=Zarejestruj si\u0119
|
||||
login.demo.only.button=Dost\u0119p demo jest dost\u0119pny wy\u0142\u0105cznie przez przycisk Demo na stronie startowej.
|
||||
demo.session.active=Tryb demo jest ju\u017c u\u017cywany przez innego u\u017cytkownika. Prosz\u0119 spr\u00f3bowa\u0107 ponownie p\u00f3\u017aniej.
|
||||
demo.start.error=Nie uda\u0142o si\u0119 uruchomi\u0107 trybu demo.
|
||||
start.button.createorder=Tworzenie zlece\u0144
|
||||
start.button.notifications=Powiadomienia
|
||||
start.button.nonotifications=Brak nowych powiadomie\u0144
|
||||
start.hero.description=Dla samozatrudnionych i ma\u0142ych przedsi\u0119biorc\u00f3w w bran\u017cy transportowej - w pe\u0142ni cyfrowo i kompleksowo. Skoncentruj si\u0119 na swoim biznesie, a my zajmiemy si\u0119 prac\u0105 biurow\u0105.
|
||||
start.hero.demo.hint=Demo uruchamia si\u0119 od razu z przygotowanymi danymi przyk\u0142adowymi.
|
||||
start.hero.trial.hint="Testuj teraz za darmo" tworzy Twoje w\u0142asne konto na bezp\u0142atny miesi\u0105c pr\u00f3bny.
|
||||
start.system.title=System
|
||||
start.system.intro=Dla samozatrudnionych i ma\u0142ych przedsi\u0119biorc\u00f3w w bran\u017cy transportowej kluczowe jest, aby mogli skupi\u0107 si\u0119 przede wszystkim na swoim w\u0142a\u015bciwym biznesie: pozyskiwaniu klient\u00f3w i dostarczaniu towar\u00f3w z punktu A do punktu B.
|
||||
start.feature.setup.title=Kreator konfiguracji
|
||||
|
||||
@@ -806,11 +806,17 @@ register.notification.failed=Регистрация не удалась: {0}
|
||||
# Start Page
|
||||
start.title=VotianLT - Ваш цифровой транспортный партнёр
|
||||
start.button.login=Войти
|
||||
start.button.demo=Demo
|
||||
start.button.register=Зарегистрироваться
|
||||
login.demo.only.button=Демо-доступ доступен только через кнопку Demo на стартовой странице.
|
||||
demo.session.active=Демо-режим уже используется другим пользователем. Пожалуйста, попробуйте позже.
|
||||
demo.start.error=Не удалось запустить демо-режим.
|
||||
start.button.createorder=Создание заказа
|
||||
start.button.notifications=Уведомления
|
||||
start.button.nonotifications=Нет новых уведомлений
|
||||
start.hero.description=Для индивидуальных предпринимателей и малого бизнеса в транспортной отрасли \u2013 полностью цифровое и комплексное решение. Сосредоточьтесь на вашем бизнесе, а мы позаботимся о бумажной работе.
|
||||
start.hero.demo.hint=Демо-режим запускается сразу с подготовленными примерными данными.
|
||||
start.hero.trial.hint="Попробовать бесплатно сейчас" создаёт ваш собственный аккаунт для бесплатного пробного месяца.
|
||||
start.system.title=Система
|
||||
start.system.intro=Для индивидуальных предпринимателей и малого бизнеса в транспортной отрасли крайне важно сосредоточиться прежде всего на своём основном деле: привлечении клиентов и доставке товаров из пункта А в пункт Б.
|
||||
start.feature.setup.title=Мастер настройки
|
||||
|
||||
@@ -806,11 +806,17 @@ register.notification.failed=Kay\u0131t ba\u015far\u0131s\u0131z: {0}
|
||||
# Start Page
|
||||
start.title=VotianLT - Dijital Ta\u015f\u0131mac\u0131l\u0131k Orta\u011f\u0131n\u0131z
|
||||
start.button.login=Giri\u015f Yap
|
||||
start.button.demo=Demo
|
||||
start.button.register=Kay\u0131t Ol
|
||||
login.demo.only.button=Demo eri\u015fimi yaln\u0131zca ba\u015flang\u0131\u00e7 sayfas\u0131ndaki Demo d\u00fc\u011fmesi \u00fczerinden kullan\u0131labilir.
|
||||
demo.session.active=Demo modu zaten ba\u015fka bir kullan\u0131c\u0131 taraf\u0131ndan kullan\u0131l\u0131yor. L\u00fctfen daha sonra tekrar deneyin.
|
||||
demo.start.error=Demo modu ba\u015flat\u0131lamad\u0131.
|
||||
start.button.createorder=\u0130\u015f Olu\u015ftur
|
||||
start.button.notifications=Bildirimler
|
||||
start.button.nonotifications=Yeni bildirim yok
|
||||
start.hero.description=Ta\u015f\u0131mac\u0131l\u0131k sekt\u00f6r\u00fcndeki bireysel giri\u015fimciler ve k\u00fc\u00e7\u00fck i\u015fletme sahipleri i\u00e7in - tamamen dijital ve b\u00fct\u00fcnle\u015fik. \u0130\u015finize odaklan\u0131n, b\u00fcro i\u015flerini biz halledelim.
|
||||
start.hero.demo.hint=Demo, \u00f6nceden haz\u0131rlanm\u0131\u015f \u00f6rnek verilerle hemen ba\u015flar.
|
||||
start.hero.trial.hint="Hemen \u00fccretsiz deneyin" \u00fccretsiz deneme ay\u0131 i\u00e7in kendi hesab\u0131n\u0131z\u0131 olu\u015fturur.
|
||||
start.system.title=Sistem
|
||||
start.system.intro=Ta\u015f\u0131mac\u0131l\u0131k sekt\u00f6r\u00fcndeki bireysel giri\u015fimciler ve k\u00fc\u00e7\u00fck i\u015fletme sahipleri i\u00e7in as\u0131l i\u015flerine odaklanabilmeleri b\u00fcy\u00fck \u00f6nem ta\u015f\u0131maktad\u0131r: m\u00fc\u015fteri kazanmak ve mallar\u0131 A'dan B'ye ta\u015f\u0131mak.
|
||||
start.feature.setup.title=Kurulum Sihirbaz\u0131
|
||||
|
||||
187
src/main/resources/templates/demo_invoice_template_default.json
Normal file
187
src/main/resources/templates/demo_invoice_template_default.json
Normal file
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"elements": [
|
||||
{
|
||||
"id": "demo-title",
|
||||
"type": "header",
|
||||
"text": "Rechnung",
|
||||
"xPercent": "7.56",
|
||||
"yPercent": "6.53",
|
||||
"widthPercent": "28.07",
|
||||
"heightPercent": "4.04",
|
||||
"fontSize": 24,
|
||||
"fontStyle": "bold",
|
||||
"color": "#111111"
|
||||
},
|
||||
{
|
||||
"id": "demo-number-label",
|
||||
"type": "text",
|
||||
"text": "Rechnungsnr.",
|
||||
"xPercent": "67.06",
|
||||
"yPercent": "7.48",
|
||||
"widthPercent": "17.98",
|
||||
"heightPercent": "2.97",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "bold",
|
||||
"color": "#111111"
|
||||
},
|
||||
{
|
||||
"id": "demo-number-value",
|
||||
"type": "text",
|
||||
"text": "000000",
|
||||
"xPercent": "84.03",
|
||||
"yPercent": "7.48",
|
||||
"widthPercent": "10.08",
|
||||
"heightPercent": "2.97",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"color": "#111111",
|
||||
"isStatic": true,
|
||||
"variable": "invoice.number"
|
||||
},
|
||||
{
|
||||
"id": "demo-date-label",
|
||||
"type": "text",
|
||||
"text": "Datum",
|
||||
"xPercent": "67.06",
|
||||
"yPercent": "11.52",
|
||||
"widthPercent": "17.98",
|
||||
"heightPercent": "2.97",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "bold",
|
||||
"color": "#111111"
|
||||
},
|
||||
{
|
||||
"id": "demo-date-value",
|
||||
"type": "text",
|
||||
"text": "2026-01-01",
|
||||
"xPercent": "84.03",
|
||||
"yPercent": "11.52",
|
||||
"widthPercent": "10.08",
|
||||
"heightPercent": "2.97",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"color": "#111111",
|
||||
"isStatic": true,
|
||||
"variable": "invoice.date"
|
||||
},
|
||||
{
|
||||
"id": "demo-sender",
|
||||
"type": "text",
|
||||
"text": null,
|
||||
"xPercent": "7.56",
|
||||
"yPercent": "16.51",
|
||||
"widthPercent": "30.08",
|
||||
"heightPercent": "2.97",
|
||||
"fontSize": 12,
|
||||
"fontStyle": "bold",
|
||||
"color": "#111111",
|
||||
"isStatic": true,
|
||||
"variable": "masterdata.contact_name"
|
||||
},
|
||||
{
|
||||
"id": "demo-customer",
|
||||
"type": "customer",
|
||||
"text": "Kundenname\nStraße Nr.\nPLZ Ort",
|
||||
"xPercent": "7.56",
|
||||
"yPercent": "25.06",
|
||||
"widthPercent": "33.95",
|
||||
"heightPercent": "12.00",
|
||||
"fontSize": 12,
|
||||
"fontStyle": "normal",
|
||||
"color": "#111111"
|
||||
},
|
||||
{
|
||||
"id": "demo-services",
|
||||
"type": "text",
|
||||
"text": "Leistungen",
|
||||
"xPercent": "7.56",
|
||||
"yPercent": "42.04",
|
||||
"widthPercent": "85.04",
|
||||
"heightPercent": "28.03",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"color": "#111111",
|
||||
"isStatic": true,
|
||||
"variable": "services.list"
|
||||
},
|
||||
{
|
||||
"id": "demo-net-label",
|
||||
"type": "text",
|
||||
"text": "Netto",
|
||||
"xPercent": "67.06",
|
||||
"yPercent": "73.99",
|
||||
"widthPercent": "11.93",
|
||||
"heightPercent": "2.97",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "bold",
|
||||
"color": "#111111"
|
||||
},
|
||||
{
|
||||
"id": "demo-net-value",
|
||||
"type": "text",
|
||||
"text": "0,00 €",
|
||||
"xPercent": "80.00",
|
||||
"yPercent": "73.99",
|
||||
"widthPercent": "13.95",
|
||||
"heightPercent": "2.97",
|
||||
"fontSize": 11,
|
||||
"fontStyle": "normal",
|
||||
"color": "#111111",
|
||||
"isStatic": true,
|
||||
"variable": "invoice.net_total"
|
||||
},
|
||||
{
|
||||
"id": "demo-total-label",
|
||||
"type": "text",
|
||||
"text": "Gesamt",
|
||||
"xPercent": "67.06",
|
||||
"yPercent": "78.98",
|
||||
"widthPercent": "11.93",
|
||||
"heightPercent": "3.44",
|
||||
"fontSize": 13,
|
||||
"fontStyle": "bold",
|
||||
"color": "#111111"
|
||||
},
|
||||
{
|
||||
"id": "demo-total-value",
|
||||
"type": "text",
|
||||
"text": "0,00 €",
|
||||
"xPercent": "80.00",
|
||||
"yPercent": "78.98",
|
||||
"widthPercent": "13.95",
|
||||
"heightPercent": "3.44",
|
||||
"fontSize": 13,
|
||||
"fontStyle": "bold",
|
||||
"color": "#111111",
|
||||
"isStatic": true,
|
||||
"variable": "invoice.gross_total"
|
||||
},
|
||||
{
|
||||
"id": "demo-template-note",
|
||||
"type": "text",
|
||||
"text": "Hinweis: Dieses Rechnungstemplate kann vom Benutzer ",
|
||||
"xPercent": "0.00",
|
||||
"yPercent": "20.78",
|
||||
"widthPercent": "100.00",
|
||||
"heightPercent": "4.51",
|
||||
"fontSize": 20,
|
||||
"fontStyle": "normal",
|
||||
"textAlign": "center",
|
||||
"color": "#555555"
|
||||
},
|
||||
{
|
||||
"id": "element-1",
|
||||
"type": "text",
|
||||
"text": "frei angepasst werden.",
|
||||
"xPercent": "31.93",
|
||||
"yPercent": "24.94",
|
||||
"widthPercent": "25.21",
|
||||
"heightPercent": "3.56",
|
||||
"fontSize": 20,
|
||||
"color": "#333333",
|
||||
"isStatic": false,
|
||||
"isCustomer": false,
|
||||
"variable": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package de.assecutor.votianlt.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.JobStatus;
|
||||
import de.assecutor.votianlt.model.User;
|
||||
import de.assecutor.votianlt.model.InvoiceTemplate;
|
||||
import de.assecutor.votianlt.pages.domain.CustomerRepository;
|
||||
import de.assecutor.votianlt.repository.AppUserRepository;
|
||||
import de.assecutor.votianlt.repository.BarcodeRepository;
|
||||
import de.assecutor.votianlt.repository.CargoItemRepository;
|
||||
import de.assecutor.votianlt.repository.CommentRepository;
|
||||
import de.assecutor.votianlt.repository.CustomerInvoiceRepository;
|
||||
import de.assecutor.votianlt.repository.InvoiceTemplateRepository;
|
||||
import de.assecutor.votianlt.repository.JobHistoryRepository;
|
||||
import de.assecutor.votianlt.repository.JobRepository;
|
||||
import de.assecutor.votianlt.repository.LocationPositionRepository;
|
||||
import de.assecutor.votianlt.repository.MessageRepository;
|
||||
import de.assecutor.votianlt.repository.PhotoRepository;
|
||||
import de.assecutor.votianlt.repository.ServiceRepository;
|
||||
import de.assecutor.votianlt.repository.SignatureRepository;
|
||||
import de.assecutor.votianlt.repository.TaskRepository;
|
||||
import de.assecutor.votianlt.repository.TaskTemplateRepository;
|
||||
import de.assecutor.votianlt.repository.UserInvoiceDataRepository;
|
||||
import de.assecutor.votianlt.repository.UserRepository;
|
||||
import de.assecutor.votianlt.security.SessionAuthenticationService;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DemoModeServiceTest {
|
||||
|
||||
@Mock
|
||||
private SessionAuthenticationService sessionAuthenticationService;
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
@Mock
|
||||
private CustomerRepository customerRepository;
|
||||
@Mock
|
||||
private JobRepository jobRepository;
|
||||
@Mock
|
||||
private CargoItemRepository cargoItemRepository;
|
||||
@Mock
|
||||
private TaskRepository taskRepository;
|
||||
@Mock
|
||||
private JobHistoryRepository jobHistoryRepository;
|
||||
@Mock
|
||||
private CustomerInvoiceRepository customerInvoiceRepository;
|
||||
@Mock
|
||||
private ServiceRepository serviceRepository;
|
||||
@Mock
|
||||
private AppUserRepository appUserRepository;
|
||||
@Mock
|
||||
private TaskTemplateRepository taskTemplateRepository;
|
||||
@Mock
|
||||
private InvoiceTemplateRepository invoiceTemplateRepository;
|
||||
@Mock
|
||||
private UserInvoiceDataRepository userInvoiceDataRepository;
|
||||
@Mock
|
||||
private MessageRepository messageRepository;
|
||||
@Mock
|
||||
private LocationPositionRepository locationPositionRepository;
|
||||
@Mock
|
||||
private PhotoRepository photoRepository;
|
||||
@Mock
|
||||
private SignatureRepository signatureRepository;
|
||||
@Mock
|
||||
private BarcodeRepository barcodeRepository;
|
||||
@Mock
|
||||
private CommentRepository commentRepository;
|
||||
@Mock
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<List<Job>> jobsCaptor;
|
||||
@Captor
|
||||
private ArgumentCaptor<List<de.assecutor.votianlt.model.Service>> servicesCaptor;
|
||||
@Captor
|
||||
private ArgumentCaptor<InvoiceTemplate> invoiceTemplateCaptor;
|
||||
|
||||
private DemoSessionRegistry demoSessionRegistry;
|
||||
private DemoModeService demoModeService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
demoSessionRegistry = new DemoSessionRegistry();
|
||||
demoModeService = new DemoModeService(demoSessionRegistry, sessionAuthenticationService, userRepository,
|
||||
customerRepository, jobRepository, cargoItemRepository, taskRepository, jobHistoryRepository,
|
||||
customerInvoiceRepository, serviceRepository, appUserRepository, taskTemplateRepository,
|
||||
invoiceTemplateRepository, userInvoiceDataRepository, messageRepository, locationPositionRepository,
|
||||
photoRepository, signatureRepository, barcodeRepository, commentRepository, passwordEncoder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tryPrepareDemoSessionSeedsTwoDemoJobs() {
|
||||
User demoUser = new User();
|
||||
demoUser.setId(new ObjectId());
|
||||
|
||||
when(userRepository.findByEmail(DemoModeService.DEMO_USERNAME)).thenReturn(Optional.of(demoUser));
|
||||
when(passwordEncoder.encode(DemoModeService.DEMO_PASSWORD)).thenReturn("encoded-demo");
|
||||
when(customerRepository.findByOwner(demoUser.getId())).thenReturn(List.of());
|
||||
when(jobRepository.findByCreatedBy(demoUser.getId().toHexString())).thenReturn(List.of());
|
||||
when(appUserRepository.findByErstelltVon(demoUser.getId())).thenReturn(List.of());
|
||||
when(taskTemplateRepository.findByUserIdOrderByTemplateNameAsc(demoUser.getId())).thenReturn(List.of());
|
||||
when(customerInvoiceRepository.findByUserId(demoUser.getId().toHexString())).thenReturn(List.of());
|
||||
when(customerRepository.saveAll(anyList())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(serviceRepository.saveAll(anyList())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(jobRepository.saveAll(anyList())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
boolean prepared = demoModeService.tryPrepareDemoSession("demo-session");
|
||||
|
||||
assertThat(prepared).isTrue();
|
||||
verify(serviceRepository).saveAll(servicesCaptor.capture());
|
||||
assertThat(servicesCaptor.getValue()).hasSize(2);
|
||||
verify(invoiceTemplateRepository).save(invoiceTemplateCaptor.capture());
|
||||
assertThat(invoiceTemplateCaptor.getValue().getUserId()).isEqualTo(demoUser.getId().toHexString());
|
||||
assertThat(invoiceTemplateCaptor.getValue().getTemplateData())
|
||||
.contains("elements")
|
||||
.contains("services.list")
|
||||
.contains("invoice.gross_total")
|
||||
.contains("frei angepasst werden")
|
||||
.contains("textAlign")
|
||||
.contains("center")
|
||||
.contains("fontSize")
|
||||
.contains("20");
|
||||
verify(jobRepository).saveAll(jobsCaptor.capture());
|
||||
assertThat(jobsCaptor.getValue()).hasSize(2);
|
||||
assertThat(jobsCaptor.getValue()).extracting(Job::getJobNumber)
|
||||
.containsExactly("DEMO-AKTIV-001", "DEMO-ERLEDIGT-001");
|
||||
assertThat(jobsCaptor.getValue()).extracting(Job::getStatus)
|
||||
.containsExactly(JobStatus.IN_PROGRESS, JobStatus.COMPLETED);
|
||||
assertThat(jobsCaptor.getValue()).allSatisfy(job -> {
|
||||
assertThat(job.getServiceIds()).hasSize(1);
|
||||
assertThat(job.getSelectedServices()).hasSize(1);
|
||||
assertThat(job.getSelectedServices().get(0).getDeliveryStationOrder()).isEqualTo(0);
|
||||
assertThat(job.getSelectedServices().get(0).getServiceId()).isEqualTo(job.getServiceIds().get(0));
|
||||
});
|
||||
assertThat(demoSessionRegistry.isHeldBy("demo-session")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanupAndReleaseIfOwnedSkipsForeignSessions() {
|
||||
demoSessionRegistry.acquire("active-session");
|
||||
|
||||
demoModeService.cleanupAndReleaseIfOwned("other-session");
|
||||
|
||||
verify(userRepository, never()).findByEmail(anyString());
|
||||
assertThat(demoSessionRegistry.isHeldBy("active-session")).isTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package de.assecutor.votianlt.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class DemoSessionRegistryTest {
|
||||
|
||||
private final DemoSessionRegistry registry = new DemoSessionRegistry();
|
||||
|
||||
@Test
|
||||
void acquireAllowsSameSessionButBlocksOtherSessions() {
|
||||
assertThat(registry.acquire("session-a")).isTrue();
|
||||
assertThat(registry.acquire("session-a")).isTrue();
|
||||
assertThat(registry.acquire("session-b")).isFalse();
|
||||
assertThat(registry.isHeldBy("session-a")).isTrue();
|
||||
assertThat(registry.isHeldBy("session-b")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void releaseFreesLockForNextSession() {
|
||||
assertThat(registry.acquire("session-a")).isTrue();
|
||||
|
||||
registry.release("session-a");
|
||||
|
||||
assertThat(registry.acquire("session-b")).isTrue();
|
||||
assertThat(registry.isHeldBy("session-b")).isTrue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user