Erweiterungen

This commit is contained in:
2026-02-19 19:43:22 +01:00
parent 0aced91206
commit 00811cdc36
46 changed files with 3221 additions and 949 deletions

View File

@@ -0,0 +1,64 @@
package de.assecutor.votianlt.config;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.server.ServiceInitEvent;
import com.vaadin.flow.server.VaadinServiceInitListener;
import de.assecutor.votianlt.model.Language;
import de.assecutor.votianlt.security.CustomUserPrincipal;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.Locale;
/**
* Sets the user's preferred locale on the UI BEFORE any layout or view is
* constructed. Registered via {@code UIInitListener} → {@code BeforeEnterListener},
* which fires prior to the router creating the layout component tree.
*/
@Component
@Slf4j
public class LocaleVaadinInitListener implements VaadinServiceInitListener {
@Override
public void serviceInit(ServiceInitEvent event) {
event.getSource().addUIInitListener(uiInitEvent -> {
UI ui = uiInitEvent.getUI();
ui.addBeforeEnterListener(beforeEnterEvent -> applyLocaleFromCurrentUser(ui));
});
}
private void applyLocaleFromCurrentUser(UI ui) {
try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated() || auth instanceof AnonymousAuthenticationToken) {
return;
}
if (!(auth.getPrincipal() instanceof CustomUserPrincipal cup)) {
return;
}
Language language = cup.getUser().getLanguage();
if (language == null) {
return;
}
Locale targetLocale = getLocaleFromLanguage(language);
if (!targetLocale.equals(ui.getLocale())) {
ui.setLocale(targetLocale);
log.debug("Locale set to {} for user {}", targetLocale, cup.getUsername());
}
} catch (Exception e) {
log.debug("Could not apply locale from user preferences: {}", e.getMessage());
}
}
private Locale getLocaleFromLanguage(Language language) {
return switch (language) {
case DE -> Locale.GERMAN;
case EN -> Locale.ENGLISH;
case FR -> Locale.FRENCH;
case ES -> Locale.of("es", "ES");
};
}
}

View File

@@ -0,0 +1,57 @@
package de.assecutor.votianlt.config;
import com.vaadin.flow.i18n.I18NProvider;
import de.assecutor.votianlt.model.Language;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
@Component
public class TranslationProvider implements I18NProvider {
public static final String BUNDLE_PREFIX = "messages";
@Override
public List<Locale> getProvidedLocales() {
return Collections.unmodifiableList(Arrays.asList(
Locale.GERMAN,
Locale.ENGLISH,
Locale.FRENCH,
Locale.of("es", "ES")
));
}
@Override
public String getTranslation(String key, Locale locale, Object... params) {
if (key == null) {
return "";
}
try {
ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_PREFIX, locale);
String value = bundle.getString(key);
if (params.length > 0) {
value = String.format(value, params);
}
return value;
} catch (Exception e) {
return key;
}
}
public String getTranslation(String key, Language language) {
Locale locale = switch (language) {
case DE -> Locale.GERMAN;
case EN -> Locale.ENGLISH;
case FR -> Locale.FRENCH;
case ES -> Locale.of("es", "ES");
};
return getTranslation(key, locale);
}
}

View File

@@ -0,0 +1,27 @@
package de.assecutor.votianlt.model;
public enum Language {
DE("Deutsch"),
EN("English"),
FR("Français"),
ES("Español");
private final String displayName;
Language(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
public static Language fromString(String text) {
for (Language language : Language.values()) {
if (language.name().equalsIgnoreCase(text)) {
return language;
}
}
return DE; // Default to German
}
}

View File

@@ -4,6 +4,7 @@ import lombok.Data;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.index.Indexed;
import java.time.LocalDateTime;
@@ -63,4 +64,8 @@ public class User {
// 2-Faktor-Authentifizierung (standardmäßig aktiviert für neue Nutzer)
private boolean twoFactorEnabled = true;
}
// Spracheinstellung (standardmäßig Deutsch)
@Field("language")
private Language language = Language.DE;
}

View File

@@ -28,7 +28,7 @@ public class BarcodeTask extends BaseTask {
@Override
public String getDisplayName() {
return "Barcode";
return TaskType.BARCODE.getDisplayName();
}
@Override

View File

@@ -28,7 +28,7 @@ public class CommentTask extends BaseTask {
@Override
public String getDisplayName() {
return "Kommentar";
return TaskType.COMMENT.getDisplayName();
}
@Override

View File

@@ -24,7 +24,7 @@ public class ConfirmationTask extends BaseTask {
@Override
public String getDisplayName() {
return "Bestätigung";
return TaskType.CONFIRMATION.getDisplayName();
}
@Override

View File

@@ -28,7 +28,7 @@ public class PhotoTask extends BaseTask {
@Override
public String getDisplayName() {
return "Foto";
return TaskType.PHOTO.getDisplayName();
}
@Override

View File

@@ -16,7 +16,7 @@ public class SignatureTask extends BaseTask {
@Override
public String getDisplayName() {
return "Unterschrift";
return TaskType.SIGNATURE.getDisplayName();
}
@Override

View File

@@ -1,20 +1,39 @@
package de.assecutor.votianlt.model.task;
import com.vaadin.flow.component.UI;
public enum TaskType {
CONFIRMATION("Bestätigung"),
SIGNATURE("Unterschrift"),
TODOLIST("To-Do Liste"),
PHOTO("Foto"),
BARCODE("Barcode"),
COMMENT("Kommentar");
CONFIRMATION("CONFIRMATION"),
SIGNATURE("SIGNATURE"),
TODOLIST("TODOLIST"),
PHOTO("PHOTO"),
BARCODE("BARCODE"),
COMMENT("COMMENT");
private final String displayName;
private final String translationKey;
TaskType(String displayName) {
this.displayName = displayName;
TaskType(String translationKey) {
this.translationKey = translationKey;
}
public String getDisplayName() {
return displayName;
// Fallback to German if UI not available
try {
if (UI.getCurrent() != null) {
return UI.getCurrent().getTranslation("tasktype." + translationKey);
}
} catch (Exception e) {
// Fallback to German if translation fails
}
// Fallback to German translations
return switch (this) {
case CONFIRMATION -> "Bestätigung";
case SIGNATURE -> "Unterschrift";
case TODOLIST -> "To-Do Liste";
case PHOTO -> "Foto";
case BARCODE -> "Barcode";
case COMMENT -> "Kommentar";
};
}
}

View File

@@ -26,7 +26,7 @@ public class TodoListTask extends BaseTask {
@Override
public String getDisplayName() {
return "To-Do Liste";
return TaskType.TODOLIST.getDisplayName();
}
@Override

View File

@@ -28,7 +28,10 @@ import de.assecutor.votianlt.model.UserInvoiceData;
import de.assecutor.votianlt.pages.service.AppUserService;
import de.assecutor.votianlt.pages.service.UserInvoiceDataService;
import de.assecutor.votianlt.pages.view.EditProfileView;
import de.assecutor.votianlt.model.Language;
import de.assecutor.votianlt.config.TranslationProvider;
import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.service.LanguageService;
import de.assecutor.votianlt.service.MessageBadgeUpdateService;
import de.assecutor.votianlt.service.MessageService;
import lombok.extern.slf4j.Slf4j;
@@ -36,6 +39,7 @@ import lombok.extern.slf4j.Slf4j;
import static com.vaadin.flow.theme.lumo.LumoUtility.*;
import java.util.List;
import java.util.Locale;
@AnonymousAllowed
@Slf4j
@@ -47,6 +51,7 @@ public final class MainLayout extends AppLayout {
private final MessageService messageService;
private final MessageBadgeUpdateService messageBadgeUpdateService;
private final AppUserService appUserService;
private final LanguageService languageService;
private Div headerRef;
private Scroller navRef;
private Component userMenuRef;
@@ -56,12 +61,13 @@ public final class MainLayout extends AppLayout {
public MainLayout(SecurityService securityService, UserInvoiceDataService userInvoiceDataService,
MessageService messageService, MessageBadgeUpdateService messageBadgeUpdateService,
AppUserService appUserService) {
AppUserService appUserService, LanguageService languageService) {
this.securityService = securityService;
this.userInvoiceDataService = userInvoiceDataService;
this.messageService = messageService;
this.messageBadgeUpdateService = messageBadgeUpdateService;
this.appUserService = appUserService;
this.languageService = languageService;
setPrimarySection(Section.DRAWER);
// Always build the drawer; keep references and toggle visibility on attach and
@@ -114,7 +120,7 @@ public final class MainLayout extends AppLayout {
// Create Details component for "Verwaltung" with collapsible list
Details verwaltungDetails = new Details();
verwaltungDetails.setSummaryText("Verwaltung");
verwaltungDetails.setSummaryText(getTranslation("nav.management"));
verwaltungDetails.addClassNames(Margin.Horizontal.MEDIUM, FontSize.MEDIUM, FontWeight.MEDIUM, "#000000");
// Create collapsible content with navigation items
@@ -123,16 +129,16 @@ public final class MainLayout extends AppLayout {
verwaltungContent.setSpacing(true);
// Create navigation items for the collapsible list
SideNavItem jobs = new SideNavItem("Aufträge", "jobs", new Icon(VaadinIcon.LIST));
SideNavItem customers = new SideNavItem("Kunden", "customers", new Icon(VaadinIcon.USERS));
SideNavItem appUsers = new SideNavItem("App-Nutzer", "app-user", new Icon(VaadinIcon.USERS));
SideNavItem statistics = new SideNavItem("Statistiken", "statistics", new Icon(VaadinIcon.BAR_CHART));
SideNavItem jobs = new SideNavItem(getTranslation("nav.jobs"), "jobs", new Icon(VaadinIcon.LIST));
SideNavItem customers = new SideNavItem(getTranslation("nav.customers"), "customers", new Icon(VaadinIcon.USERS));
SideNavItem appUsers = new SideNavItem(getTranslation("nav.appusers"), "app-user", new Icon(VaadinIcon.USERS));
SideNavItem statistics = new SideNavItem(getTranslation("nav.statistics"), "statistics", new Icon(VaadinIcon.BAR_CHART));
verwaltungContent.add(jobs, customers, appUsers);
verwaltungContent.add(jobs, customers, appUsers, statistics);
// Only show invoices menu if billing is enabled for the current user
if (isBillingEnabledForCurrentUser()) {
SideNavItem invoices = new SideNavItem("Rechnungen", "invoices", new Icon(VaadinIcon.FILE_TEXT));
SideNavItem invoices = new SideNavItem(getTranslation("nav.invoices"), "invoices", new Icon(VaadinIcon.FILE_TEXT));
verwaltungContent.add(invoices);
}
@@ -141,7 +147,7 @@ public final class MainLayout extends AppLayout {
// Create Details component for "Verwaltung" with collapsible list
Details userDetails = new Details();
userDetails.setSummaryText("Benutzer");
userDetails.setSummaryText(getTranslation("nav.users"));
userDetails.addClassNames(Margin.Horizontal.MEDIUM, FontSize.MEDIUM, FontWeight.MEDIUM, TextColor.BODY);
// Create collapsible content with navigation items
@@ -150,9 +156,9 @@ public final class MainLayout extends AppLayout {
userContent.setSpacing(true);
// Create navigation items for the collapsible list
SideNavItem profile = new SideNavItem("Mein Profil", "edit-profile", new Icon(VaadinIcon.USER));
SideNavItem myInvoices = new SideNavItem("Meine Rechnungen", "my-invoices", new Icon(VaadinIcon.FILE_TEXT));
SideNavItem imprint = new SideNavItem("Impressum", "impressum", new Icon(VaadinIcon.INFO_CIRCLE));
SideNavItem profile = new SideNavItem(getTranslation("nav.profile"), "edit-profile", new Icon(VaadinIcon.USER));
SideNavItem myInvoices = new SideNavItem(getTranslation("nav.myinvoices"), "my-invoices", new Icon(VaadinIcon.FILE_TEXT));
SideNavItem imprint = new SideNavItem(getTranslation("nav.imprint"), "impressum", new Icon(VaadinIcon.INFO_CIRCLE));
userContent.add(profile, myInvoices, imprint);
userDetails.add(userContent);
@@ -261,9 +267,9 @@ public final class MainLayout extends AppLayout {
userMenuItem.add(userNameSpan);
// Profil anzeigen mit Navigation
userMenuItem.getSubMenu().addItem("Profil anzeigen", e -> UI.getCurrent().navigate(EditProfileView.class));
userMenuItem.getSubMenu().addItem("Einstellungen");
userMenuItem.getSubMenu().addItem("Abmelden", e -> securityService.logout());
userMenuItem.getSubMenu().addItem(getTranslation("nav.showprofile"), e -> UI.getCurrent().navigate(EditProfileView.class));
userMenuItem.getSubMenu().addItem(getTranslation("nav.settings"));
userMenuItem.getSubMenu().addItem(getTranslation("nav.logout"), e -> securityService.logout());
// Update-Funktion für Benutzername und Avatar
Runnable updateUserInfo = () -> {
@@ -298,6 +304,9 @@ public final class MainLayout extends AppLayout {
super.onAttach(attachEvent);
UI ui = attachEvent.getUI();
// Apply user's preferred language immediately after login
applyUserLanguagePreference();
// Update badge immediately when layout is attached
updateMessagesBadge();
@@ -309,6 +318,42 @@ public final class MainLayout extends AppLayout {
});
}
/**
* Applies the user's preferred language if it differs from the current UI locale.
* The primary locale setup happens in {@code LocaleVaadinInitListener} before the
* layout is constructed. This method handles edge cases such as language changes
* within an active session.
*/
private void applyUserLanguagePreference() {
try {
if (securityService.isUserLoggedIn()) {
User currentUser = securityService.getCurrentDatabaseUser();
if (currentUser != null && currentUser.getLanguage() != null) {
UI ui = UI.getCurrent();
if (ui != null) {
Locale targetLocale = getLocaleFromLanguage(currentUser.getLanguage());
if (!targetLocale.equals(ui.getLocale())) {
ui.setLocale(targetLocale);
log.info("Applied locale {} for user {}", targetLocale, currentUser.getEmail());
}
}
}
}
} catch (Exception e) {
log.error("Error applying user language preference: {}", e.getMessage(), e);
}
}
private Locale getLocaleFromLanguage(Language language) {
return switch (language) {
case DE -> Locale.GERMAN;
case EN -> Locale.ENGLISH;
case FR -> Locale.FRENCH;
case ES -> new Locale("es", "ES");
default -> Locale.GERMAN;
};
}
@Override
protected void onDetach(DetachEvent detachEvent) {
if (badgeUpdateRegistration != null) {
@@ -317,4 +362,4 @@ public final class MainLayout extends AppLayout {
}
super.onDetach(detachEvent);
}
}
}

View File

@@ -14,28 +14,27 @@ import com.vaadin.flow.component.textfield.PasswordField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.AppUser;
import de.assecutor.votianlt.pages.service.AppUserService;
import jakarta.annotation.security.RolesAllowed;
import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("Neuen App-Nutzer anlegen")
@Route(value = "add-app-user", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({ "USER", "ADMIN" })
public class AddAppUserView extends VerticalLayout {
public class AddAppUserView extends VerticalLayout implements HasDynamicTitle {
private final AppUserService appUserService;
private final Binder<AppUser> binder = new Binder<>(AppUser.class);
// Form fields
private final TextField designationField = new TextField("Bezeichnung (HH H 000)");
private final TextField firstnameField = new TextField("Vorname");
private final TextField lastnameField = new TextField("Nachname");
private final TextField phoneField = new TextField("Telefon (Mobil)");
private final TextField emailField = new TextField("E-Mail-Adresse");
private final PasswordField passwordField = new PasswordField("Passwort");
private final PasswordField confirmPasswordField = new PasswordField("Passwort wiederholen");
// Form fields - labels set in constructor
private final TextField designationField = new TextField();
private final TextField firstnameField = new TextField();
private final TextField lastnameField = new TextField();
private final TextField phoneField = new TextField();
private final TextField emailField = new TextField();
private final PasswordField passwordField = new PasswordField();
private final PasswordField confirmPasswordField = new PasswordField();
@Autowired
public AddAppUserView(AppUserService appUserService) {
@@ -44,6 +43,15 @@ public class AddAppUserView extends VerticalLayout {
setPadding(true);
setSpacing(true);
// Set field labels via i18n
designationField.setLabel(getTranslation("addappuser.designation"));
firstnameField.setLabel(getTranslation("profile.firstname"));
lastnameField.setLabel(getTranslation("profile.lastname"));
phoneField.setLabel(getTranslation("addappuser.phone"));
emailField.setLabel(getTranslation("profile.email"));
passwordField.setLabel(getTranslation("addappuser.password"));
confirmPasswordField.setLabel(getTranslation("addappuser.password.confirm"));
// Center content vertically
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
@@ -63,10 +71,10 @@ public class AddAppUserView extends VerticalLayout {
header.setAlignItems(FlexComponent.Alignment.CENTER);
header.setSpacing(true);
H2 title = new H2("Neuen App-Nutzer anlegen");
H2 title = new H2(getTranslation("addappuser.title"));
title.getStyle().set("margin", "0");
Button backButton = new Button("Zurück", new Icon(VaadinIcon.ARROW_LEFT));
Button backButton = new Button(getTranslation("button.back"), new Icon(VaadinIcon.ARROW_LEFT));
backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
backButton.addClickListener(e -> navigateBack());
@@ -82,15 +90,15 @@ public class AddAppUserView extends VerticalLayout {
designationField.setPlaceholder("(HH H 000)");
designationField.setWidthFull();
designationField.setRequiredIndicatorVisible(true);
designationField.addBlurListener(e -> validateField(designationField, "Kennung ist ein Pflichtfeld"));
designationField.addBlurListener(e -> validateField(designationField, getTranslation("addappuser.validation.designation")));
firstnameField.setWidthFull();
firstnameField.setRequiredIndicatorVisible(true);
firstnameField.addBlurListener(e -> validateField(firstnameField, "Vorname ist ein Pflichtfeld"));
firstnameField.addBlurListener(e -> validateField(firstnameField, getTranslation("profile.validation.firstname")));
lastnameField.setWidthFull();
lastnameField.setRequiredIndicatorVisible(true);
lastnameField.addBlurListener(e -> validateField(lastnameField, "Nachname ist ein Pflichtfeld"));
lastnameField.addBlurListener(e -> validateField(lastnameField, getTranslation("profile.validation.lastname")));
// Create horizontal layout for firstname and lastname
HorizontalLayout nameLayout = new HorizontalLayout();
@@ -100,7 +108,7 @@ public class AddAppUserView extends VerticalLayout {
phoneField.setWidthFull();
phoneField.setRequiredIndicatorVisible(true);
phoneField.addBlurListener(e -> validateField(phoneField, "Telefonnummer ist ein Pflichtfeld"));
phoneField.addBlurListener(e -> validateField(phoneField, getTranslation("addappuser.validation.phone")));
emailField.setWidthFull();
emailField.setRequiredIndicatorVisible(true);
@@ -132,7 +140,7 @@ public class AddAppUserView extends VerticalLayout {
contentContainer.add(formLayout);
// Submit button
Button submitButton = new Button("App-Nutzer anlegen", e -> createAppUser());
Button submitButton = new Button(getTranslation("addappuser.button.submit"), e -> createAppUser());
submitButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
submitButton.setWidthFull();
contentContainer.add(submitButton);
@@ -154,13 +162,13 @@ public class AddAppUserView extends VerticalLayout {
binder.forField(lastnameField).bind(AppUser::getNachname, AppUser::setNachname);
binder.forField(phoneField).bind(AppUser::getTelefon, AppUser::setTelefon);
binder.forField(emailField).bind(AppUser::getEmail, AppUser::setEmail);
binder.forField(passwordField).asRequired("Passwort ist erforderlich").bind(AppUser::getPassword,
binder.forField(passwordField).asRequired(getTranslation("addappuser.validation.password.required")).bind(AppUser::getPassword,
AppUser::setPassword);
// Confirm password field validation
binder.forField(confirmPasswordField).asRequired("Passwort wiederholen ist erforderlich")
binder.forField(confirmPasswordField).asRequired(getTranslation("addappuser.validation.password.confirm"))
.withValidator(confirmPassword -> confirmPassword.equals(passwordField.getValue()),
"Passwörter stimmen nicht überein")
getTranslation("addappuser.validation.password.mismatch"))
.bind(appUser -> "", // Dummy getter - this field is not stored
(appUser, value) -> {
} // Dummy setter - this field is not stored
@@ -170,7 +178,7 @@ public class AddAppUserView extends VerticalLayout {
private void createAppUser() {
// Validate all fields first
if (!validateAllFields()) {
Notification.show("Bitte füllen Sie alle Pflichtfelder korrekt aus", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("addappuser.notification.validation"), 3000, Notification.Position.MIDDLE);
return;
}
@@ -183,26 +191,26 @@ public class AddAppUserView extends VerticalLayout {
appUserService.createAppUser(newAppUser);
// Show success message
Notification.show("App-Nutzer erfolgreich angelegt", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("addappuser.notification.success"), 3000, Notification.Position.MIDDLE);
// Navigate back to app user list
navigateBack();
} catch (ValidationException e) {
Notification.show("Bitte überprüfen Sie die Eingaben", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("addappuser.notification.check"), 3000, Notification.Position.MIDDLE);
} catch (org.springframework.dao.DuplicateKeyException e) {
// Handle duplicate email error
if (e.getMessage().contains("email")) {
Notification.show("Diese E-Mail-Adresse ist bereits vergeben", 5000, Notification.Position.MIDDLE);
Notification.show(getTranslation("addappuser.notification.email.duplicate"), 5000, Notification.Position.MIDDLE);
emailField.focus();
emailField.setInvalid(true);
emailField.setErrorMessage("E-Mail-Adresse bereits vorhanden");
emailField.setErrorMessage(getTranslation("addappuser.notification.email.duplicate"));
} else {
Notification.show("Ein Fehler ist aufgetreten: Doppelter Wert gefunden", 5000,
Notification.show(getTranslation("addappuser.notification.check"), 5000,
Notification.Position.MIDDLE);
}
} catch (Exception e) {
Notification.show("Fehler beim Anlegen des App-Nutzers: " + e.getMessage(), 5000,
Notification.show(getTranslation("addappuser.notification.error", e.getMessage()), 5000,
Notification.Position.MIDDLE);
}
}
@@ -230,10 +238,10 @@ public class AddAppUserView extends VerticalLayout {
String value = emailField.getValue();
if (value == null || value.trim().isEmpty()) {
emailField.setInvalid(true);
emailField.setErrorMessage("E-Mail-Adresse ist ein Pflichtfeld");
emailField.setErrorMessage(getTranslation("addappuser.validation.email.required"));
} else if (!value.contains("@") || !value.contains(".")) {
emailField.setInvalid(true);
emailField.setErrorMessage("Bitte geben Sie eine gültige E-Mail-Adresse ein");
emailField.setErrorMessage(getTranslation("addappuser.validation.email.invalid"));
} else {
emailField.setInvalid(false);
emailField.setErrorMessage("");
@@ -244,10 +252,10 @@ public class AddAppUserView extends VerticalLayout {
String value = passwordField.getValue();
if (value == null || value.trim().isEmpty()) {
passwordField.setInvalid(true);
passwordField.setErrorMessage("Passwort ist ein Pflichtfeld");
passwordField.setErrorMessage(getTranslation("addappuser.validation.password.required"));
} else if (value.length() < 6) {
passwordField.setInvalid(true);
passwordField.setErrorMessage("Passwort muss mindestens 6 Zeichen lang sein");
passwordField.setErrorMessage(getTranslation("addappuser.validation.password.min"));
} else {
passwordField.setInvalid(false);
passwordField.setErrorMessage("");
@@ -259,10 +267,10 @@ public class AddAppUserView extends VerticalLayout {
String confirmPassword = confirmPasswordField.getValue();
if (confirmPassword == null || confirmPassword.trim().isEmpty()) {
confirmPasswordField.setInvalid(true);
confirmPasswordField.setErrorMessage("Bitte bestätigen Sie das Passwort");
confirmPasswordField.setErrorMessage(getTranslation("addappuser.validation.password.confirm"));
} else if (!confirmPassword.equals(password)) {
confirmPasswordField.setInvalid(true);
confirmPasswordField.setErrorMessage("Passwörter stimmen nicht überein");
confirmPasswordField.setErrorMessage(getTranslation("addappuser.validation.password.mismatch"));
} else {
confirmPasswordField.setInvalid(false);
confirmPasswordField.setErrorMessage("");
@@ -270,10 +278,10 @@ public class AddAppUserView extends VerticalLayout {
}
private boolean validateAllFields() {
validateField(designationField, "Kennung ist ein Pflichtfeld");
validateField(firstnameField, "Vorname ist ein Pflichtfeld");
validateField(lastnameField, "Nachname ist ein Pflichtfeld");
validateField(phoneField, "Telefonnummer ist ein Pflichtfeld");
validateField(designationField, getTranslation("addappuser.validation.designation"));
validateField(firstnameField, getTranslation("profile.validation.firstname"));
validateField(lastnameField, getTranslation("profile.validation.lastname"));
validateField(phoneField, getTranslation("addappuser.validation.phone"));
validateEmailField();
validatePasswordField();
validateConfirmPasswordField();
@@ -282,4 +290,9 @@ public class AddAppUserView extends VerticalLayout {
&& !phoneField.isInvalid() && !emailField.isInvalid() && !passwordField.isInvalid()
&& !confirmPasswordField.isInvalid();
}
@Override
public String getPageTitle() {
return getTranslation("page.title.appuser.create");
}
}

View File

@@ -8,7 +8,7 @@ import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.votianlt.model.Company;
@@ -19,11 +19,10 @@ import jakarta.annotation.security.RolesAllowed;
import java.time.Clock;
@Route(value = "add_company", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@PageTitle("Neuen Firma anlegen")
// @Menu(order = 0, icon = "vaadin:clipboard-check", title = "Neue Firma
// anlegen")
@RolesAllowed("USER")
public class AddCompanyView extends Main {
public class AddCompanyView extends Main implements HasDynamicTitle {
private final AddCompanyService addCompanyService;
TextField companyName;
@@ -44,24 +43,24 @@ public class AddCompanyView extends Main {
public AddCompanyView(AddCompanyService addCompanyService, Clock clock) {
this.addCompanyService = addCompanyService;
companyName = new TextField("Firmenname");
companyName = new TextField(getTranslation("profile.company"));
companyName.setRequiredIndicatorVisible(true);
binder.forField(companyName).asRequired("Firmenname ist ein Pflichtfeld") // Pflichtfeldmeldung
binder.forField(companyName).asRequired(getTranslation("profile.validation.company.required"))
.bind(Company::getName, Company::setName);
firstName = new TextField("Vorname");
lastName = new TextField("Nachname");
telephone = new TextField("Telefonnummer");
fax = new TextField("Faxnummer");
mail = new TextField("E-Mail-Adresse");
street = new TextField("Straße");
houseNumber = new TextField("Hausnummer");
addressAddition = new TextField("Adresszusatz");
zip = new TextField("Postleitzahl");
city = new TextField("Stadt");
firstName = new TextField(getTranslation("profile.firstname"));
lastName = new TextField(getTranslation("profile.lastname"));
telephone = new TextField(getTranslation("profile.phone"));
fax = new TextField(getTranslation("profile.fax"));
mail = new TextField(getTranslation("profile.email"));
street = new TextField(getTranslation("profile.street"));
houseNumber = new TextField(getTranslation("profile.housenr"));
addressAddition = new TextField(getTranslation("profile.addressadd"));
zip = new TextField(getTranslation("profile.zip"));
city = new TextField(getTranslation("profile.city"));
// Setze den Button als primär
submitButton = new Button("Kunden anlegen", event -> submit());
submitButton = new Button(getTranslation("addcompany.button.submit"), event -> submit());
submitButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
// Erstelle ein Div als Container (oder direkt ein Layout)
@@ -80,7 +79,7 @@ public class AddCompanyView extends Main {
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
add(new ViewToolbar("Neuen Kunden anlegen"));
add(new ViewToolbar(getTranslation("addcompany.title")));
add(formLayout);
}
@@ -97,4 +96,9 @@ public class AddCompanyView extends Main {
System.err.println("Validierungsfehler: " + e.getMessage());
}
}
@Override
public String getPageTitle() {
return getTranslation("page.title.company.create");
}
}

View File

@@ -10,7 +10,7 @@ import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.votianlt.model.Customer;
@@ -21,11 +21,10 @@ import jakarta.annotation.security.RolesAllowed;
import java.time.Clock;
@Route(value = "add-customer", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@PageTitle("Neuen Kunden anlegen")
// @Menu(order = 0, icon = "vaadin:clipboard-check", title = "Neuen Kunden
// anlegen")
@RolesAllowed("USER")
public class AddCustomerView extends Main {
public class AddCustomerView extends Main implements HasDynamicTitle {
private final AddCustomerService addCustomerService;
TextField companyName;
@@ -48,66 +47,66 @@ public class AddCustomerView extends Main {
this.addCustomerService = todoService;
// Firma (Pflichtfeld)
companyName = new TextField("Firma");
companyName = new TextField(getTranslation("profile.company"));
companyName.setRequiredIndicatorVisible(true);
companyName.setWidthFull();
companyName.addBlurListener(e -> validateField(companyName));
// Anrede (Dropdown)
title = new ComboBox<>("Anrede");
title.setItems("Herr", "Frau", "Divers");
title.setPlaceholder("Anrede");
title = new ComboBox<>(getTranslation("addjob.address.salutation"));
title.setItems(getTranslation("addjob.salutation.mr"), getTranslation("addjob.salutation.ms"), getTranslation("addjob.salutation.other"));
title.setPlaceholder(getTranslation("addjob.address.salutation.placeholder"));
title.setWidthFull();
// Vorname (Pflichtfeld)
firstName = new TextField("Vorname");
firstName = new TextField(getTranslation("profile.firstname"));
firstName.setRequiredIndicatorVisible(true);
firstName.setWidthFull();
firstName.addBlurListener(e -> validateField(firstName));
// Nachname (Pflichtfeld)
lastName = new TextField("Nachname");
lastName = new TextField(getTranslation("profile.lastname"));
lastName.setRequiredIndicatorVisible(true);
lastName.setWidthFull();
lastName.addBlurListener(e -> validateField(lastName));
// Telefonnummer (Pflichtfeld)
telephone = new TextField("Telefonnummer");
telephone = new TextField(getTranslation("profile.phone"));
telephone.setRequiredIndicatorVisible(true);
telephone.setWidthFull();
telephone.addBlurListener(e -> validateField(telephone));
// Fax (optional)
fax = new TextField("Fax");
fax = new TextField(getTranslation("profile.fax"));
fax.setWidthFull();
// E-Mail (Pflichtfeld)
mail = new TextField("E-Mail-Adresse");
mail = new TextField(getTranslation("profile.email"));
mail.setRequiredIndicatorVisible(true);
mail.setWidthFull();
mail.addBlurListener(e -> validateEmail());
// Straße (Pflichtfeld)
street = new TextField("Straße");
street = new TextField(getTranslation("profile.street"));
street.setRequiredIndicatorVisible(true);
street.addBlurListener(e -> validateField(street));
// Hausnummer (Pflichtfeld)
houseNumber = new TextField("Hausnr.");
houseNumber = new TextField(getTranslation("profile.housenr"));
houseNumber.setRequiredIndicatorVisible(true);
houseNumber.addBlurListener(e -> validateField(houseNumber));
// Adresszusatz (optional)
addressAddition = new TextField("Adresszusatz");
addressAddition = new TextField(getTranslation("profile.addressadd"));
addressAddition.setWidthFull();
// PLZ (Pflichtfeld)
zip = new TextField("Postleitzahl");
zip = new TextField(getTranslation("profile.zip"));
zip.setRequiredIndicatorVisible(true);
zip.addBlurListener(e -> validateField(zip));
// Ort (Pflichtfeld)
city = new TextField("Ort");
city = new TextField(getTranslation("profile.city"));
city.setRequiredIndicatorVisible(true);
city.addBlurListener(e -> validateField(city));
@@ -118,7 +117,7 @@ public class AddCustomerView extends Main {
setTestData();
// Setze den Button als primär
submitButton = new Button("Kunden anlegen", event -> submit());
submitButton = new Button(getTranslation("addcustomer.button.submit"), event -> submit());
submitButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
submitButton.setWidthFull();
@@ -156,41 +155,41 @@ public class AddCustomerView extends Main {
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
add(new ViewToolbar("Neuen Kunden anlegen"));
add(new ViewToolbar(getTranslation("addcustomer.title")));
add(container);
}
private void configureBinder() {
binder.forField(companyName).asRequired("Firma ist ein Pflichtfeld").bind(Customer::getCompanyName,
binder.forField(companyName).asRequired(getTranslation("profile.validation.company.required")).bind(Customer::getCompanyName,
Customer::setCompanyName);
binder.forField(title).bind(Customer::getTitle, Customer::setTitle);
binder.forField(firstName).asRequired("Vorname ist ein Pflichtfeld").bind(Customer::getFirstname,
binder.forField(firstName).asRequired(getTranslation("profile.validation.firstname.required")).bind(Customer::getFirstname,
Customer::setFirstname);
binder.forField(lastName).asRequired("Nachname ist ein Pflichtfeld").bind(Customer::getLastName,
binder.forField(lastName).asRequired(getTranslation("profile.validation.lastname.required")).bind(Customer::getLastName,
Customer::setLastName);
binder.forField(telephone).asRequired("Telefonnummer ist ein Pflichtfeld").bind(Customer::getTelephone,
binder.forField(telephone).asRequired(getTranslation("profile.validation.phone")).bind(Customer::getTelephone,
Customer::setTelephone);
binder.forField(fax).bind(Customer::getFax, Customer::setFax);
binder.forField(mail).asRequired("E-Mail-Adresse ist ein Pflichtfeld")
.withValidator(email -> email.contains("@"), "Bitte geben Sie eine gültige E-Mail-Adresse ein")
binder.forField(mail).asRequired(getTranslation("profile.validation.email.required"))
.withValidator(email -> email.contains("@"), getTranslation("profile.validation.email.invalid"))
.bind(Customer::getMail, Customer::setMail);
binder.forField(street).asRequired("Straße ist ein Pflichtfeld").bind(Customer::getStreet, Customer::setStreet);
binder.forField(street).asRequired(getTranslation("profile.validation.street.required")).bind(Customer::getStreet, Customer::setStreet);
binder.forField(houseNumber).asRequired("Hausnummer ist ein Pflichtfeld").bind(Customer::getHouseNumber,
binder.forField(houseNumber).asRequired(getTranslation("profile.validation.housenr.required")).bind(Customer::getHouseNumber,
Customer::setHouseNumber);
binder.forField(addressAddition).bind(Customer::getAddressAddition, Customer::setAddressAddition);
binder.forField(zip).asRequired("Postleitzahl ist ein Pflichtfeld").bind(Customer::getZip, Customer::setZip);
binder.forField(zip).asRequired(getTranslation("profile.validation.zip.required")).bind(Customer::getZip, Customer::setZip);
binder.forField(city).asRequired("Ort ist ein Pflichtfeld").bind(Customer::getCity, Customer::setCity);
binder.forField(city).asRequired(getTranslation("profile.validation.city.required")).bind(Customer::getCity, Customer::setCity);
}
private void setTestData() {
@@ -202,7 +201,7 @@ public class AddCustomerView extends Main {
boolean isValid = validateAllFields();
if (!isValid) {
com.vaadin.flow.component.notification.Notification.show("Bitte füllen Sie alle Pflichtfelder aus", 3000,
com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.validation"), 3000,
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
return;
}
@@ -213,17 +212,16 @@ public class AddCustomerView extends Main {
addCustomerService.addCustomer(customer);
// Erfolg anzeigen und zur Kundenliste navigieren
com.vaadin.flow.component.notification.Notification.show("Kunde erfolgreich angelegt", 3000,
com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.success"), 3000,
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
getUI().ifPresent(ui -> ui.navigate("customers"));
} catch (ValidationException e) {
com.vaadin.flow.component.notification.Notification.show("Bitte überprüfen Sie Ihre Eingaben", 3000,
com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.check"), 3000,
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
} catch (Exception e) {
com.vaadin.flow.component.notification.Notification.show("Fehler beim Speichern: " + e.getMessage(), 5000,
com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.error", e.getMessage()), 5000,
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
}
}
@@ -232,7 +230,7 @@ public class AddCustomerView extends Main {
String value = field.getValue();
if (value == null || value.trim().isEmpty()) {
field.setInvalid(true);
field.setErrorMessage("Dieses Feld ist ein Pflichtfeld");
field.setErrorMessage(getTranslation("addcustomer.validation.required"));
} else {
field.setInvalid(false);
field.setErrorMessage("");
@@ -243,10 +241,10 @@ public class AddCustomerView extends Main {
String value = mail.getValue();
if (value == null || value.trim().isEmpty()) {
mail.setInvalid(true);
mail.setErrorMessage("E-Mail-Adresse ist ein Pflichtfeld");
mail.setErrorMessage(getTranslation("profile.email.required"));
} else if (!value.contains("@")) {
mail.setInvalid(true);
mail.setErrorMessage("Bitte geben Sie eine gültige E-Mail-Adresse ein");
mail.setErrorMessage(getTranslation("profile.validation.email.invalid"));
} else {
mail.setInvalid(false);
mail.setErrorMessage("");
@@ -268,4 +266,9 @@ public class AddCustomerView extends Main {
&& !mail.isInvalid() && !street.isInvalid() && !houseNumber.isInvalid() && !zip.isInvalid()
&& !city.isInvalid();
}
@Override
public String getPageTitle() {
return getTranslation("page.title.customer.create");
}
}

View File

@@ -27,7 +27,7 @@ import com.vaadin.flow.router.Menu;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.tabs.TabSheet;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
import org.springframework.security.core.Authentication;
@@ -69,11 +69,15 @@ import java.util.Objects;
import java.util.Optional;
@Route(value = "add_job", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@PageTitle("Neuen Auftrag anlegen")
@Menu(order = 0, icon = "vaadin:clipboard-check", title = "Auftragserstellung")
@RolesAllowed("USER")
@Slf4j
public class AddJobView extends Main {
public class AddJobView extends Main implements HasDynamicTitle {
@Override
public String getPageTitle() {
return getTranslation("page.title.job.create");
}
private final AddJobService addJobService;
private final CustomerService customerService;
@@ -204,8 +208,8 @@ public class AddJobView extends Main {
private void initializeComponents() {
// Customer selection
customerSelection = new ComboBox<>("Auftraggeber/Rechnungsempfänger");
customerSelection.setPlaceholder("Wählen Sie einen Auftraggeber aus...");
customerSelection = new ComboBox<>(getTranslation("addjob.customer.label"));
customerSelection.setPlaceholder(getTranslation("addjob.customer.placeholder"));
customerSelection.setWidthFull();
customerSelection.setRequiredIndicatorVisible(true);
// Mit Kunden des angemeldeten Benutzers befüllen und Mapping aufbauen
@@ -219,7 +223,7 @@ public class AddJobView extends Main {
: ((c.getFirstname() != null ? c.getFirstname() : "") + " "
+ (c.getLastName() != null ? c.getLastName() : "")).trim();
if (label.isBlank()) {
label = "Unbenannter Kunde";
label = getTranslation("addjob.customer.unnamed");
}
// Bei Duplikaten Label einzigartig machen
String uniqueLabel = label;
@@ -308,78 +312,78 @@ public class AddJobView extends Main {
}
});
preloadAddressButton = new Button("Vorbelegte Adressfelder leeren");
preloadAddressButton = new Button(getTranslation("addjob.button.clearfields"));
preloadAddressButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
preloadAddressButton.addClickListener(event -> clearAllFields());
// Pickup address
pickupCompany = new ComboBox<>("Firma");
pickupCompany.setPlaceholder("Firmenname");
pickupCompany = new ComboBox<>(getTranslation("profile.company"));
pickupCompany.setPlaceholder(getTranslation("addjob.address.company.placeholder"));
pickupCompany.setAllowCustomValue(true);
setupCompanyAutocomplete(pickupCompany, true); // true für Pickup
pickupSalutation = new ComboBox<>("Anrede");
pickupSalutation.setItems("Herr", "Frau", "Divers");
pickupSalutation.setPlaceholder("Anrede wählen...");
pickupFirstName = new TextField("Vorname");
pickupFirstName.setPlaceholder("Vorname");
pickupSalutation = new ComboBox<>(getTranslation("addjob.address.salutation"));
pickupSalutation.setItems(getTranslation("addjob.salutation.mr"), getTranslation("addjob.salutation.ms"), getTranslation("addjob.salutation.other"));
pickupSalutation.setPlaceholder(getTranslation("addjob.address.salutation.placeholder"));
pickupFirstName = new TextField(getTranslation("profile.firstname"));
pickupFirstName.setPlaceholder(getTranslation("profile.firstname"));
pickupFirstName.setRequiredIndicatorVisible(true);
pickupLastName = new TextField("Nachname");
pickupLastName.setPlaceholder("Nachname");
pickupLastName = new TextField(getTranslation("profile.lastname"));
pickupLastName.setPlaceholder(getTranslation("profile.lastname"));
pickupLastName.setRequiredIndicatorVisible(true);
pickupPhone = new TextField("Telefonnummer");
pickupPhone.setPlaceholder("Telefonnummer");
pickupStreet = new TextField("Straße");
pickupStreet.setPlaceholder("Musterstraße");
pickupPhone = new TextField(getTranslation("profile.phone"));
pickupPhone.setPlaceholder(getTranslation("profile.phone"));
pickupStreet = new TextField(getTranslation("profile.street"));
pickupStreet.setPlaceholder(getTranslation("addjob.address.street.placeholder"));
pickupStreet.setRequiredIndicatorVisible(true);
pickupHouseNumber = new TextField("Hausnummer");
pickupHouseNumber.setPlaceholder("Hausnummer");
pickupHouseNumber = new TextField(getTranslation("addjob.address.housenumber"));
pickupHouseNumber.setPlaceholder(getTranslation("addjob.address.housenumber"));
pickupHouseNumber.setRequiredIndicatorVisible(true);
pickupAddressAddition = new TextField("Adresszusatz");
pickupAddressAddition.setPlaceholder("2. OG, Hinterhaus...");
pickupZip = new TextField("Postleitzahl");
pickupZip.setPlaceholder("Postleitzahl");
pickupAddressAddition = new TextField(getTranslation("profile.addressadd"));
pickupAddressAddition.setPlaceholder(getTranslation("addjob.address.addition.placeholder"));
pickupZip = new TextField(getTranslation("profile.zip"));
pickupZip.setPlaceholder(getTranslation("profile.zip"));
pickupZip.setRequiredIndicatorVisible(true);
pickupCity = new TextField("Ort");
pickupCity.setPlaceholder("Hamburg");
pickupCity = new TextField(getTranslation("addjob.address.city"));
pickupCity.setPlaceholder(getTranslation("addjob.address.city.placeholder.pickup"));
pickupCity.setRequiredIndicatorVisible(true);
savePickupAddress = new Checkbox("Die Adresse für zukünftige Aufträge speichern.");
savePickupAddress = new Checkbox(getTranslation("addjob.address.save"));
savePickupAddress.setValue(true);
// Delivery address
deliveryCompany = new ComboBox<>("Firma");
deliveryCompany.setPlaceholder("Firmenname");
deliveryCompany = new ComboBox<>(getTranslation("profile.company"));
deliveryCompany.setPlaceholder(getTranslation("addjob.address.company.placeholder"));
deliveryCompany.setAllowCustomValue(true);
setupCompanyAutocomplete(deliveryCompany, false); // false für Delivery
deliverySalutation = new ComboBox<>("Anrede");
deliverySalutation.setItems("Herr", "Frau", "Divers");
deliverySalutation.setPlaceholder("Anrede wählen...");
deliveryFirstName = new TextField("Vorname");
deliveryFirstName.setPlaceholder("Vorname");
deliverySalutation = new ComboBox<>(getTranslation("addjob.address.salutation"));
deliverySalutation.setItems(getTranslation("addjob.salutation.mr"), getTranslation("addjob.salutation.ms"), getTranslation("addjob.salutation.other"));
deliverySalutation.setPlaceholder(getTranslation("addjob.address.salutation.placeholder"));
deliveryFirstName = new TextField(getTranslation("profile.firstname"));
deliveryFirstName.setPlaceholder(getTranslation("profile.firstname"));
deliveryFirstName.setRequiredIndicatorVisible(true);
deliveryLastName = new TextField("Nachname");
deliveryLastName.setPlaceholder("Nachname");
deliveryLastName = new TextField(getTranslation("profile.lastname"));
deliveryLastName.setPlaceholder(getTranslation("profile.lastname"));
deliveryLastName.setRequiredIndicatorVisible(true);
deliveryPhone = new TextField("Telefonnummer");
deliveryPhone.setPlaceholder("Telefonnummer");
deliveryStreet = new TextField("Straße");
deliveryStreet.setPlaceholder("Beispielweg");
deliveryPhone = new TextField(getTranslation("profile.phone"));
deliveryPhone.setPlaceholder(getTranslation("profile.phone"));
deliveryStreet = new TextField(getTranslation("profile.street"));
deliveryStreet.setPlaceholder(getTranslation("addjob.address.delivery.street.placeholder"));
deliveryStreet.setRequiredIndicatorVisible(true);
deliveryHouseNumber = new TextField("Hausnr");
deliveryHouseNumber.setPlaceholder("Hausnummer");
deliveryHouseNumber = new TextField(getTranslation("profile.housenr"));
deliveryHouseNumber.setPlaceholder(getTranslation("addjob.address.housenumber"));
deliveryHouseNumber.setRequiredIndicatorVisible(true);
deliveryAddressAddition = new TextField("Adresszusatz");
deliveryAddressAddition.setPlaceholder("Erdgeschoss, links...");
deliveryZip = new TextField("Postleitzahl");
deliveryZip.setPlaceholder("Postleitzahl");
deliveryAddressAddition = new TextField(getTranslation("profile.addressadd"));
deliveryAddressAddition.setPlaceholder(getTranslation("addjob.address.delivery.addition.placeholder"));
deliveryZip = new TextField(getTranslation("profile.zip"));
deliveryZip.setPlaceholder(getTranslation("profile.zip"));
deliveryZip.setRequiredIndicatorVisible(true);
deliveryCity = new TextField("Ort");
deliveryCity.setPlaceholder("Berlin");
deliveryCity = new TextField(getTranslation("addjob.address.city"));
deliveryCity.setPlaceholder(getTranslation("addjob.address.city.placeholder.delivery"));
deliveryCity.setRequiredIndicatorVisible(true);
saveDeliveryAddress = new Checkbox("Die Adresse für zukünftige Aufträge speichern.");
saveDeliveryAddress = new Checkbox(getTranslation("addjob.address.save"));
saveDeliveryAddress.setValue(true);
// Digital processing - set value based on user's profile setting
digitalProcessing = new Checkbox("Digitale Abwicklung per App");
digitalProcessing = new Checkbox(getTranslation("profile.settings.digitalprocess"));
// Get current user's digital processing preference from profile
try {
User currentUser = securityService.getCurrentDatabaseUser();
@@ -387,18 +391,18 @@ public class AddJobView extends Main {
} catch (Exception e) {
digitalProcessing.setValue(true); // Default to true if user not found
}
appUser = new ComboBox<>("App-Nutzer");
appUser = new ComboBox<>(getTranslation("addjob.appuser.label"));
// Load app users for current user and set up the ComboBox
availableAppUsers = appUserService.findByCurrentUser();
appUser.setItems(availableAppUsers);
appUser.setItemLabelGenerator(
user -> user.getVorname() + " " + user.getNachname() + " (" + user.getEmail() + ")");
appUser.setPlaceholder("App-Nutzer auswählen...");
appUser.setPlaceholder(getTranslation("addjob.appuser.placeholder"));
// Services grid will be initialized in createPriceAndSubmitTab()
// Date picker fields for appointments
pickupDate = new DatePicker("Datum");
pickupDate = new DatePicker(getTranslation("addjob.appointment.date"));
pickupDate.setRequiredIndicatorVisible(true);
pickupDate.setMin(LocalDate.now());
pickupDate.setLocale(java.util.Locale.GERMANY); // Monday as first day of week
@@ -409,7 +413,7 @@ public class AddJobView extends Main {
"Freitag", "Samstag"))
.setWeekdaysShort(java.util.Arrays.asList("So", "Mo", "Di", "Mi", "Do", "Fr", "Sa")));
deliveryDate = new DatePicker("Datum");
deliveryDate = new DatePicker(getTranslation("addjob.appointment.date"));
deliveryDate.setRequiredIndicatorVisible(true);
deliveryDate.setMin(LocalDate.now());
deliveryDate.setLocale(java.util.Locale.GERMANY); // Monday as first day of week
@@ -421,7 +425,7 @@ public class AddJobView extends Main {
.setWeekdaysShort(java.util.Arrays.asList("So", "Mo", "Di", "Mi", "Do", "Fr", "Sa")));
// Submit button - initially disabled until all required fields are valid
submitButton = new Button("Auftrag anlegen", event -> submit());
submitButton = new Button(getTranslation("addjob.button.submit"), event -> submit());
submitButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
submitButton.setEnabled(false);
@@ -436,7 +440,7 @@ public class AddJobView extends Main {
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
H2 title = new H2("Neuen Auftrag anlegen");
H2 title = new H2(getTranslation("addjob.title"));
add(title);
// Create TabSheet for organizing the form
@@ -445,16 +449,16 @@ public class AddJobView extends Main {
tabSheet.setSizeFull();
// Tab 1: Customer & Addresses
addressesTab = tabSheet.add("Auftraggeber & Adressen", createCustomerAndAddressesTab());
addressesTab = tabSheet.add(getTranslation("addjob.tab.addresses"), createCustomerAndAddressesTab());
// Tab 2: Appointments & Processing
appointmentsTab = tabSheet.add("Termine & Verarbeitung", createAppointmentsAndProcessingTab());
appointmentsTab = tabSheet.add(getTranslation("addjob.tab.appointments"), createAppointmentsAndProcessingTab());
// Tab 3: Cargo
cargoTab = tabSheet.add("Ladung", createCargoTab());
cargoTab = tabSheet.add(getTranslation("addjob.tab.cargo"), createCargoTab());
// Tab 4: Tasks
tasksTab = tabSheet.add("Aufgaben", createTasksTab());
tasksTab = tabSheet.add(getTranslation("addjob.tab.tasks"), createTasksTab());
// Disable tasks tab initially if digital processing is off
if (!Boolean.TRUE.equals(digitalProcessing.getValue())) {
@@ -466,7 +470,7 @@ public class AddJobView extends Main {
}
// Tab 5: Price & Submit
priceTab = tabSheet.add("Leistungen und Preis", createPriceAndSubmitTab());
priceTab = tabSheet.add(getTranslation("addjob.tab.price"), createPriceAndSubmitTab());
// Tab-Wechsel-Listener für Adressvalidierung
tabSheet.addSelectedChangeListener(this::onTabChange);
@@ -549,9 +553,9 @@ public class AddJobView extends Main {
content.add(digitalRow, appUser);
// Appointment (Pickup)
H3 pickupApptTitle = new H3("Termin (Abholung)");
H3 pickupApptTitle = new H3(getTranslation("addjob.appointment.pickup"));
pickupApptTitle.getStyle().set("margin", "0");
pickupTime = new TimePicker("Uhrzeit");
pickupTime = new TimePicker(getTranslation("addjob.appointment.time"));
pickupTime.setLocale(java.util.Locale.GERMANY);
HorizontalLayout pickupApptRow = new HorizontalLayout(pickupDate, pickupTime);
pickupApptRow.setWidthFull();
@@ -561,9 +565,9 @@ public class AddJobView extends Main {
content.add(pickupApptTitle, pickupApptRow);
// Appointment (Delivery)
H3 deliveryApptTitle = new H3("Termin (Lieferung)");
H3 deliveryApptTitle = new H3(getTranslation("addjob.appointment.delivery"));
deliveryApptTitle.getStyle().set("margin", "0");
deliveryTime = new TimePicker("Uhrzeit");
deliveryTime = new TimePicker(getTranslation("addjob.appointment.time"));
deliveryTime.setLocale(java.util.Locale.GERMANY);
HorizontalLayout deliveryApptRow = new HorizontalLayout(deliveryDate, deliveryTime);
deliveryApptRow.setWidthFull();
@@ -625,7 +629,7 @@ public class AddJobView extends Main {
routeInfoBox.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
routeInfoBox.setVisible(false); // Initial versteckt
H3 routeTitle = new H3("Streckeninformation");
H3 routeTitle = new H3(getTranslation("addjob.route.title"));
routeTitle.getStyle().set("margin", "0");
routeTitle.getStyle().set("color", "var(--lumo-primary-text-color)");
@@ -634,7 +638,7 @@ public class AddJobView extends Main {
routeRow.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
routeRow.setAlignItems(FlexComponent.Alignment.CENTER);
Span distanceLabel = new Span("Entfernung:");
Span distanceLabel = new Span(getTranslation("addjob.route.distance") + ":");
routeDistanceLabel = new Span("-");
routeDistanceLabel.getStyle().set("font-weight", "bold");
routeDistanceLabel.getStyle().set("font-size", "var(--lumo-font-size-l)");
@@ -647,7 +651,7 @@ public class AddJobView extends Main {
durationRow.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
durationRow.setAlignItems(FlexComponent.Alignment.CENTER);
Span durationLabel = new Span("Fahrtzeit:");
Span durationLabel = new Span(getTranslation("addjob.route.duration") + ":");
routeDurationLabel = new Span("-");
routeDurationLabel.getStyle().set("font-weight", "bold");
routeDurationLabel.getStyle().set("color", "var(--lumo-secondary-text-color)");
@@ -668,7 +672,7 @@ public class AddJobView extends Main {
manualRouteInputBox.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
manualRouteInputBox.setVisible(true); // Initial sichtbar
H3 manualRouteTitle = new H3("Streckeninformation (manuelle Eingabe)");
H3 manualRouteTitle = new H3(getTranslation("addjob.route.manual.title"));
manualRouteTitle.getStyle().set("margin", "0");
manualRouteTitle.getStyle().set("color", "var(--lumo-secondary-text-color)");
@@ -676,9 +680,9 @@ public class AddJobView extends Main {
manualInputRow.setWidthFull();
manualInputRow.setSpacing(true);
manualDistanceInput = new com.vaadin.flow.component.textfield.NumberField("Entfernung (km)");
manualDistanceInput = new com.vaadin.flow.component.textfield.NumberField(getTranslation("addjob.route.distance.km"));
manualDistanceInput.setWidthFull();
manualDistanceInput.setPlaceholder("z.B. 125,5");
manualDistanceInput.setPlaceholder(getTranslation("addjob.route.distance.placeholder"));
manualDistanceInput.setMin(0);
manualDistanceInput.setStep(0.1);
manualDistanceInput.setClearButtonVisible(true);
@@ -690,16 +694,16 @@ public class AddJobView extends Main {
}
});
manualDurationInput = new com.vaadin.flow.component.textfield.IntegerField("Fahrtzeit (Minuten)");
manualDurationInput = new com.vaadin.flow.component.textfield.IntegerField(getTranslation("addjob.route.duration.min"));
manualDurationInput.setWidthFull();
manualDurationInput.setPlaceholder("z.B. 90");
manualDurationInput.setPlaceholder(getTranslation("addjob.route.duration.placeholder"));
manualDurationInput.setMin(0);
manualDurationInput.setStep(1);
manualDurationInput.setClearButtonVisible(true);
manualInputRow.add(manualDistanceInput, manualDurationInput);
Span manualRouteHint = new Span("Bitte geben Sie die Entfernung und Fahrtzeit ein, da keine automatische Routenberechnung durchgeführt wurde.");
Span manualRouteHint = new Span(getTranslation("addjob.route.manual.hint"));
manualRouteHint.getStyle().set("font-size", "var(--lumo-font-size-s)");
manualRouteHint.getStyle().set("color", "var(--lumo-secondary-text-color)");
manualRouteHint.getStyle().set("font-style", "italic");
@@ -708,7 +712,7 @@ public class AddJobView extends Main {
content.add(manualRouteInputBox);
// Title
H3 servicesTitle = new H3("Leistungen");
H3 servicesTitle = new H3(getTranslation("addjob.services.title"));
servicesTitle.getStyle().set("margin", "0");
content.add(servicesTitle);
@@ -718,17 +722,17 @@ public class AddJobView extends Main {
servicesGrid.setHeight("250px");
servicesGrid.setItems(selectedServices);
servicesGrid.addColumn(Service::getName).setHeader("Leistung").setSortable(true);
servicesGrid.addColumn(Service::getName).setHeader(getTranslation("common.service")).setSortable(true);
servicesGrid.addColumn(service -> {
if (service.getCalculationBasis() != null) {
return switch (service.getCalculationBasis()) {
case DISTANCE -> "Gefahrene Kilometer";
case TIME -> "Zeit";
case FLAT_RATE -> "Pauschal";
case DISTANCE -> getTranslation("addjob.services.basis.distance");
case TIME -> getTranslation("addjob.services.basis.time");
case FLAT_RATE -> getTranslation("addjob.services.basis.flatrate");
};
}
return "";
}).setHeader("Berechnung").setSortable(true);
}).setHeader(getTranslation("addjob.services.calculation")).setSortable(true);
servicesGrid.addColumn(service -> {
// Get route distance for distance-based calculations (berechnet oder manuell)
Double routeDistance = getEffectiveRouteDistance();
@@ -738,18 +742,18 @@ public class AddJobView extends Main {
}
// Show price info if no route calculated yet
if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE && routeDistance == null) {
return service.getPricePerKilometer().setScale(2, RoundingMode.HALF_UP) + " €/km (Route fehlt)";
return service.getPricePerKilometer().setScale(2, RoundingMode.HALF_UP) + " €/km (" + getTranslation("addjob.services.route.missing") + ")";
}
return service.getEffectivePrice() != null
? service.getEffectivePrice().setScale(2, RoundingMode.HALF_UP) + ""
: "";
}).setHeader("Preis").setSortable(false);
}).setHeader(getTranslation("common.price")).setSortable(false);
servicesGrid.addColumn(service -> {
if (service.getVatRate() != null) {
return service.getVatRate().multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + " %";
}
return "";
}).setHeader("USt").setSortable(true);
}).setHeader(getTranslation("addjob.services.vat")).setSortable(true);
servicesGrid.addComponentColumn(service -> {
// Verbindliche Leistungen können nicht gelöscht werden
if (service.isMandatory()) {
@@ -766,12 +770,12 @@ public class AddJobView extends Main {
updateTabLabels();
});
return removeButton;
}).setHeader("Aktion").setAutoWidth(true).setFlexGrow(0);
}).setHeader(getTranslation("common.actions")).setAutoWidth(true).setFlexGrow(0);
content.add(servicesGrid);
// Add Service Button
Button addServiceButton = new Button("Leistung hinzufügen", new Icon(VaadinIcon.PLUS));
Button addServiceButton = new Button(getTranslation("addjob.services.add"), new Icon(VaadinIcon.PLUS));
addServiceButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addServiceButton.addClickListener(e -> openAddServiceDialog());
content.add(addServiceButton);
@@ -786,7 +790,7 @@ public class AddJobView extends Main {
summaryLayout.setWidthFull();
summaryLayout.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
H3 summaryTitle = new H3("Zusammenfassung");
H3 summaryTitle = new H3(getTranslation("addjob.summary.title"));
summaryTitle.getStyle().set("margin", "0");
summaryLayout.add(summaryTitle);
@@ -794,7 +798,7 @@ public class AddJobView extends Main {
HorizontalLayout netRow = new HorizontalLayout();
netRow.setWidthFull();
netRow.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
Span netLabel = new Span("Nettosumme:");
Span netLabel = new Span(getTranslation("addjob.summary.net") + ":");
netTotalLabel = new Span("0,00 €");
netTotalLabel.getStyle().set("font-weight", "bold");
netRow.add(netLabel, netTotalLabel);
@@ -804,7 +808,7 @@ public class AddJobView extends Main {
HorizontalLayout vatRow = new HorizontalLayout();
vatRow.setWidthFull();
vatRow.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
Span vatLabel = new Span("Umsatzsteuer:");
Span vatLabel = new Span(getTranslation("addjob.summary.vat") + ":");
vatTotalLabel = new Span("0,00 €");
vatTotalLabel.getStyle().set("font-weight", "bold");
vatRow.add(vatLabel, vatTotalLabel);
@@ -814,7 +818,7 @@ public class AddJobView extends Main {
HorizontalLayout grossRow = new HorizontalLayout();
grossRow.setWidthFull();
grossRow.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
Span grossLabel = new Span("Bruttosumme:");
Span grossLabel = new Span(getTranslation("addjob.summary.gross") + ":");
grossLabel.getStyle().set("font-size", "var(--lumo-font-size-l)");
grossTotalLabel = new Span("0,00 €");
grossTotalLabel.getStyle().set("font-size", "var(--lumo-font-size-l)");
@@ -831,7 +835,7 @@ public class AddJobView extends Main {
private void openAddServiceDialog() {
Dialog dialog = new Dialog();
dialog.setHeaderTitle("Leistung auswählen");
dialog.setHeaderTitle(getTranslation("addjob.services.dialog.title"));
dialog.setWidth("500px");
VerticalLayout dialogContent = new VerticalLayout();
@@ -842,7 +846,7 @@ public class AddJobView extends Main {
List<Service> availableServices = serviceRepository
.findByUserId(securityService.getCurrentDatabaseUser().getId().toString());
ComboBox<Service> serviceCombo = new ComboBox<>("Leistung");
ComboBox<Service> serviceCombo = new ComboBox<>(getTranslation("common.service"));
serviceCombo.setWidthFull();
serviceCombo.setItems(availableServices);
serviceCombo.setItemLabelGenerator(service -> {
@@ -853,7 +857,7 @@ public class AddJobView extends Main {
}
return service.getName();
});
serviceCombo.setPlaceholder("Leistung auswählen...");
serviceCombo.setPlaceholder(getTranslation("addjob.services.dialog.placeholder"));
serviceCombo.setRequired(true);
dialogContent.add(serviceCombo);
@@ -863,10 +867,10 @@ public class AddJobView extends Main {
buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
buttonLayout.setSpacing(true);
Button cancelButton = new Button("Abbrechen", e -> dialog.close());
Button cancelButton = new Button(getTranslation("button.cancel"), e -> dialog.close());
cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
Button addButton = new Button("Hinzufügen", e -> {
Button addButton = new Button(getTranslation("addjob.services.dialog.add"), e -> {
if (serviceCombo.getValue() != null) {
selectedServices.add(serviceCombo.getValue());
servicesGrid.getDataProvider().refreshAll();
@@ -956,7 +960,7 @@ public class AddJobView extends Main {
section.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
section.getStyle().set("background-color", "var(--lumo-base-color)");
H3 title = new H3("Abholadresse");
H3 title = new H3(getTranslation("addjob.section.pickup"));
title.getStyle().set("margin", "0");
HorizontalLayout titleLayout = new HorizontalLayout();
@@ -1016,7 +1020,7 @@ public class AddJobView extends Main {
section.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
section.getStyle().set("background-color", "var(--lumo-base-color)");
H3 title = new H3("Lieferadresse");
H3 title = new H3(getTranslation("addjob.section.delivery"));
title.getStyle().set("margin", "0");
HorizontalLayout titleLayout = new HorizontalLayout();
@@ -1198,12 +1202,12 @@ public class AddJobView extends Main {
// Bind date picker fields with validation
binder.forField(pickupDate).asRequired("")
.withValidator(date -> date == null || !date.isBefore(LocalDate.now()),
"Das Abholdatum darf nicht in der Vergangenheit liegen")
getTranslation("addjob.validation.pickupdate.future"))
.bind(Job::getPickupDate, Job::setPickupDate);
binder.forField(deliveryDate).asRequired("")
.withValidator(date -> date == null || !date.isBefore(LocalDate.now()),
"Das Lieferdatum darf nicht in der Vergangenheit liegen")
getTranslation("addjob.validation.deliverydate.future"))
.bind(Job::getDeliveryDate, Job::setDeliveryDate);
// Bind time picker fields (optional)
@@ -1243,7 +1247,7 @@ public class AddJobView extends Main {
boolean digital = Boolean.TRUE.equals(digitalProcessing.getValue());
boolean hasUser = selectedUserId != null && !selectedUserId.trim().isEmpty();
return !digital || hasUser;
}, "Bitte App-Nutzer auswählen, wenn Digitale Abwicklung aktiv ist")
}, getTranslation("addjob.validation.appuser.required"))
.bind(Job::getAppUser, Job::setAppUser);
// Toggle required indicator and visibility for App-Nutzer based on
@@ -1386,11 +1390,11 @@ public class AddJobView extends Main {
private void updateTabLabels() {
// Check validation state for each tab and update labels with exclamation marks
updateTabLabel(addressesTab, "Auftraggeber & Adressen", hasAddressValidationErrors());
updateTabLabel(appointmentsTab, "Termine & Verarbeitung", hasAppointmentValidationErrors());
updateTabLabel(cargoTab, "Ladung", hasCargoValidationErrors());
updateTabLabel(tasksTab, "Aufgaben", hasTasksValidationErrors());
updateTabLabel(priceTab, "Preis & Abschluss", hasPriceValidationErrors());
updateTabLabel(addressesTab, getTranslation("addjob.tab.addresses"), hasAddressValidationErrors());
updateTabLabel(appointmentsTab, getTranslation("addjob.tab.appointments"), hasAppointmentValidationErrors());
updateTabLabel(cargoTab, getTranslation("addjob.tab.cargo"), hasCargoValidationErrors());
updateTabLabel(tasksTab, getTranslation("addjob.tab.tasks"), hasTasksValidationErrors());
updateTabLabel(priceTab, getTranslation("addjob.tab.price"), hasPriceValidationErrors());
}
private void updateTabLabel(com.vaadin.flow.component.tabs.Tab tab, String baseLabel, boolean hasErrors) {
@@ -1530,7 +1534,7 @@ public class AddJobView extends Main {
// selected
if (digitalProcessing.getValue() && appUser.getValue() == null) {
Notification errorNotification = Notification.show(
"Wenn digitale Abwicklung per App aktiviert ist, muss ein App-Nutzer ausgewählt werden.");
getTranslation("addjob.validation.appuser.required"));
errorNotification.setDuration(5000);
return;
}
@@ -1543,7 +1547,7 @@ public class AddJobView extends Main {
if (cargoFilled.isEmpty()) {
Notification errorNotification = Notification
.show("Bitte fügen Sie mindestens eine Ladungszeile hinzu.");
.show(getTranslation("addjob.validation.cargo.required"));
errorNotification.setDuration(5000);
return;
}
@@ -1593,13 +1597,13 @@ public class AddJobView extends Main {
// Erfolgsmeldung und Navigation zur Zusammenfassung
Notification successNotification = Notification
.show("Auftrag erfolgreich erstellt! Auftragsnummer: " + savedJob.getJobNumber());
.show(getTranslation("addjob.notification.success", savedJob.getJobNumber()));
successNotification.setDuration(2000);
getUI().ifPresent(ui -> ui.navigate(JobSummaryView.class, savedJob.getId().toHexString()));
} else {
// Validation failed, show error message
Notification errorNotification = Notification
.show("Bitte füllen Sie alle Pflichtfelder aus (markiert mit *)");
.show(getTranslation("addjob.validation.required.fields"));
errorNotification.setDuration(5000);
}
@@ -1610,7 +1614,7 @@ public class AddJobView extends Main {
cargoError.setVisible(false);
if (cargoAreaContainer != null)
cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
Notification errorNotification = Notification.show("Fehler beim Erstellen des Auftrags: " + e.getMessage());
Notification errorNotification = Notification.show(getTranslation("addjob.notification.error", e.getMessage()));
errorNotification.setDuration(5000);
}
}
@@ -1658,7 +1662,7 @@ public class AddJobView extends Main {
// Benutzer informieren
Notification notification = Notification
.show("Entwurf wiederhergestellt. Sie können Ihre Arbeit fortsetzen.");
.show(getTranslation("addjob.notification.draft.restored"));
notification.setDuration(4000);
}
}
@@ -1690,8 +1694,8 @@ public class AddJobView extends Main {
cargoAreaContainer.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
cargoAreaContainer.getStyle().set("padding", "var(--lumo-space-m)");
H3 cargoTitle = new H3("Ladung");
cargoError = new Span("Bitte fügen Sie mindestens eine Ladungszeile hinzu.");
H3 cargoTitle = new H3(getTranslation("addjob.tab.cargo"));
cargoError = new Span(getTranslation("addjob.validation.cargo.required"));
cargoError.getStyle().set("color", "var(--lumo-error-text-color)");
cargoError.getStyle().set("font-size", "var(--lumo-font-size-s)");
cargoError.setVisible(false);
@@ -1709,34 +1713,33 @@ public class AddJobView extends Main {
row.setWidthFull();
row.setAlignItems(FlexComponent.Alignment.END);
ComboBox<String> desc = new ComboBox<>("Beschreibung");
desc.setItems("Europalette", "Einwegpalette", "sseldorfer-Palette", "Gitterboxpalette", "Gitterwagen",
"Paket");
ComboBox<String> desc = new ComboBox<>(getTranslation("addjob.cargo.description"));
desc.setItems(getTranslation("addjob.cargo.europalette"), getTranslation("addjob.cargo.disposablepalette"), getTranslation("addjob.cargo.dusseldorfpalette"), getTranslation("addjob.cargo.gridboxpalette"), getTranslation("addjob.cargo.gridcart"), getTranslation("addjob.cargo.parcel"));
desc.setAllowCustomValue(true);
desc.setPlaceholder("Wählen Sie eine Option oder geben Sie eigenen Text ein...");
desc.setPlaceholder(getTranslation("addjob.cargo.description.placeholder"));
desc.setWidth("40%");
desc.setRequiredIndicatorVisible(true);
IntegerField qty = new IntegerField("Anzahl");
IntegerField qty = new IntegerField(getTranslation("addjob.cargo.quantity"));
qty.setMin(1);
qty.setMax(9999); // Set reasonable maximum
qty.setWidth("10%");
qty.setRequiredIndicatorVisible(true);
NumberField weight = new NumberField("Gewicht");
NumberField weight = new NumberField(getTranslation("addjob.cargo.weight"));
weight.setSuffixComponent(new Span("kg"));
weight.setWidth("15%");
weight.setRequiredIndicatorVisible(true);
NumberField len = new NumberField("Länge");
NumberField len = new NumberField(getTranslation("addjob.cargo.length"));
len.setSuffixComponent(new Span("cm"));
len.setWidth("12%");
len.setRequiredIndicatorVisible(true);
NumberField wid = new NumberField("Breite");
NumberField wid = new NumberField(getTranslation("addjob.cargo.width"));
wid.setSuffixComponent(new Span("cm"));
wid.setWidth("12%");
wid.setRequiredIndicatorVisible(true);
NumberField hei = new NumberField("Höhe");
NumberField hei = new NumberField(getTranslation("addjob.cargo.height"));
hei.setSuffixComponent(new Span("cm"));
hei.setWidth("12%");
hei.setRequiredIndicatorVisible(true);
@@ -1846,7 +1849,7 @@ public class AddJobView extends Main {
}); // Show only one empty row by default
// Add button to add more cargo rows
Button addCargoButton = new Button("Ladung hinzufügen", new Icon(VaadinIcon.PLUS));
Button addCargoButton = new Button(getTranslation("addjob.cargo.add"), new Icon(VaadinIcon.PLUS));
addCargoButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addCargoButton.setWidthFull(); // Make button full width of container
addCargoButton.addClickListener(e -> addCargoRow.accept("", r -> {
@@ -1870,12 +1873,12 @@ public class AddJobView extends Main {
content.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH);
// Aufgabentitel mit Template-Auswahl
H3 tasksTitle = new H3("Zu quittierende Aufgaben");
H3 tasksTitle = new H3(getTranslation("addjob.tasks.title"));
tasksTitle.getStyle().set("margin", "0");
tasksTitle.getStyle().set("white-space", "nowrap");
templateComboBox = new ComboBox<>();
templateComboBox.setPlaceholder("Template auswählen...");
templateComboBox.setPlaceholder(getTranslation("addjob.tasks.template.placeholder"));
templateComboBox.setItemLabelGenerator(TaskTemplate::getTemplateName);
templateComboBox.setClearButtonVisible(true);
// Breite auf verbleibenden Platz einstellen
@@ -1894,7 +1897,7 @@ public class AddJobView extends Main {
// Icon-Button zum Speichern als Template
Button saveAsTemplateBtn = new Button(new Icon(VaadinIcon.BOOKMARK));
saveAsTemplateBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
saveAsTemplateBtn.setTooltipText("Aufgaben als Template speichern");
saveAsTemplateBtn.setTooltipText(getTranslation("addjob.tasks.template.save.tooltip"));
saveAsTemplateBtn.addClickListener(e -> saveTasksAsTemplate());
HorizontalLayout titleWithTemplate = new HorizontalLayout(tasksTitle, templateComboBox, saveAsTemplateBtn);
@@ -1914,17 +1917,17 @@ public class AddJobView extends Main {
// 1 Beispielzeile
addTask.accept(null);
Button addTaskBtn = new Button("Aufgabe hinzufügen", new Icon(VaadinIcon.PLUS));
Button addTaskBtn = new Button(getTranslation("addjob.tasks.add"), new Icon(VaadinIcon.PLUS));
addTaskBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addTaskBtn.addClickListener(e -> addTask.accept(null));
content.add(tasksList, addTaskBtn);
// Bemerkung
H3 remarksTitle = new H3("Bemerkung");
H3 remarksTitle = new H3(getTranslation("addjob.tasks.remark"));
remarksTitle.getStyle().set("margin", "0");
remarkArea = new TextArea();
remarkArea.setPlaceholder("z.B. rückwärtigen Liefereingang benutzen o. ä.");
remarkArea.setPlaceholder(getTranslation("addjob.tasks.remark.placeholder"));
remarkArea.setWidthFull();
remarkArea.setMinHeight("180px");
content.add(remarksTitle, remarkArea);
@@ -1998,7 +2001,7 @@ public class AddJobView extends Main {
updatePriceSummary();
// Benutzer-Feedback
Notification.show("Alle Felder wurden geleert", 2000, Notification.Position.BOTTOM_CENTER);
Notification.show(getTranslation("addjob.notification.cleared"), 2000, Notification.Position.BOTTOM_CENTER);
}
/**
@@ -2097,10 +2100,10 @@ public class AddJobView extends Main {
taskContainer.getStyle().set("position", "relative");
// Task type selection
ComboBox<TaskType> taskTypeCombo = new ComboBox<>("Aufgabentyp");
ComboBox<TaskType> taskTypeCombo = new ComboBox<>(getTranslation("addjob.tasks.tasktype"));
taskTypeCombo.setItems(TaskType.values());
taskTypeCombo.setItemLabelGenerator(TaskType::getDisplayName);
taskTypeCombo.setPlaceholder("Aufgabentyp wählen...");
taskTypeCombo.setPlaceholder(getTranslation("addjob.tasks.tasktype.placeholder"));
taskTypeCombo.setWidthFull();
// Configuration container for dynamic fields
@@ -2226,8 +2229,8 @@ public class AddJobView extends Main {
switch (taskType) {
case CONFIRMATION:
// Description field (required)
TextField descriptionField = new TextField("Beschreibung");
descriptionField.setPlaceholder("Beschreibung der Aufgabe...");
TextField descriptionField = new TextField(getTranslation("addjob.tasks.description"));
descriptionField.setPlaceholder(getTranslation("addjob.tasks.description.placeholder"));
descriptionField.setWidthFull();
descriptionField.setRequiredIndicatorVisible(true);
descriptionField.setValue(task.getDescription() != null ? task.getDescription() : "");
@@ -2252,8 +2255,8 @@ public class AddJobView extends Main {
}
// Button text field (required)
TextField buttonTextField = new TextField("Button-Text");
buttonTextField.setPlaceholder("z.B. 'Bestätigen', 'Abgeschlossen'");
TextField buttonTextField = new TextField(getTranslation("addjob.tasks.buttontext"));
buttonTextField.setPlaceholder(getTranslation("addjob.tasks.buttontext.placeholder"));
buttonTextField.setWidthFull();
buttonTextField.setRequiredIndicatorVisible(true);
ConfirmationTask confirmationTask = (ConfirmationTask) task;
@@ -2287,7 +2290,7 @@ public class AddJobView extends Main {
case SIGNATURE:
// No additional configuration needed
Span info = new Span("Keine zusätzliche Konfiguration erforderlich");
Span info = new Span(getTranslation("addjob.tasks.signature.noconfig"));
info.getStyle().set("color", "var(--lumo-secondary-text-color)");
info.getStyle().set("font-style", "italic");
configContainer.add(info);
@@ -2298,7 +2301,7 @@ public class AddJobView extends Main {
todoContainer.setPadding(false);
todoContainer.setSpacing(true);
H3 todoTitle = new H3("To-Do Punkte");
H3 todoTitle = new H3(getTranslation("addjob.tasks.todolist.title"));
todoTitle.getStyle().set("margin", "0");
todoContainer.add(todoTitle);
@@ -2325,7 +2328,7 @@ public class AddJobView extends Main {
todoRow.setAlignItems(FlexComponent.Alignment.END);
TextField todoField = new TextField();
todoField.setPlaceholder("To-Do Punkt");
todoField.setPlaceholder(getTranslation("addjob.tasks.todolist.item.placeholder"));
todoField.setWidth("100%");
todoField.setRequiredIndicatorVisible(true);
// Initial red styling for empty field
@@ -2348,7 +2351,7 @@ public class AddJobView extends Main {
});
};
Button addTodoBtn = new Button("To-Do Punkt hinzufügen", new Icon(VaadinIcon.PLUS));
Button addTodoBtn = new Button(getTranslation("addjob.tasks.todolist.add"), new Icon(VaadinIcon.PLUS));
addTodoBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
addTodoBtn.addClickListener(e -> addTodoItem.accept(null));
@@ -2363,7 +2366,7 @@ public class AddJobView extends Main {
todoRow.setAlignItems(FlexComponent.Alignment.END);
TextField todoField = new TextField();
todoField.setPlaceholder("To-Do Punkt");
todoField.setPlaceholder(getTranslation("addjob.tasks.todolist.item.placeholder"));
todoField.setWidth("100%");
todoField.setRequiredIndicatorVisible(true);
todoField.setValue(todoText != null ? todoText : ""); // Set the saved text
@@ -2405,12 +2408,12 @@ public class AddJobView extends Main {
photoLayout.setSpacing(true);
PhotoTask photoTask = (PhotoTask) task;
IntegerField minPhotos = new IntegerField("Min. Anzahl Fotos");
IntegerField minPhotos = new IntegerField(getTranslation("addjob.tasks.photo.min"));
minPhotos.setPlaceholder("1");
minPhotos.setMin(1);
minPhotos.setValue(photoTask.getMinPhotoCount() != null ? photoTask.getMinPhotoCount() : 1);
IntegerField maxPhotos = new IntegerField("Max. Anzahl Fotos");
IntegerField maxPhotos = new IntegerField(getTranslation("addjob.tasks.photo.max"));
maxPhotos.setPlaceholder("10");
maxPhotos.setMin(1);
maxPhotos.setValue(photoTask.getMaxPhotoCount() != null ? photoTask.getMaxPhotoCount() : 10);
@@ -2434,12 +2437,12 @@ public class AddJobView extends Main {
barcodeLayout.setSpacing(true);
BarcodeTask barcodeTask = (BarcodeTask) task;
IntegerField minBarcodes = new IntegerField("Min. Anzahl Barcodes");
IntegerField minBarcodes = new IntegerField(getTranslation("addjob.tasks.barcode.min"));
minBarcodes.setPlaceholder("1");
minBarcodes.setMin(1);
minBarcodes.setValue(barcodeTask.getMinBarcodeCount() != null ? barcodeTask.getMinBarcodeCount() : 1);
IntegerField maxBarcodes = new IntegerField("Max. Anzahl Barcodes");
IntegerField maxBarcodes = new IntegerField(getTranslation("addjob.tasks.barcode.max"));
maxBarcodes.setPlaceholder("10");
maxBarcodes.setMin(1);
maxBarcodes.setValue(barcodeTask.getMaxBarcodeCount() != null ? barcodeTask.getMaxBarcodeCount() : 10);
@@ -2460,8 +2463,8 @@ public class AddJobView extends Main {
case COMMENT:
CommentTask commentTask = (CommentTask) task;
TextField commentTextField = new TextField("Kommentar-Platzhalter");
commentTextField.setPlaceholder("Hinweis für den Benutzer...");
TextField commentTextField = new TextField(getTranslation("addjob.tasks.comment.label"));
commentTextField.setPlaceholder(getTranslation("addjob.tasks.comment.placeholder"));
commentTextField.setWidthFull();
commentTextField.setValue(commentTask.getCommentText() != null ? commentTask.getCommentText() : "");
commentTextField.addValueChangeListener(ev -> {
@@ -2469,7 +2472,7 @@ public class AddJobView extends Main {
});
com.vaadin.flow.component.checkbox.Checkbox requiredCheckbox = new com.vaadin.flow.component.checkbox.Checkbox(
"Pflichtfeld");
getTranslation("addjob.tasks.comment.required"));
requiredCheckbox.setValue(commentTask.isRequired());
requiredCheckbox.addValueChangeListener(ev -> {
commentTask.setRequired(ev.getValue());
@@ -2509,30 +2512,30 @@ public class AddJobView extends Main {
try {
// Check if there are any tasks to save
if (tasksState.isEmpty()) {
Notification.show("Keine Aufgaben zum Speichern vorhanden", 3000, Notification.Position.BOTTOM_END);
Notification.show(getTranslation("addjob.tasks.template.no.tasks"), 3000, Notification.Position.BOTTOM_END);
return;
}
// Create dialog for template name input
Dialog dialog = new Dialog();
dialog.setHeaderTitle("Template speichern");
dialog.setHeaderTitle(getTranslation("addjob.tasks.template.save.title"));
dialog.setWidth("400px");
VerticalLayout dialogLayout = new VerticalLayout();
dialogLayout.setPadding(false);
dialogLayout.setSpacing(true);
TextField templateNameField = new TextField("Template-Name");
templateNameField.setPlaceholder("Geben Sie einen Namen für das Template ein");
TextField templateNameField = new TextField(getTranslation("addjob.tasks.template.name"));
templateNameField.setPlaceholder(getTranslation("addjob.tasks.template.name.placeholder"));
templateNameField.setWidthFull();
templateNameField.setRequiredIndicatorVisible(true);
Button saveButton = new Button("Speichern");
Button saveButton = new Button(getTranslation("button.savechanges"));
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
saveButton.addClickListener(e -> {
String templateName = templateNameField.getValue();
if (templateName == null || templateName.trim().isEmpty()) {
Notification.show("Bitte geben Sie einen Template-Namen ein", 3000,
Notification.show(getTranslation("addjob.tasks.template.name.required"), 3000,
Notification.Position.BOTTOM_END);
return;
}
@@ -2552,19 +2555,19 @@ public class AddJobView extends Main {
dialog.close();
loadTemplatesIntoComboBox(templateComboBox);
Notification.show("Template '" + templateName + "' erfolgreich gespeichert", 3000,
Notification.show(getTranslation("addjob.tasks.template.saved", templateName), 3000,
Notification.Position.BOTTOM_END);
} catch (RuntimeException ex) {
Notification.show(ex.getMessage(), 4000, Notification.Position.MIDDLE);
} catch (Exception ex) {
log.error("Error saving task template", ex);
Notification.show("Fehler beim Speichern des Templates: " + ex.getMessage(), 4000,
Notification.show(getTranslation("addjob.tasks.template.save.error", ex.getMessage()), 4000,
Notification.Position.MIDDLE);
}
});
Button cancelButton = new Button("Abbrechen");
Button cancelButton = new Button(getTranslation("button.cancel"));
cancelButton.addClickListener(e -> dialog.close());
HorizontalLayout buttonLayout = new HorizontalLayout(cancelButton, saveButton);
@@ -2576,7 +2579,7 @@ public class AddJobView extends Main {
} catch (Exception e) {
log.error("Error opening save template dialog", e);
Notification.show("Fehler beim Öffnen des Dialogs: " + e.getMessage(), 4000, Notification.Position.MIDDLE);
Notification.show(getTranslation("addjob.tasks.template.dialog.error", e.getMessage()), 4000, Notification.Position.MIDDLE);
}
}
@@ -2643,7 +2646,7 @@ public class AddJobView extends Main {
templateComboBox.setItems(templates);
} catch (Exception e) {
log.error("Error loading templates", e);
Notification.show("Fehler beim Laden der Templates: " + e.getMessage(), 4000, Notification.Position.MIDDLE);
Notification.show(getTranslation("addjob.tasks.template.load.templates.error", e.getMessage()), 4000, Notification.Position.MIDDLE);
}
}
@@ -2652,12 +2655,11 @@ public class AddJobView extends Main {
*/
private void loadTasksFromTemplate(TaskTemplate template, ComboBox<TaskTemplate> templateComboBox) {
ConfirmDialog confirmDialog = new ConfirmDialog();
confirmDialog.setHeader("Template laden");
confirmDialog.setText("Möchten Sie wirklich das Template '" + template.getTemplateName()
+ "' laden? Alle aktuellen Aufgaben werden ersetzt.");
confirmDialog.setHeader(getTranslation("addjob.tasks.template.load.title"));
confirmDialog.setText(getTranslation("addjob.tasks.template.load.text", template.getTemplateName()));
confirmDialog.setCancelable(true);
confirmDialog.setCancelText("Abbrechen");
confirmDialog.setConfirmText("Laden");
confirmDialog.setCancelText(getTranslation("button.cancel"));
confirmDialog.setConfirmText(getTranslation("addjob.tasks.template.load.confirm"));
confirmDialog.setConfirmButtonTheme("primary");
confirmDialog.addConfirmListener(e -> {
@@ -2684,12 +2686,12 @@ public class AddJobView extends Main {
triggerValidation();
updateTabLabels();
Notification.show("Template '" + template.getTemplateName() + "' erfolgreich geladen", 3000,
Notification.show(getTranslation("addjob.tasks.template.loaded", template.getTemplateName()), 3000,
Notification.Position.BOTTOM_END);
} catch (Exception ex) {
log.error("Error loading template tasks", ex);
Notification.show("Fehler beim Laden des Templates: " + ex.getMessage(), 4000,
Notification.show(getTranslation("addjob.tasks.template.load.error", ex.getMessage()), 4000,
Notification.Position.MIDDLE);
}
});
@@ -2720,10 +2722,10 @@ public class AddJobView extends Main {
taskContainer.getStyle().set("position", "relative");
// Task type selection
ComboBox<TaskType> taskTypeCombo = new ComboBox<>("Aufgabentyp");
ComboBox<TaskType> taskTypeCombo = new ComboBox<>(getTranslation("addjob.tasks.tasktype"));
taskTypeCombo.setItems(TaskType.values());
taskTypeCombo.setItemLabelGenerator(TaskType::getDisplayName);
taskTypeCombo.setPlaceholder("Aufgabentyp wählen...");
taskTypeCombo.setPlaceholder(getTranslation("addjob.tasks.tasktype.placeholder"));
taskTypeCombo.setWidthFull();
// Configuration container for dynamic fields
@@ -2922,7 +2924,7 @@ public class AddJobView extends Main {
*/
private void showAddressValidationDialog(com.vaadin.flow.component.tabs.Tab targetTab) {
final Dialog dialog = new Dialog();
dialog.setHeaderTitle("Adressen werden überprüft");
dialog.setHeaderTitle(getTranslation("addjob.validation.dialog.title"));
dialog.setWidth("500px");
dialog.setModal(true);
dialog.setCloseOnOutsideClick(false);
@@ -2933,7 +2935,7 @@ public class AddJobView extends Main {
content.setSpacing(true);
// Initiale Meldung mit Progress
final Span loadingMessage = new Span("Adressen werden bei Google Maps überprüft...");
final Span loadingMessage = new Span(getTranslation("addjob.validation.dialog.loading"));
loadingMessage.getStyle().set("font-style", "italic");
loadingMessage.getStyle().set("color", "var(--lumo-secondary-text-color)");
@@ -2968,7 +2970,7 @@ public class AddJobView extends Main {
buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
buttonLayout.setVisible(false);
final Button cancelButton = new Button("Zurück", e -> {
final Button cancelButton = new Button(getTranslation("addjob.validation.dialog.back"), e -> {
dialog.close();
// Im Adress-Tab bleiben
validationDialogOpen = false;
@@ -2976,7 +2978,7 @@ public class AddJobView extends Main {
});
cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
final Button continueButton = new Button("Trotzdem wechseln", e -> {
final Button continueButton = new Button(getTranslation("addjob.validation.dialog.continue.anyway"), e -> {
dialog.close();
// Zum Ziel-Tab wechseln
tabSheet.setSelectedTab(targetTab);
@@ -3075,10 +3077,10 @@ public class AddJobView extends Main {
// Abholadresse anzeigen
if (pickupResult != null) {
if (pickupResult.isValid()) {
pickupResultLabel.setText("Abholadresse: " + pickupResult.getFormattedAddress());
pickupResultLabel.setText("" + getTranslation("addjob.validation.pickup.address") + ": " + pickupResult.getFormattedAddress());
pickupResultLabel.getStyle().set("color", "var(--lumo-success-text-color)");
} else {
pickupResultLabel.setText("Abholadresse: " + pickupResult.getValidationMessage());
pickupResultLabel.setText("" + getTranslation("addjob.validation.pickup.address") + ": " + pickupResult.getValidationMessage());
pickupResultLabel.getStyle().set("color", "var(--lumo-error-text-color)");
hasInvalidAddress = true;
bothAddressesValid = false;
@@ -3090,10 +3092,10 @@ public class AddJobView extends Main {
// Lieferadresse anzeigen
if (deliveryResult != null) {
if (deliveryResult.isValid()) {
deliveryResultLabel.setText("Lieferadresse: " + deliveryResult.getFormattedAddress());
deliveryResultLabel.setText("" + getTranslation("addjob.validation.delivery.address") + ": " + deliveryResult.getFormattedAddress());
deliveryResultLabel.getStyle().set("color", "var(--lumo-success-text-color)");
} else {
deliveryResultLabel.setText("Lieferadresse: " + deliveryResult.getValidationMessage());
deliveryResultLabel.setText("" + getTranslation("addjob.validation.delivery.address") + ": " + deliveryResult.getValidationMessage());
deliveryResultLabel.getStyle().set("color", "var(--lumo-error-text-color)");
hasInvalidAddress = true;
bothAddressesValid = false;
@@ -3104,8 +3106,8 @@ public class AddJobView extends Main {
// Route anzeigen, wenn beide Adressen gültig sind
if (bothAddressesValid && routeCalculationResult != null && routeCalculationResult.isValid()) {
routeResultLabel.setText("🚛 Route: " + String.format("%.1f km", routeCalculationResult.getDistanceKm())
+ " (Fahrtzeit: " + routeCalculationResult.getFormattedDurationLong() + ")");
routeResultLabel.setText("🚛 " + getTranslation("addjob.validation.route") + ": " + String.format("%.1f km", routeCalculationResult.getDistanceKm())
+ " (" + getTranslation("addjob.route.duration") + ": " + routeCalculationResult.getFormattedDurationLong() + ")");
routeResultLabel.getStyle().set("color", "var(--lumo-primary-text-color)");
routeResultLabel.setVisible(true);
} else {
@@ -3123,9 +3125,9 @@ public class AddJobView extends Main {
// Wenn beide Adressen gültig sind, direkt weiter
if (!hasInvalidAddress) {
continueButton.setText("Weiter");
continueButton.setText(getTranslation("addjob.validation.dialog.continue"));
} else {
continueButton.setText("Trotzdem wechseln");
continueButton.setText(getTranslation("addjob.validation.dialog.continue.anyway"));
}
// Route-Info im Preis-Tab aktualisieren

View File

@@ -10,8 +10,8 @@ import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Menu;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.votianlt.model.JobStatus;
@@ -25,11 +25,10 @@ import java.time.LocalDateTime;
import java.util.concurrent.CompletableFuture;
@Route(value = "admin-dashboard", layout = de.assecutor.votianlt.pages.base.ui.view.AdminLayout.class)
@PageTitle("Admin Dashboard")
@RolesAllowed("ADMIN")
@Menu(order = 1, icon = "lumo:edit")
@Slf4j
public class AdminDashboardView extends Main {
public class AdminDashboardView extends Main implements HasDynamicTitle {
private final JobRepository jobRepository;
private final TaskRepository taskRepository;
@@ -64,7 +63,7 @@ public class AdminDashboardView extends Main {
LumoUtility.Padding.MEDIUM);
// Header
H1 title = new H1("Administrator Dashboard");
H1 title = new H1(getTranslation("admindashboard.title"));
title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, LumoUtility.Margin.Top.NONE);
HorizontalLayout header = new HorizontalLayout(title);
@@ -92,7 +91,7 @@ public class AdminDashboardView extends Main {
// Show loading indicator
statisticsContainer.removeAll();
statisticsContainer.add(new Span("Lade Statistiken..."));
statisticsContainer.add(new Span(getTranslation("admindashboard.loading")));
// Load statistics asynchronously
CompletableFuture.runAsync(() -> {
@@ -102,7 +101,7 @@ public class AdminDashboardView extends Main {
} catch (Exception e) {
log.error("Error loading dashboard statistics", e);
statisticsContainer.removeAll();
statisticsContainer.add(new Span("Fehler beim Laden der Statistiken: " + e.getMessage()));
statisticsContainer.add(new Span(getTranslation("admindashboard.error", e.getMessage())));
}
}));
});
@@ -140,7 +139,7 @@ public class AdminDashboardView extends Main {
section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem");
H3 title = new H3("System-Übersicht");
H3 title = new H3(getTranslation("admindashboard.section.overview"));
title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
HorizontalLayout cards = new HorizontalLayout();
@@ -149,19 +148,19 @@ public class AdminDashboardView extends Main {
// Total jobs card
long totalJobs = jobRepository.count();
cards.add(createStatCard("Gesamt Jobs", String.valueOf(totalJobs), VaadinIcon.PACKAGE, "blue"));
cards.add(createStatCard(getTranslation("admindashboard.stat.totaljobs"), String.valueOf(totalJobs), VaadinIcon.PACKAGE, "blue"));
// Total users card
long totalUsers = userRepository.count();
cards.add(createStatCard("Benutzer", String.valueOf(totalUsers), VaadinIcon.USERS, "green"));
cards.add(createStatCard(getTranslation("admindashboard.stat.users"), String.valueOf(totalUsers), VaadinIcon.USERS, "green"));
// Total app users card
long totalAppUsers = appUserRepository.count();
cards.add(createStatCard("App-Benutzer", String.valueOf(totalAppUsers), VaadinIcon.MOBILE, "purple"));
cards.add(createStatCard(getTranslation("admindashboard.stat.appusers"), String.valueOf(totalAppUsers), VaadinIcon.MOBILE, "purple"));
// Current time
String currentTime = DateTimeFormatUtil.formatDateTime(LocalDateTime.now());
cards.add(createStatCard("Letzte Aktualisierung", currentTime, VaadinIcon.CLOCK, "gray"));
cards.add(createStatCard(getTranslation("admindashboard.stat.lastupdated"), currentTime, VaadinIcon.CLOCK, "gray"));
section.add(title, cards);
return section;
@@ -173,7 +172,7 @@ public class AdminDashboardView extends Main {
section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem");
H3 title = new H3("Job-Statistiken");
H3 title = new H3(getTranslation("admindashboard.section.jobs"));
title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
HorizontalLayout cards = new HorizontalLayout();
@@ -186,17 +185,17 @@ public class AdminDashboardView extends Main {
long inProgressJobs = jobRepository.countByStatus(JobStatus.IN_PROGRESS);
long completedJobs = jobRepository.countByStatus(JobStatus.COMPLETED);
cards.add(createStatCard("Offene Jobs", String.valueOf(openJobs), VaadinIcon.HOURGLASS_START, "orange"));
cards.add(createStatCard("In Bearbeitung", String.valueOf(inProgressJobs), VaadinIcon.PLAY, "blue"));
cards.add(createStatCard("Abgeschlossen", String.valueOf(completedJobs), VaadinIcon.CHECK_CIRCLE, "green"));
cards.add(createStatCard(getTranslation("admindashboard.stat.openjobs"), String.valueOf(openJobs), VaadinIcon.HOURGLASS_START, "orange"));
cards.add(createStatCard(getTranslation("admindashboard.stat.inprogress"), String.valueOf(inProgressJobs), VaadinIcon.PLAY, "blue"));
cards.add(createStatCard(getTranslation("admindashboard.stat.completed"), String.valueOf(completedJobs), VaadinIcon.CHECK_CIRCLE, "green"));
// Total cargo items
long totalCargoItems = cargoItemRepository.count();
cards.add(createStatCard("Frachtgüter", String.valueOf(totalCargoItems), VaadinIcon.CUBE, "purple"));
cards.add(createStatCard(getTranslation("admindashboard.stat.cargo"), String.valueOf(totalCargoItems), VaadinIcon.CUBE, "purple"));
} catch (Exception e) {
log.warn("Could not load job statistics by status", e);
cards.add(createStatCard("Status-Info", "Nicht verfügbar", VaadinIcon.WARNING, "red"));
cards.add(createStatCard(getTranslation("admindashboard.stat.status.info"), getTranslation("admindashboard.stat.status.unavailable"), VaadinIcon.WARNING, "red"));
}
section.add(title, cards);
@@ -209,7 +208,7 @@ public class AdminDashboardView extends Main {
section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem");
H3 title = new H3("Aufgaben-Statistiken");
H3 title = new H3(getTranslation("admindashboard.section.tasks"));
title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
HorizontalLayout cards = new HorizontalLayout();
@@ -218,19 +217,19 @@ public class AdminDashboardView extends Main {
// Total tasks
long totalTasks = taskRepository.count();
cards.add(createStatCard("Gesamt Aufgaben", String.valueOf(totalTasks), VaadinIcon.TASKS, "blue"));
cards.add(createStatCard(getTranslation("admindashboard.stat.totaltasks"), String.valueOf(totalTasks), VaadinIcon.TASKS, "blue"));
// Completed tasks
long completedTasks = taskRepository.countByCompleted(true);
cards.add(createStatCard("Abgeschlossen", String.valueOf(completedTasks), VaadinIcon.CHECK, "green"));
cards.add(createStatCard(getTranslation("admindashboard.stat.completedtasks"), String.valueOf(completedTasks), VaadinIcon.CHECK, "green"));
// Pending tasks
long pendingTasks = totalTasks - completedTasks;
cards.add(createStatCard("Offen", String.valueOf(pendingTasks), VaadinIcon.CLOCK, "orange"));
cards.add(createStatCard(getTranslation("admindashboard.stat.pendingtasks"), String.valueOf(pendingTasks), VaadinIcon.CLOCK, "orange"));
// Completion rate
double completionRate = totalTasks > 0 ? (completedTasks * 100.0 / totalTasks) : 0;
cards.add(createStatCard("Erfolgsquote", String.format("%.1f%%", completionRate), VaadinIcon.TRENDING_UP,
cards.add(createStatCard(getTranslation("admindashboard.stat.successrate"), String.format("%.1f%%", completionRate), VaadinIcon.TRENDING_UP,
"purple"));
section.add(title, cards);
@@ -243,7 +242,7 @@ public class AdminDashboardView extends Main {
section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem");
H3 title = new H3("Benutzer-Aktivität");
H3 title = new H3(getTranslation("admindashboard.section.users"));
title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
HorizontalLayout cards = new HorizontalLayout();
@@ -252,16 +251,16 @@ public class AdminDashboardView extends Main {
// Content statistics
long totalPhotos = photoRepository.count();
cards.add(createStatCard("Fotos", String.valueOf(totalPhotos), VaadinIcon.CAMERA, "blue"));
cards.add(createStatCard(getTranslation("admindashboard.stat.photos"), String.valueOf(totalPhotos), VaadinIcon.CAMERA, "blue"));
long totalBarcodes = barcodeRepository.count();
cards.add(createStatCard("Barcodes", String.valueOf(totalBarcodes), VaadinIcon.BARCODE, "green"));
cards.add(createStatCard(getTranslation("admindashboard.stat.barcodes"), String.valueOf(totalBarcodes), VaadinIcon.BARCODE, "green"));
long totalSignatures = signatureRepository.count();
cards.add(createStatCard("Unterschriften", String.valueOf(totalSignatures), VaadinIcon.EDIT, "purple"));
cards.add(createStatCard(getTranslation("admindashboard.stat.signatures"), String.valueOf(totalSignatures), VaadinIcon.EDIT, "purple"));
long totalComments = commentRepository.count();
cards.add(createStatCard("Kommentare", String.valueOf(totalComments), VaadinIcon.COMMENT, "orange"));
cards.add(createStatCard(getTranslation("admindashboard.stat.comments"), String.valueOf(totalComments), VaadinIcon.COMMENT, "orange"));
section.add(title, cards);
return section;
@@ -273,7 +272,7 @@ public class AdminDashboardView extends Main {
section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem");
H3 title = new H3("System-Status");
H3 title = new H3(getTranslation("admindashboard.section.health"));
title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
HorizontalLayout cards = new HorizontalLayout();
@@ -283,16 +282,16 @@ public class AdminDashboardView extends Main {
// Database connection status
try {
userRepository.count(); // Test database connection
cards.add(createStatCard("Datenbank", "Verbunden", VaadinIcon.DATABASE, "green"));
cards.add(createStatCard(getTranslation("admindashboard.stat.database"), getTranslation("admindashboard.stat.database.connected"), VaadinIcon.DATABASE, "green"));
} catch (Exception e) {
cards.add(createStatCard("Datenbank", "Fehler", VaadinIcon.DATABASE, "red"));
cards.add(createStatCard(getTranslation("admindashboard.stat.database"), getTranslation("admindashboard.stat.database.error"), VaadinIcon.DATABASE, "red"));
}
// Messaging status
cards.add(createStatCard("WebSocket", "Aktiv", VaadinIcon.CONNECT, "green"));
cards.add(createStatCard(getTranslation("admindashboard.stat.websocket"), getTranslation("admindashboard.stat.websocket.active"), VaadinIcon.CONNECT, "green"));
// System uptime (placeholder)
cards.add(createStatCard("Anwendung", "Läuft", VaadinIcon.HEART, "green"));
cards.add(createStatCard(getTranslation("admindashboard.stat.app"), getTranslation("admindashboard.stat.app.running"), VaadinIcon.HEART, "green"));
// Memory usage (placeholder)
Runtime runtime = Runtime.getRuntime();
@@ -300,7 +299,7 @@ public class AdminDashboardView extends Main {
long totalMemory = runtime.totalMemory() / 1024 / 1024; // MB
long usedMemory = totalMemory - (runtime.freeMemory() / 1024 / 1024); // MB
String memoryInfo = usedMemory + "/" + maxMemory + " MB";
cards.add(createStatCard("Speicher", memoryInfo, VaadinIcon.SERVER, "blue"));
cards.add(createStatCard(getTranslation("admindashboard.stat.memory"), memoryInfo, VaadinIcon.SERVER, "blue"));
section.add(title, cards);
return section;
@@ -337,4 +336,9 @@ public class AdminDashboardView extends Main {
card.add(content);
return card;
}
@Override
public String getPageTitle() {
return getTranslation("page.title.admin.dashboard");
}
}

View File

@@ -5,7 +5,7 @@ import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.PriceTable;
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
@@ -13,9 +13,8 @@ import de.assecutor.votianlt.repository.PriceTableRepository;
import jakarta.annotation.security.RolesAllowed;
@Route(value = "admin-price-table", layout = AdminLayout.class)
@PageTitle("Preis-Tabelle")
@RolesAllowed("ADMIN")
public class AdminPricetableView extends VerticalLayout {
public class AdminPricetableView extends VerticalLayout implements HasDynamicTitle {
private final PriceTableRepository priceTableRepository;
private final TextField monthlyBasePackage;
@@ -30,22 +29,25 @@ public class AdminPricetableView extends VerticalLayout {
getStyle().set("margin", "14px");
setWidth("90%");
H2 title = new H2("Preis-Tabelle");
H2 title = new H2(getTranslation("adminpricetable.title"));
add(title);
VerticalLayout fieldsLayout = new VerticalLayout();
fieldsLayout.setSpacing(true);
fieldsLayout.setPadding(false);
monthlyBasePackage = new TextField("Monatliche Grundpauschale");
monthlyBasePackage = new TextField();
monthlyBasePackage.setLabel(getTranslation("adminpricetable.field.monthly"));
monthlyBasePackage.setWidth("40%");
monthlyBasePackage.setMaxWidth("40%");
appUsageLicense = new TextField("App-Nutzungslizenz");
appUsageLicense = new TextField();
appUsageLicense.setLabel(getTranslation("adminpricetable.field.applicense"));
appUsageLicense.setWidth("40%");
appUsageLicense.setMaxWidth("40%");
revenueParticipation = new TextField("Umsatzbeteiligung in Prozent");
revenueParticipation = new TextField();
revenueParticipation.setLabel(getTranslation("adminpricetable.field.revenue"));
revenueParticipation.setWidth("40%");
revenueParticipation.setMaxWidth("40%");
@@ -53,7 +55,7 @@ public class AdminPricetableView extends VerticalLayout {
add(fieldsLayout);
Button saveButton = new Button("Speichern");
Button saveButton = new Button(getTranslation("button.savechanges"));
saveButton.getStyle().set("margin-top", "20px");
saveButton.addClickListener(e -> savePriceTable());
@@ -73,9 +75,9 @@ public class AdminPricetableView extends VerticalLayout {
priceTable.setRevenueParticipation(revenueParticipation.getValue());
priceTableRepository.save(priceTable);
Notification.show("Preise erfolgreich gespeichert!", 3000, Notification.Position.BOTTOM_CENTER);
Notification.show(getTranslation("adminpricetable.notification.saved"), 3000, Notification.Position.BOTTOM_CENTER);
} catch (Exception ex) {
Notification.show("Fehler beim Speichern: " + ex.getMessage(), 5000, Notification.Position.BOTTOM_CENTER);
Notification.show(getTranslation("adminpricetable.notification.save.error", ex.getMessage()), 5000, Notification.Position.BOTTOM_CENTER);
}
}
@@ -92,8 +94,13 @@ public class AdminPricetableView extends VerticalLayout {
priceTable.getRevenueParticipation() != null ? priceTable.getRevenueParticipation() : "");
}
} catch (Exception ex) {
Notification.show("Fehler beim Laden der Daten: " + ex.getMessage(), 5000,
Notification.show(getTranslation("adminpricetable.notification.load.error", ex.getMessage()), 5000,
Notification.Position.BOTTOM_CENTER);
}
}
@Override
public String getPageTitle() {
return getTranslation("page.title.pricetable");
}
}

View File

@@ -9,17 +9,16 @@ import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.AppUser;
import de.assecutor.votianlt.pages.service.AppUserService;
import jakarta.annotation.security.RolesAllowed;
import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("App-Nutzer")
@Route(value = "app-user", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({ "USER", "ADMIN" })
public class AppUserView extends VerticalLayout {
public class AppUserView extends VerticalLayout implements HasDynamicTitle {
private final AppUserService appUserService;
private final Grid<AppUser> appUserGrid;
@@ -37,10 +36,10 @@ public class AppUserView extends VerticalLayout {
header.setWidthFull();
header.setAlignItems(FlexComponent.Alignment.CENTER);
H2 title = new H2("App-Nutzer");
H2 title = new H2(getTranslation("appuser.title"));
title.getStyle().set("margin", "0");
Button addButton = new Button("Neuen App-Nutzer anlegen", new Icon(VaadinIcon.PLUS));
Button addButton = new Button(getTranslation("appuser.button.add"), new Icon(VaadinIcon.PLUS));
addButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addButton.addClickListener(e -> navigateToAddAppUser());
@@ -53,12 +52,12 @@ public class AppUserView extends VerticalLayout {
appUserGrid.setSizeFull();
// Grid-Spalten konfigurieren
appUserGrid.addColumn(AppUser::getBezeichnung).setHeader("Bezeichnung").setAutoWidth(true);
appUserGrid.addColumn(AppUser::getVorname).setHeader("Vorname").setAutoWidth(true);
appUserGrid.addColumn(AppUser::getNachname).setHeader("Nachname").setAutoWidth(true);
appUserGrid.addColumn(AppUser::getTelefon).setHeader("Telefon").setAutoWidth(true);
appUserGrid.addColumn(AppUser::getAppCode).setHeader("App-Code").setAutoWidth(true);
appUserGrid.addColumn(AppUser::getEmail).setHeader("E-Mail").setAutoWidth(true);
appUserGrid.addColumn(AppUser::getBezeichnung).setHeader(getTranslation("appuser.column.designation")).setAutoWidth(true);
appUserGrid.addColumn(AppUser::getVorname).setHeader(getTranslation("appuser.column.firstname")).setAutoWidth(true);
appUserGrid.addColumn(AppUser::getNachname).setHeader(getTranslation("appuser.column.lastname")).setAutoWidth(true);
appUserGrid.addColumn(AppUser::getTelefon).setHeader(getTranslation("appuser.column.phone")).setAutoWidth(true);
appUserGrid.addColumn(AppUser::getAppCode).setHeader(getTranslation("appuser.column.appcode")).setAutoWidth(true);
appUserGrid.addColumn(AppUser::getEmail).setHeader(getTranslation("appuser.column.email")).setAutoWidth(true);
// Make grid rows clickable
appUserGrid.setSelectionMode(Grid.SelectionMode.SINGLE);
@@ -86,4 +85,9 @@ public class AppUserView extends VerticalLayout {
private void navigateToAddAppUser() {
getUI().ifPresent(ui -> ui.navigate("add-app-user"));
}
@Override
public String getPageTitle() {
return getTranslation("page.title.appusers");
}
}

View File

@@ -7,16 +7,16 @@ import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.pages.base.ui.view.MainLayout;
import jakarta.annotation.security.RolesAllowed;
@Route(value = "dashboard", layout = MainLayout.class)
@PageTitle("VotianLT - Dashboard")
@RolesAllowed({ "USER" })
public class AuthenticatedStartView extends VerticalLayout {
public class AuthenticatedStartView extends VerticalLayout implements HasDynamicTitle {
private final SecurityService securityService;
@@ -59,14 +59,16 @@ public class AuthenticatedStartView extends VerticalLayout {
heroSection.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
// Welcome message for authenticated user
String currentUser = securityService.getCurrentUsername();
H1 welcomeTitle = new H1("Willkommen zurück, " + currentUser + "!");
User currentUser = securityService.getCurrentDatabaseUser();
String displayName = currentUser != null && currentUser.getFirstname() != null && currentUser.getName() != null
? currentUser.getFirstname() + " " + currentUser.getName()
: securityService.getCurrentUsername();
H1 welcomeTitle = new H1(getTranslation("dashboard.welcome", displayName));
welcomeTitle.getStyle().set("text-align", "center");
welcomeTitle.getStyle().set("color", "var(--lumo-primary-text-color)");
welcomeTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)");
Paragraph welcomeDescription = new Paragraph(
"Nutzen Sie die Navigation links, um neue Aufträge zu erstellen oder Ihre Verwaltung zu bearbeiten.");
Paragraph welcomeDescription = new Paragraph(getTranslation("dashboard.description"));
welcomeDescription.getStyle().set("text-align", "center");
welcomeDescription.getStyle().set("max-width", "600px");
welcomeDescription.getStyle().set("font-size", "var(--lumo-font-size-l)");
@@ -84,13 +86,11 @@ public class AuthenticatedStartView extends VerticalLayout {
systemSection.getStyle().set("background-color", "var(--lumo-base-color)");
// Section Header
H2 systemTitle = new H2("Das System");
H2 systemTitle = new H2(getTranslation("dashboard.system.title"));
systemTitle.getStyle().set("color", "var(--lumo-primary-color)");
systemTitle.getStyle().set("text-align", "center");
Paragraph systemIntro = new Paragraph(
"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.");
Paragraph systemIntro = new Paragraph(getTranslation("dashboard.system.intro"));
systemIntro.getStyle().set("text-align", "center");
systemIntro.getStyle().set("max-width", "800px");
systemIntro.getStyle().set("margin-bottom", "var(--lumo-space-xl)");
@@ -105,12 +105,16 @@ public class AuthenticatedStartView extends VerticalLayout {
featuresGrid.getStyle().set("width", "100%");
// Feature Cards
featuresGrid.add(createFeatureCard(VaadinIcon.COG, "Einrichtungsassistent",
"Mithilfe des Einrichtungsassistenten haben Sie die Möglichkeit, Ihr Nutzerprofil zu vervollständigen."),
createFeatureCard(VaadinIcon.USERS, "Kunden- und Auftragsverwaltung",
"Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick."),
createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, "Auftragserstellung",
"Stellen Sie mit wenigen Mausklicks Aufträge ins System ein und legen Sie fest, welcher Mitarbeiter welchen Transportauftrag abarbeiten soll."));
featuresGrid.add(
createFeatureCard(VaadinIcon.COG,
getTranslation("dashboard.feature.setup.title"),
getTranslation("dashboard.feature.setup.desc")),
createFeatureCard(VaadinIcon.USERS,
getTranslation("dashboard.feature.customers.title"),
getTranslation("dashboard.feature.customers.desc")),
createFeatureCard(VaadinIcon.CLIPBOARD_TEXT,
getTranslation("dashboard.feature.jobs.title"),
getTranslation("dashboard.feature.jobs.desc")));
systemSection.add(systemTitle, systemIntro, featuresGrid);
return systemSection;
@@ -160,13 +164,11 @@ public class AuthenticatedStartView extends VerticalLayout {
appSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
appSection.getStyle().set("background-color", "var(--lumo-contrast-5pct)");
H2 appTitle = new H2("Die App");
H2 appTitle = new H2(getTranslation("dashboard.app.title"));
appTitle.getStyle().set("color", "var(--lumo-primary-color)");
appTitle.getStyle().set("text-align", "center");
Paragraph appDescription = new Paragraph(
"Mit unserer mobilen App bleiben Sie auch unterwegs immer über Ihre Aufträge informiert "
+ "und können wichtige Aufgaben direkt vom Smartphone aus erledigen.");
Paragraph appDescription = new Paragraph(getTranslation("dashboard.app.description"));
appDescription.getStyle().set("text-align", "center");
appDescription.getStyle().set("max-width", "600px");
@@ -188,7 +190,7 @@ public class AuthenticatedStartView extends VerticalLayout {
footerContent.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
footerContent.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
Paragraph copyright = new Paragraph("© 2024 VotianLT. Alle Rechte vorbehalten.");
Paragraph copyright = new Paragraph(getTranslation("dashboard.footer.copyright"));
copyright.getStyle().set("color", "var(--lumo-secondary-text-color)");
copyright.getStyle().set("font-size", "var(--lumo-font-size-s)");
copyright.getStyle().set("margin", "0");
@@ -198,4 +200,9 @@ public class AuthenticatedStartView extends VerticalLayout {
return footer;
}
}
@Override
public String getPageTitle() {
return getTranslation("page.title.dashboard");
}
}

View File

@@ -12,7 +12,7 @@ import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.BeforeEvent;
import com.vaadin.flow.router.HasUrlParameter;
@@ -44,11 +44,10 @@ import com.vaadin.flow.component.html.IFrame;
import com.vaadin.flow.server.StreamResource;
import com.vaadin.flow.server.VaadinSession;
@PageTitle("Rechnung erstellen")
@Route(value = "create_invoice", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({ "USER" })
@Slf4j
public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter<String> {
public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter<String>, HasDynamicTitle {
private final JobRepository jobRepository;
private final ServiceRepository serviceRepository;
@@ -125,14 +124,14 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
loadJob(jobId);
} catch (Exception e) {
log.error("Fehler beim Parsen der Job-ID: " + jobIdHex, e);
add(new Span("Ungültige Auftrags-ID"));
add(new Span(getTranslation("createinvoice.error.invalidid")));
}
}
public void loadJob(ObjectId jobId) {
currentJob = jobRepository.findById(jobId).orElse(null);
if (currentJob == null) {
add(new Span("Auftrag nicht gefunden"));
add(new Span(getTranslation("createinvoice.error.notfound")));
return;
}
@@ -143,7 +142,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
removeAll();
// Title
H2 title = new H2("Rechnung erstellen für Auftrag " + currentJob.getJobNumber());
H2 title = new H2(getTranslation("createinvoice.title", currentJob.getJobNumber()));
add(title);
// Load previously selected services from job
@@ -168,7 +167,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
add(summarySection);
// Create Invoice Button
Button createInvoiceButton = new Button("Rechnung erstellen");
Button createInvoiceButton = new Button(getTranslation("createinvoice.button.create"));
createInvoiceButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
createInvoiceButton.addClickListener(e -> createInvoice());
createInvoiceButton.getStyle().set("margin-bottom", "15px");
@@ -181,7 +180,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box");
H3 sectionTitle = new H3("Auftragsdetails");
H3 sectionTitle = new H3(getTranslation("createinvoice.section.job"));
section.add(sectionTitle);
// Job information
@@ -189,11 +188,11 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
jobInfo.setSpacing(true);
jobInfo.setWidthFull();
jobInfo.add(new HorizontalLayout(new Span("Auftragsnummer:"), new Span(currentJob.getJobNumber())));
jobInfo.add(new HorizontalLayout(new Span("Kunde:"),
jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.jobnumber")), new Span(currentJob.getJobNumber())));
jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.customer")),
new Span(extractCompanyName(currentJob.getCustomerSelection()))));
jobInfo.add(new HorizontalLayout(new Span("Status:"), new Span(currentJob.getStatus().toString())));
jobInfo.add(new HorizontalLayout(new Span("Preis:"), new Span(currentJob.getPrice() + "")));
jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.status")), new Span(currentJob.getStatus().toString())));
jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.price")), new Span(currentJob.getPrice() + "")));
section.add(jobInfo);
return section;
@@ -206,7 +205,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
.set("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box")
.set("background-color", "var(--lumo-primary-color-10pct)");
H3 sectionTitle = new H3("Streckeninformation");
H3 sectionTitle = new H3(getTranslation("createinvoice.section.route"));
sectionTitle.getStyle().set("color", "var(--lumo-primary-text-color)");
section.add(sectionTitle);
@@ -216,14 +215,14 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
Double distance = currentJob.getRouteDistanceKm();
if (distance != null) {
routeInfo.add(new HorizontalLayout(new Span("Berechnete Entfernung:"),
routeInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.route.distance")),
new Span(String.format("%.1f km", distance))));
}
Integer durationSeconds = currentJob.getRouteDurationSeconds();
if (durationSeconds != null && durationSeconds > 0) {
String formattedDuration = formatDuration(durationSeconds);
routeInfo.add(new HorizontalLayout(new Span("Geschätzte Fahrtzeit:"),
routeInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.route.duration")),
new Span(formattedDuration)));
}
@@ -237,7 +236,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box");
H3 sectionTitle = new H3("Leistungen");
H3 sectionTitle = new H3(getTranslation("createinvoice.section.services"));
servicesSection.add(sectionTitle);
// Create grid with read-only rows
@@ -251,19 +250,19 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
return row.getService().getName();
}
return "";
}).setHeader("Leistung").setAutoWidth(true).setFlexGrow(2);
}).setHeader(getTranslation("createinvoice.column.service")).setAutoWidth(true).setFlexGrow(2);
// Calculation basis column (read-only)
servicesGrid.addColumn(row -> {
if (row.getService() != null && row.getService().getCalculationBasis() != null) {
return switch (row.getService().getCalculationBasis()) {
case DISTANCE -> "Gefahrene Kilometer";
case TIME -> "Zeit";
case FLAT_RATE -> "Pauschal";
case DISTANCE -> getTranslation("addjob.services.basis.distance");
case TIME -> getTranslation("addjob.services.basis.time");
case FLAT_RATE -> getTranslation("addjob.services.basis.flatrate");
};
}
return "";
}).setHeader("Berechnungsgrundlage").setAutoWidth(true).setFlexGrow(1);
}).setHeader(getTranslation("createinvoice.column.basis")).setAutoWidth(true).setFlexGrow(1);
// Price column (read-only)
servicesGrid.addColumn(row -> {
@@ -274,7 +273,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
}
}
return "";
}).setHeader("Preis").setAutoWidth(true).setFlexGrow(1).setKey("price");
}).setHeader(getTranslation("common.price")).setAutoWidth(true).setFlexGrow(1).setKey("price");
servicesGrid.setItems(gridRows);
servicesSection.add(servicesGrid);
@@ -292,7 +291,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box");
H3 sectionTitle = new H3("Zusammenfassung");
H3 sectionTitle = new H3(getTranslation("createinvoice.section.summary"));
section.add(sectionTitle);
// Calculate totals
@@ -306,14 +305,14 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
summaryInfo.setWidthFull();
// Show only net sum, VAT sums, and total amount without individual services
summaryInfo.add(new HorizontalLayout(new Span("Nettosumme:"),
summaryInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.summary.net")),
new Span(netAmount.setScale(2, RoundingMode.HALF_UP) + "")));
summaryInfo
.add(new HorizontalLayout(
new Span("Mehrwertsteuer ("
+ vatRate.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + "%):"),
new Span(getTranslation("createinvoice.summary.vat",
vatRate.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).toString())),
new Span(vatAmount.setScale(2, RoundingMode.HALF_UP) + "")));
summaryInfo.add(new HorizontalLayout(new Span("Gesamtbetrag (brutto):"),
summaryInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.summary.total")),
new Span(totalAmount.setScale(2, RoundingMode.HALF_UP) + "")));
section.add(summaryInfo);
@@ -399,7 +398,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
private void createInvoice() {
if (getSelectedServices().isEmpty()) {
Notification.show("Bitte wählen Sie mindestens eine Leistung aus", 3000, Notification.Position.BOTTOM_END);
Notification.show(getTranslation("createinvoice.notification.noservices"), 3000, Notification.Position.BOTTOM_END);
return;
}
@@ -412,7 +411,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
.flatMap(auth -> userRepository.findByEmail(auth.getUsername()));
if (currentUserOpt.isEmpty()) {
Notification.show("Fehler: Benutzer nicht gefunden", 3000, Notification.Position.BOTTOM_END);
Notification.show(getTranslation("createinvoice.notification.nouser"), 3000, Notification.Position.BOTTOM_END);
return;
}
@@ -421,16 +420,16 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
// Load invoice template from service
Optional<InvoiceTemplate> templateOpt = invoiceTemplateService.getTemplateByUserId(currentUser.getId().toString());
if (templateOpt.isEmpty()) {
Notification.show("Fehler: Kein Rechnungstemplate im Profil hinterlegt", 3000, Notification.Position.BOTTOM_END);
Notification.show(getTranslation("createinvoice.notification.notemplate"), 3000, Notification.Position.BOTTOM_END);
return;
}
String templateData = templateOpt.get().getTemplateData();
System.out.println("DEBUG CreateInvoiceView: Template data length: " + (templateData != null ? templateData.length() : 0));
System.out.println("DEBUG CreateInvoiceView: Template data preview: " + (templateData != null ? templateData.substring(0, Math.min(200, templateData.length())) : "null"));
if (templateData == null || templateData.isBlank()) {
Notification.show("Fehler: Kein Rechnungstemplate im Profil hinterlegt", 3000, Notification.Position.BOTTOM_END);
Notification.show(getTranslation("createinvoice.notification.notemplate"), 3000, Notification.Position.BOTTOM_END);
return;
}
@@ -443,7 +442,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
} catch (Exception ex) {
log.error("Fehler beim Erstellen der Rechnung", ex);
Notification.show("Fehler beim Erstellen der Rechnung: " + ex.getMessage(), 5000, Notification.Position.BOTTOM_END);
Notification.show(getTranslation("createinvoice.notification.error", ex.getMessage()), 5000, Notification.Position.BOTTOM_END);
}
}
@@ -589,11 +588,11 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
pdfFrame.getStyle().set("border", "none");
// Close button
Button closeButton = new Button("Schließen", e -> pdfDialog.close());
Button closeButton = new Button(getTranslation("button.close"), e -> pdfDialog.close());
closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
// Download button
Button downloadButton = new Button("Herunterladen", e -> {
Button downloadButton = new Button(getTranslation("button.download"), e -> {
getElement()
.executeJs("const link = document.createElement('a');" +
"link.href = 'data:application/pdf;base64," + base64Pdf + "';" +
@@ -617,4 +616,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
return String.format("%d Min.", minutes);
}
}
@Override
public String getPageTitle() {
return getTranslation("page.title.invoice.create");
}
}

View File

@@ -7,8 +7,8 @@ import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Menu;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.votianlt.model.Customer;
@@ -19,32 +19,31 @@ import java.time.Clock;
import static com.vaadin.flow.spring.data.VaadinSpringDataHelpers.toSpringPageRequest;
@Route(value = "customer", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@PageTitle("Kunden")
@Menu(order = 0, icon = "vaadin:clipboard-check", title = "Kunden")
public class CustomersView extends Main {
public class CustomersView extends Main implements HasDynamicTitle {
final TextField description;
final Button createBtn;
final Grid<Customer> todoGrid;
public CustomersView(CustomerService todoService, Clock clock) {
description = new TextField();
description.setPlaceholder("Suche");
description.setPlaceholder(getTranslation("jobs.filter.search"));
description.setMaxLength(255);
description.setMinWidth("20em");
createBtn = new Button("Kunde anlegen", event -> addCustomer());
createBtn = new Button(getTranslation("addcustomer.button.submit"), event -> addCustomer());
createBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
todoGrid = new Grid<>();
todoGrid.setItems(query -> todoService.list(toSpringPageRequest(query)).stream());
todoGrid.addColumn(Customer::getCompanyName).setHeader("Firmenname");
todoGrid.addColumn(Customer::getCompanyName).setHeader(getTranslation("customers.column.company"));
todoGrid.setSizeFull();
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
H2 title = new H2("Aufträge");
H2 title = new H2(getTranslation("customers.title"));
add(title);
add(todoGrid);
@@ -53,4 +52,9 @@ public class CustomersView extends Main {
private void addCustomer() {
UI.getCurrent().navigate("add_customer");
}
@Override
public String getPageTitle() {
return getTranslation("page.title.customers");
}
}

View File

@@ -16,7 +16,7 @@ import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.router.BeforeEvent;
import com.vaadin.flow.router.HasUrlParameter;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.AppUser;
import de.assecutor.votianlt.pages.service.AppUserService;
@@ -24,23 +24,22 @@ import jakarta.annotation.security.RolesAllowed;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("App-Nutzer bearbeiten")
@Route(value = "edit-app-user", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({ "USER", "ADMIN" })
public class EditAppUserView extends VerticalLayout implements HasUrlParameter<String> {
public class EditAppUserView extends VerticalLayout implements HasUrlParameter<String>, HasDynamicTitle {
private final AppUserService appUserService;
private AppUser appUser;
private final Binder<AppUser> binder = new Binder<>(AppUser.class);
// Form fields
private final TextField designationField = new TextField("Bezeichnung (HH H 000)");
private final TextField firstnameField = new TextField("Vorname");
private final TextField lastnameField = new TextField("Nachname");
private final TextField phoneField = new TextField("Telefon (Mobil)");
private final TextField emailField = new TextField("E-Mail-Adresse");
private final PasswordField changePasswordField = new PasswordField("Passwort ändern");
private final PasswordField confirmChangePasswordField = new PasswordField("Passwort ändern wiederholen");
// Form fields - labels set in constructor
private final TextField designationField = new TextField();
private final TextField firstnameField = new TextField();
private final TextField lastnameField = new TextField();
private final TextField phoneField = new TextField();
private final TextField emailField = new TextField();
private final PasswordField changePasswordField = new PasswordField();
private final PasswordField confirmChangePasswordField = new PasswordField();
@Autowired
public EditAppUserView(AppUserService appUserService) {
@@ -49,6 +48,15 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
setPadding(true);
setSpacing(true);
// Set field labels via i18n
designationField.setLabel(getTranslation("addappuser.designation"));
firstnameField.setLabel(getTranslation("profile.firstname"));
lastnameField.setLabel(getTranslation("profile.lastname"));
phoneField.setLabel(getTranslation("addappuser.phone"));
emailField.setLabel(getTranslation("profile.email"));
changePasswordField.setLabel(getTranslation("editappuser.password.change"));
confirmChangePasswordField.setLabel(getTranslation("editappuser.password.change.confirm"));
// Center content vertically
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
@@ -68,10 +76,10 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
header.setAlignItems(FlexComponent.Alignment.CENTER);
header.setSpacing(true);
H2 title = new H2("App-Nutzer bearbeiten");
H2 title = new H2(getTranslation("editappuser.title"));
title.getStyle().set("margin", "0");
Button backButton = new Button("Zurück", new Icon(VaadinIcon.ARROW_LEFT));
Button backButton = new Button(getTranslation("button.back"), new Icon(VaadinIcon.ARROW_LEFT));
backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
backButton.addClickListener(e -> navigateBack());
@@ -101,11 +109,9 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
// Configure password fields
changePasswordField.setWidthFull();
changePasswordField.setPlaceholder("Leer lassen, wenn nicht ändern");
changePasswordField.setPlaceholder(getTranslation("editappuser.password.placeholder"));
confirmChangePasswordField.setWidthFull();
confirmChangePasswordField.setPlaceholder("Leer lassen, wenn nicht ändern");
// Configure device dropdown
confirmChangePasswordField.setPlaceholder(getTranslation("editappuser.password.placeholder"));
// Add fields to form
formLayout.add(designationField);
@@ -122,10 +128,10 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
buttonLayout.setWidthFull();
Button saveButton = new Button("Speichern", e -> saveAppUser());
Button saveButton = new Button(getTranslation("button.savechanges"), e -> saveAppUser());
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Button deleteButton = new Button("Löschen", e -> deleteAppUser());
Button deleteButton = new Button(getTranslation("button.delete"), e -> deleteAppUser());
deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
buttonLayout.add(saveButton, deleteButton);
@@ -158,7 +164,7 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
binder.readBean(appUser);
} catch (IllegalArgumentException e) {
Notification.show("Ungültige App-Nutzer-ID", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("editappuser.notification.invalid.id"), 3000, Notification.Position.MIDDLE);
navigateBack();
}
}
@@ -185,7 +191,7 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
// Passwords match, set new password for hashing
appUser.setPassword(newPassword);
} else {
Notification.show("Passwörter stimmen nicht überein", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("editappuser.notification.password.mismatch"), 3000, Notification.Position.MIDDLE);
return;
}
} else {
@@ -194,10 +200,10 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
}
appUserService.updateAppUser(appUser);
Notification.show("App-Nutzer erfolgreich gespeichert", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("editappuser.notification.saved"), 3000, Notification.Position.MIDDLE);
navigateBack();
} catch (ValidationException e) {
Notification.show("Bitte überprüfen Sie die Eingaben", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("editappuser.notification.check"), 3000, Notification.Position.MIDDLE);
}
}
@@ -210,18 +216,18 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
boolean confirmPasswordFilled = confirmPassword != null && !confirmPassword.trim().isEmpty();
if (newPasswordFilled && !confirmPasswordFilled) {
Notification.show("Bitte bestätigen Sie das neue Passwort", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("editappuser.notification.password.confirm"), 3000, Notification.Position.MIDDLE);
return false;
}
if (!newPasswordFilled && confirmPasswordFilled) {
Notification.show("Bitte geben Sie das neue Passwort ein", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("editappuser.notification.password.enter"), 3000, Notification.Position.MIDDLE);
return false;
}
// If both are filled, they must match
if (newPasswordFilled && confirmPasswordFilled && newPassword != null && !newPassword.equals(confirmPassword)) {
Notification.show("Passwörter stimmen nicht überein", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("editappuser.notification.password.mismatch"), 3000, Notification.Position.MIDDLE);
return false;
}
@@ -231,20 +237,20 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
private void deleteAppUser() {
// Show confirmation dialog
com.vaadin.flow.component.dialog.Dialog confirmDialog = new com.vaadin.flow.component.dialog.Dialog();
confirmDialog.add("Möchten Sie diesen App-Nutzer wirklich löschen?");
confirmDialog.add(getTranslation("editappuser.dialog.delete.text"));
HorizontalLayout buttonLayout = new HorizontalLayout();
Button confirmDeleteButton = new Button("Ja, löschen", e -> {
Button confirmDeleteButton = new Button(getTranslation("editappuser.dialog.delete.confirm"), e -> {
if (appUser != null && appUser.getId() != null) {
appUserService.deleteById(appUser.getId());
Notification.show("App-Nutzer erfolgreich gelöscht", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("editappuser.notification.deleted"), 3000, Notification.Position.MIDDLE);
confirmDialog.close();
navigateBack();
}
});
confirmDeleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
Button cancelDeleteButton = new Button("Abbrechen", e -> confirmDialog.close());
Button cancelDeleteButton = new Button(getTranslation("button.cancel"), e -> confirmDialog.close());
buttonLayout.add(confirmDeleteButton, cancelDeleteButton);
buttonLayout.setSpacing(true);
@@ -256,4 +262,9 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
private void navigateBack() {
getUI().ifPresent(ui -> ui.navigate("app-user"));
}
@Override
public String getPageTitle() {
return getTranslation("page.title.appuser.edit");
}
}

View File

@@ -13,8 +13,8 @@ import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.router.BeforeEvent;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.HasUrlParameter;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.Customer;
import de.assecutor.votianlt.pages.service.CustomerService;
@@ -23,28 +23,27 @@ import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
@PageTitle("Kunde bearbeiten")
@Route(value = "edit-customer", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({ "USER", "ADMIN" })
public class EditCustomerView extends VerticalLayout implements HasUrlParameter<String> {
public class EditCustomerView extends VerticalLayout implements HasUrlParameter<String>, HasDynamicTitle {
private final CustomerService customerService;
private Customer customer;
private final Binder<Customer> binder = new Binder<>(Customer.class);
// Form fields
private final TextField titleField = new TextField("Titel");
private final TextField companyNameField = new TextField("Firmenname");
private final TextField firstnameField = new TextField("Vorname");
private final TextField lastNameField = new TextField("Nachname");
private final TextField telephoneField = new TextField("Telefon");
private final TextField faxField = new TextField("Fax");
private final EmailField mailField = new EmailField("E-Mail");
private final TextField streetField = new TextField("Straße");
private final TextField houseNumberField = new TextField("Hausnummer");
private final TextField addressAdditionField = new TextField("Adresszusatz");
private final TextField zipField = new TextField("PLZ");
private final TextField cityField = new TextField("Stadt");
// Form fields - labels set in constructor via setLabel()
private final TextField titleField = new TextField();
private final TextField companyNameField = new TextField();
private final TextField firstnameField = new TextField();
private final TextField lastNameField = new TextField();
private final TextField telephoneField = new TextField();
private final TextField faxField = new TextField();
private final EmailField mailField = new EmailField();
private final TextField streetField = new TextField();
private final TextField houseNumberField = new TextField();
private final TextField addressAdditionField = new TextField();
private final TextField zipField = new TextField();
private final TextField cityField = new TextField();
@Autowired
public EditCustomerView(CustomerService customerService) {
@@ -53,6 +52,20 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
setPadding(true);
setSpacing(true);
// Set field labels via i18n
titleField.setLabel(getTranslation("addjob.address.salutation"));
companyNameField.setLabel(getTranslation("profile.company"));
firstnameField.setLabel(getTranslation("profile.firstname"));
lastNameField.setLabel(getTranslation("profile.lastname"));
telephoneField.setLabel(getTranslation("profile.phone"));
faxField.setLabel(getTranslation("profile.fax"));
mailField.setLabel(getTranslation("profile.email"));
streetField.setLabel(getTranslation("profile.street"));
houseNumberField.setLabel(getTranslation("profile.housenr"));
addressAdditionField.setLabel(getTranslation("profile.addressadd"));
zipField.setLabel(getTranslation("profile.zip"));
cityField.setLabel(getTranslation("profile.city"));
// Center content vertically
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
@@ -68,7 +81,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
contentContainer.getStyle().set("box-shadow", "var(--lumo-box-shadow-s)");
// Header
H2 header = new H2("Kunde bearbeiten");
H2 header = new H2(getTranslation("editcustomer.title"));
header.getStyle().set("text-align", "center");
header.getStyle().set("margin", "0");
contentContainer.add(header);
@@ -98,11 +111,11 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
buttonLayout.setWidthFull();
Button saveButton = new Button("Speichern", e -> saveCustomer());
Button saveButton = new Button(getTranslation("button.savechanges"), e -> saveCustomer());
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Button cancelButton = new Button("Abbrechen", e -> navigateBack());
Button deleteButton = new Button("Löschen", e -> deleteCustomer());
Button cancelButton = new Button(getTranslation("button.cancel"), e -> navigateBack());
Button deleteButton = new Button(getTranslation("button.delete"), e -> deleteCustomer());
deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
buttonLayout.add(saveButton, cancelButton, deleteButton);
@@ -138,7 +151,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
customer = customerService.findById(customerId);
if (customer == null) {
Notification.show("Kunde nicht gefunden", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("editcustomer.notification.notfound"), 3000, Notification.Position.MIDDLE);
navigateBack();
return;
}
@@ -147,7 +160,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
binder.readBean(customer);
} catch (IllegalArgumentException e) {
Notification.show("Ungültige Kunden-ID", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("editcustomer.notification.invalid.id"), 3000, Notification.Position.MIDDLE);
navigateBack();
}
}
@@ -156,29 +169,29 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
try {
binder.writeBean(customer);
customerService.save(customer);
Notification.show("Kunde erfolgreich gespeichert", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("editcustomer.notification.saved"), 3000, Notification.Position.MIDDLE);
navigateBack();
} catch (ValidationException e) {
Notification.show("Bitte überprüfen Sie die Eingaben", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("editcustomer.notification.check"), 3000, Notification.Position.MIDDLE);
}
}
private void deleteCustomer() {
// Show confirmation dialog
Dialog confirmDialog = new Dialog();
confirmDialog.add("Möchten Sie diesen Kunden wirklich löschen?");
confirmDialog.add(getTranslation("editcustomer.dialog.delete.text"));
HorizontalLayout buttonLayout = new HorizontalLayout();
Button confirmDeleteButton = new Button("Ja, löschen", e -> {
Button confirmDeleteButton = new Button(getTranslation("editcustomer.dialog.delete.confirm"), e -> {
if (customer != null && customer.getId() != null) {
Notification.show("Kunde erfolgreich gelöscht", 3000, Notification.Position.MIDDLE);
Notification.show(getTranslation("editcustomer.notification.deleted"), 3000, Notification.Position.MIDDLE);
confirmDialog.close();
navigateBack();
}
});
confirmDeleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR);
Button cancelDeleteButton = new Button("Abbrechen", e -> confirmDialog.close());
Button cancelDeleteButton = new Button(getTranslation("button.cancel"), e -> confirmDialog.close());
buttonLayout.add(confirmDeleteButton, cancelDeleteButton);
buttonLayout.setSpacing(true);
@@ -190,4 +203,9 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
private void navigateBack() {
getUI().ifPresent(ui -> ui.navigate("customers"));
}
@Override
public String getPageTitle() {
return getTranslation("page.title.customer.edit");
}
}

View File

@@ -30,8 +30,9 @@ import com.vaadin.flow.component.upload.Upload;
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.validator.EmailValidator;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.Language;
import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.model.Service;
import de.assecutor.votianlt.model.UserInvoiceData;
@@ -43,6 +44,7 @@ import de.assecutor.votianlt.repository.ServiceRepository;
import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.service.CustomerInvoiceService;
import de.assecutor.votianlt.service.InvoiceTemplateService;
import de.assecutor.votianlt.service.LanguageService;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.textfield.NumberField;
@@ -51,11 +53,10 @@ import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.ClientCallable;
import jakarta.annotation.security.RolesAllowed;
@PageTitle("Profil bearbeiten")
@Route(value = "edit-profile", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({ "USER", "ADMIN" })
@JsModule("./invoice-generator/profile-invoice-generator.js")
public class EditProfileView extends HorizontalLayout {
public class EditProfileView extends HorizontalLayout implements HasDynamicTitle {
private final TextField prefixField;
private final TextField ustIdField;
private final TextField taxNumberField;
@@ -78,14 +79,20 @@ public class EditProfileView extends HorizontalLayout {
private final ServiceRepository serviceRepository;
private Grid<Service> servicesGrid;
// Store the original language from the database
private final Language originalLanguage;
public EditProfileView(UserService userService, UserInvoiceDataService userInvoiceDataService,
CustomerInvoiceService customerInvoiceService, InvoiceTemplateService invoiceTemplateService,
SecurityService securityService, ServiceRepository serviceRepository) {
LanguageService languageService, SecurityService securityService, ServiceRepository serviceRepository) {
this.userInvoiceDataService = userInvoiceDataService;
this.customerInvoiceService = customerInvoiceService;
this.invoiceTemplateService = invoiceTemplateService;
this.currentUser = securityService.getCurrentDatabaseUser();
this.serviceRepository = serviceRepository;
// Store the original language before any changes
this.originalLanguage = this.currentUser != null ? this.currentUser.getLanguage() : Language.DE;
setSizeFull();
setPadding(true);
setSpacing(true);
@@ -113,53 +120,53 @@ public class EditProfileView extends HorizontalLayout {
form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 2));
// Firmenfelder
TextField companyField = new TextField("Firma");
companyField.addBlurListener(e -> validateField(companyField, "Firma ist ein Pflichtfeld"));
TextField companyField = new TextField(getTranslation("profile.company"));
companyField.addBlurListener(e -> validateField(companyField, getTranslation("profile.validation.company")));
TextField companyAddField = new TextField("Firmenzusatz");
TextField companyAddField = new TextField(getTranslation("profile.companyadd"));
TextField firstnameField = new TextField("Vorname");
firstnameField.addBlurListener(e -> validateField(firstnameField, "Vorname ist ein Pflichtfeld"));
TextField firstnameField = new TextField(getTranslation("profile.firstname"));
firstnameField.addBlurListener(e -> validateField(firstnameField, getTranslation("profile.validation.firstname")));
TextField lastnameField = new TextField("Nachname");
lastnameField.addBlurListener(e -> validateField(lastnameField, "Nachname ist ein Pflichtfeld"));
TextField lastnameField = new TextField(getTranslation("profile.lastname"));
lastnameField.addBlurListener(e -> validateField(lastnameField, getTranslation("profile.validation.lastname")));
TextField phoneField = new TextField("Telefonnummer");
phoneField.addBlurListener(e -> validateField(phoneField, "Telefonnummer ist ein Pflichtfeld"));
TextField phoneField = new TextField(getTranslation("profile.phone"));
phoneField.addBlurListener(e -> validateField(phoneField, getTranslation("profile.validation.phone")));
TextField faxField = new TextField("Telefon (Fax)");
TextField mobileField = new TextField("Telefon (Mobil)");
TextField faxField = new TextField(getTranslation("profile.fax"));
TextField mobileField = new TextField(getTranslation("profile.mobile"));
EmailField emailField = new EmailField("E-Mail-Adresse (Login)*");
EmailField emailField = new EmailField(getTranslation("profile.email.required"));
emailField.addBlurListener(e -> validateEmailField(emailField));
TextField streetField = new TextField("Straße");
streetField.addBlurListener(e -> validateField(streetField, "Straße ist ein Pflichtfeld"));
TextField streetField = new TextField(getTranslation("profile.street"));
streetField.addBlurListener(e -> validateField(streetField, getTranslation("profile.validation.street")));
TextField houseNumberField = new TextField("Hausnr");
houseNumberField.addBlurListener(e -> validateField(houseNumberField, "Hausnummer ist ein Pflichtfeld"));
TextField houseNumberField = new TextField(getTranslation("profile.housenr"));
houseNumberField.addBlurListener(e -> validateField(houseNumberField, getTranslation("profile.validation.housenr")));
TextField addressAddField = new TextField("Adresszusatz");
TextField addressAddField = new TextField(getTranslation("profile.addressadd"));
TextField zipField = new TextField("Postleitzahl");
zipField.addBlurListener(e -> validateField(zipField, "Postleitzahl ist ein Pflichtfeld"));
TextField zipField = new TextField(getTranslation("profile.zip"));
zipField.addBlurListener(e -> validateField(zipField, getTranslation("profile.validation.zip")));
TextField cityField = new TextField("Stadt");
cityField.addBlurListener(e -> validateField(cityField, "Stadt ist ein Pflichtfeld"));
TextField cityField = new TextField(getTranslation("profile.city"));
cityField.addBlurListener(e -> validateField(cityField, getTranslation("profile.validation.city")));
// Abweichende Rechnungsadresse
Checkbox diffInvoiceAddress = new Checkbox("Abweichende Rechnungsadresse");
Checkbox diffInvoiceAddress = new Checkbox(getTranslation("profile.diffinvoice"));
diffInvoiceAddress.getStyle().set("marginTop", "1em");
// Rechnungsadresse Felder (disabled by default)
TextField invCompanyField = new TextField("Firma");
TextField invCompanyAddField = new TextField("Firmenzusatz");
TextField invFirstnameField = new TextField("Vorname");
TextField invLastnameField = new TextField("Nachname");
TextField invStreetField = new TextField("Straße");
TextField invHouseNumberField = new TextField("Hausnr");
TextField invAddressAddField = new TextField("Adresszusatz");
TextField invZipField = new TextField("Postleitzahl");
TextField invCityField = new TextField("Stadt");
TextField invCompanyField = new TextField(getTranslation("profile.company"));
TextField invCompanyAddField = new TextField(getTranslation("profile.companyadd"));
TextField invFirstnameField = new TextField(getTranslation("profile.firstname"));
TextField invLastnameField = new TextField(getTranslation("profile.lastname"));
TextField invStreetField = new TextField(getTranslation("profile.street"));
TextField invHouseNumberField = new TextField(getTranslation("profile.housenr"));
TextField invAddressAddField = new TextField(getTranslation("profile.addressadd"));
TextField invZipField = new TextField(getTranslation("profile.zip"));
TextField invCityField = new TextField(getTranslation("profile.city"));
invCompanyField.setEnabled(false);
invCompanyAddField.setEnabled(false);
invFirstnameField.setEnabled(false);
@@ -220,22 +227,22 @@ public class EditProfileView extends HorizontalLayout {
cityField.setRequiredIndicatorVisible(true);
// Hauptadresse binden
binder.forField(companyField).asRequired("Firma ist erforderlich").bind(User::getCompany, User::setCompany);
binder.forField(companyField).asRequired(getTranslation("profile.validation.company.required")).bind(User::getCompany, User::setCompany);
binder.forField(companyAddField).bind(User::getCompanyAddition, User::setCompanyAddition);
binder.forField(streetField).asRequired("Straße ist erforderlich").bind(User::getStreet, User::setStreet);
binder.forField(houseNumberField).asRequired("Hausnummer ist erforderlich").bind(User::getHouseNumber,
binder.forField(streetField).asRequired(getTranslation("profile.validation.street.required")).bind(User::getStreet, User::setStreet);
binder.forField(houseNumberField).asRequired(getTranslation("profile.validation.housenr.required")).bind(User::getHouseNumber,
User::setHouseNumber);
binder.forField(addressAddField).bind(User::getAddressAddition, User::setAddressAddition);
binder.forField(zipField).asRequired("Postleitzahl ist erforderlich").bind(User::getZip, User::setZip);
binder.forField(cityField).asRequired("Stadt ist erforderlich").bind(User::getCity, User::setCity);
binder.forField(zipField).asRequired(getTranslation("profile.validation.zip.required")).bind(User::getZip, User::setZip);
binder.forField(cityField).asRequired(getTranslation("profile.validation.city.required")).bind(User::getCity, User::setCity);
// Personendaten binden
binder.forField(firstnameField).asRequired("Vorname ist erforderlich").bind(User::getFirstname,
binder.forField(firstnameField).asRequired(getTranslation("profile.validation.firstname.required")).bind(User::getFirstname,
User::setFirstname);
binder.forField(lastnameField).asRequired("Nachname ist erforderlich").bind(User::getName, User::setName);
binder.forField(phoneField).asRequired("Telefonnummer ist erforderlich").bind(User::getPhone, User::setPhone);
binder.forField(emailField).asRequired("E-Mail ist erforderlich")
.withValidator(new EmailValidator("Ungültige E-Mail-Adresse")).bind(User::getEmail, User::setEmail);
binder.forField(lastnameField).asRequired(getTranslation("profile.validation.lastname.required")).bind(User::getName, User::setName);
binder.forField(phoneField).asRequired(getTranslation("profile.validation.phone.required")).bind(User::getPhone, User::setPhone);
binder.forField(emailField).asRequired(getTranslation("profile.validation.email.required"))
.withValidator(new EmailValidator(getTranslation("profile.validation.email.invalid"))).bind(User::getEmail, User::setEmail);
// Optionale Felder
binder.forField(mobileField).bind(User::getPhone2, User::setPhone2);
binder.forField(faxField).bind(User::getFax, User::setFax);
@@ -272,7 +279,7 @@ public class EditProfileView extends HorizontalLayout {
form.add(invAddressAddField, 2);
form.add(invZipField, invCityField);
tabSheet.add("Stammdaten", form);
tabSheet.add(getTranslation("profile.basicdata"), form);
// Karte (2. Tab)
Span coordsLabel = new Span("53°36'25.1\"N 10°06'46.9\"E");
@@ -287,7 +294,7 @@ public class EditProfileView extends HorizontalLayout {
mapTab.setPadding(false);
mapTab.setSpacing(true);
mapTab.add(coordsLabel, mapDiv);
tabSheet.add("Karte", mapTab);
tabSheet.add(getTranslation("profile.map"), mapTab);
// Dritter Tab: Rechnungserstellung
VerticalLayout billingTab = new VerticalLayout();
billingTab.setWidthFull();
@@ -307,7 +314,7 @@ public class EditProfileView extends HorizontalLayout {
pdfFrame = new IFrame();
// Nur die Checkbox "Rechnungslegung über votianLT"
billingEnabled = new Checkbox("Rechnungslegung über votianLT");
billingEnabled = new Checkbox(getTranslation("profile.billing.enabled"));
billingEnabled.setValue(true); // Standardmäßig aktiviert
billingTab.add(billingEnabled);
@@ -368,25 +375,25 @@ public class EditProfileView extends HorizontalLayout {
actionLayout.setSpacing(true);
actionLayout.getStyle().set("margin-top", "var(--lumo-space-s)");
Button clearButton = new Button("Leeren", new Icon(VaadinIcon.TRASH));
Button clearButton = new Button(getTranslation("button.clear"), new Icon(VaadinIcon.TRASH));
clearButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
clearButton.addClickListener(e -> {
getElement().executeJs("if (window.clearProfileCanvas) { window.clearProfileCanvas(); }");
Notification.show("Canvas wurde geleert", 2000, Notification.Position.BOTTOM_CENTER);
Notification.show(getTranslation("profile.canvas.cleared"), 2000, Notification.Position.BOTTOM_CENTER);
});
Button previewPdfButton = new Button("Vorschau", new Icon(VaadinIcon.EYE));
Button previewPdfButton = new Button(getTranslation("button.preview"), new Icon(VaadinIcon.EYE));
previewPdfButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
previewPdfButton.addClickListener(e -> generatePreviewPdfFromProfile());
Button saveTemplateButton = new Button("Template speichern", new Icon(VaadinIcon.DOWNLOAD));
Button saveTemplateButton = new Button(getTranslation("button.savetemplate"), new Icon(VaadinIcon.DOWNLOAD));
saveTemplateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
saveTemplateButton.addClickListener(e -> {
getElement().executeJs(
"if (window.getProfileCanvasData) { return JSON.stringify(window.getProfileCanvasData()); } else { return null; }")
.then(result -> {
if (result == null) {
Notification.show("Fehler: Canvas-Daten konnten nicht gelesen werden", 3000,
Notification.show(getTranslation("profile.canvas.read.error"), 3000,
Notification.Position.BOTTOM_CENTER);
return;
}
@@ -399,10 +406,10 @@ public class EditProfileView extends HorizontalLayout {
templateData = result.toString();
}
invoiceTemplateService.saveTemplate(currentUser.getId().toString(), templateData);
Notification.show("Template erfolgreich gespeichert", 3000,
Notification.show(getTranslation("profile.template.saved"), 3000,
Notification.Position.BOTTOM_CENTER);
} catch (Exception ex) {
Notification.show("Fehler beim Speichern: " + ex.getMessage(), 5000,
Notification.show(getTranslation("profile.save.error", ex.getMessage()), 5000,
Notification.Position.BOTTOM_CENTER);
}
});
@@ -414,7 +421,7 @@ public class EditProfileView extends HorizontalLayout {
// Initialen Zustand setzen (sichtbar da checkbox standardmäßig true)
actionLayout.setVisible(true);
tabSheet.add("Rechnungserstellung", billingTab);
tabSheet.add(getTranslation("profile.invoicecreation"), billingTab);
// Sichtbarkeit des Invoice Generators an Checkbox binden
billingEnabled.addValueChangeListener(e -> {
@@ -429,7 +436,7 @@ public class EditProfileView extends HorizontalLayout {
// Initialize invoice generator when the billing tab is selected
// Also register this view instance for JavaScript callbacks
tabSheet.addSelectedChangeListener(e -> {
if ("Rechnungserstellung".equals(e.getSelectedTab().getLabel())) {
if (getTranslation("profile.invoicecreation").equals(e.getSelectedTab().getLabel())) {
getElement().executeJs("window.invoiceGeneratorViewProfile = $0;" + "setTimeout(function() { "
+ " if (window.initProfileInvoiceGenerator) { " + " window.initProfileInvoiceGenerator(); "
+ " console.log('Canvas initialized, now loading template...'); "
@@ -444,17 +451,17 @@ public class EditProfileView extends HorizontalLayout {
switches.setPadding(false);
switches.setSpacing(true);
Checkbox digitalProcess = new Checkbox("Digitale Abwicklung per App");
Checkbox digitalProcess = new Checkbox(getTranslation("profile.settings.digitalprocess"));
digitalProcess.setValue(currentUser.isDigitalProcessingEnabled());
Span digitalProcessInfo = new Span("Aktiviert die digitale Auftragsabwicklung über die mobile App");
Span digitalProcessInfo = new Span(getTranslation("profile.settings.digitalprocess.info"));
digitalProcessInfo.getStyle().set("font-size", "var(--lumo-font-size-s)")
.set("color", "var(--lumo-secondary-text-color)").set("margin-left", "var(--lumo-space-xl)");
Checkbox locateAppUser = new Checkbox("App-Nutzer orten");
Checkbox locateAppUser = new Checkbox(getTranslation("profile.settings.locateappuser"));
locateAppUser.setValue(currentUser.isLocationTrackingEnabled());
Span locateAppUserInfo = new Span("Ermöglicht die Ortung von App-Nutzern während der Auftragsausführung");
Span locateAppUserInfo = new Span(getTranslation("profile.settings.locateappuser.info"));
locateAppUserInfo.getStyle().set("font-size", "var(--lumo-font-size-s)")
.set("color", "var(--lumo-secondary-text-color)").set("margin-left", "var(--lumo-space-xl)");
@@ -468,7 +475,42 @@ public class EditProfileView extends HorizontalLayout {
});
switches.add(digitalProcess, digitalProcessInfo, locateAppUser, locateAppUserInfo);
tabSheet.add("Einstellungen", switches);
tabSheet.add(getTranslation("profile.settings"), switches);
// Spracheinstellung unter Einstellungen
HorizontalLayout languageLayout = new HorizontalLayout();
languageLayout.setWidthFull();
languageLayout.setSpacing(true);
languageLayout.setAlignItems(FlexComponent.Alignment.CENTER);
// Sprache Label
Span languageLabel = new Span(getTranslation("profile.language"));
languageLabel.getStyle().set("font-size", "var(--lumo-font-size-m)").set("font-weight", "500");
// ComboBox mit Flaggen-Icons
ComboBox<Language> languageCombo = new ComboBox<>();
languageCombo.setWidth("200px");
languageCombo.setItems(Language.values());
languageCombo.setItemLabelGenerator(language -> {
// Flaggen-Emoji für jede Sprache
String flag = switch (language) {
case DE -> "🇩🇪 ";
case EN -> "🇬🇧 ";
case FR -> "🇫🇷 ";
case ES -> "🇪🇸 ";
};
return flag + language.getDisplayName();
});
languageCombo.setValue(currentUser.getLanguage());
// Store the selected language temporarily, but don't save yet
languageCombo.addValueChangeListener(e -> {
// Language will be saved when the user clicks save button
currentUser.setLanguage(e.getValue());
});
languageLayout.add(languageLabel, languageCombo);
switches.add(languageLayout);
// Sicherheit-Tab (2FA, Passwort, Konto)
VerticalLayout securityTab = new VerticalLayout();
@@ -476,7 +518,7 @@ public class EditProfileView extends HorizontalLayout {
securityTab.setSpacing(true);
// 2-Faktor Auth
Checkbox twoFactor = new Checkbox("2-Faktor-Authentifizierung");
Checkbox twoFactor = new Checkbox(getTranslation("profile.security.twofactor"));
twoFactor.setValue(currentUser.isTwoFactorEnabled());
twoFactor.addValueChangeListener(e -> currentUser.setTwoFactorEnabled(e.getValue()));
Icon twoFactorInfo = VaadinIcon.QUESTION_CIRCLE_O.create();
@@ -484,30 +526,30 @@ public class EditProfileView extends HorizontalLayout {
HorizontalLayout twoFactorLayout = new HorizontalLayout(twoFactor, twoFactorInfo);
twoFactorLayout.setAlignItems(Alignment.CENTER);
Span twoFactorDescription = new Span("Bei Aktivierung wird bei jeder Anmeldung ein Code per E-Mail gesendet");
Span twoFactorDescription = new Span(getTranslation("profile.security.twofactor.info"));
twoFactorDescription.getStyle().set("font-size", "var(--lumo-font-size-s)")
.set("color", "var(--lumo-secondary-text-color)").set("margin-left", "var(--lumo-space-xl)");
securityTab.add(twoFactorLayout, twoFactorDescription);
// Passwort ändern Button
Button changePassword = new Button("Passwort ändern");
Button changePassword = new Button(getTranslation("button.changepassword"));
changePassword.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
securityTab.add(changePassword);
// Benutzerkonto löschen Button
Button deleteAccount = new Button("Benutzerkonto löschen");
Button deleteAccount = new Button(getTranslation("button.deleteaccount"));
deleteAccount.addThemeVariants(ButtonVariant.LUMO_ERROR);
securityTab.add(deleteAccount);
tabSheet.add("Konto", securityTab);
tabSheet.add(getTranslation("profile.account"), securityTab);
// Leistungskatalog Tab
VerticalLayout servicesTab = createServicesTab();
tabSheet.add("Leistungskatalog", servicesTab);
tabSheet.add(getTranslation("profile.services"), servicesTab);
// Profil speichern Button (unten rechts)
Button saveProfile = new Button("Profiländerungen speichern");
Button saveProfile = new Button(getTranslation("button.save"));
saveProfile.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
saveProfile.addClickListener(e -> {
// Validate all required fields first
@@ -515,7 +557,7 @@ public class EditProfileView extends HorizontalLayout {
emailField, streetField, houseNumberField, zipField, cityField);
if (!isValid) {
Notification.show("Bitte füllen Sie alle Pflichtfelder korrekt aus", 3000,
Notification.show(getTranslation("profile.validation.required.fill"), 3000,
Notification.Position.MIDDLE);
return;
}
@@ -529,14 +571,17 @@ public class EditProfileView extends HorizontalLayout {
binder.writeBean(currentUser);
userService.save(currentUser);
saveInvoiceData();
Notification.show("Profil gespeichert", 3000, Notification.Position.BOTTOM_END);
Notification.show(getTranslation("profile.saved"), 3000, Notification.Position.BOTTOM_END);
// Always reload if billing status changed to update the sidebar
if (oldBillingStatus != newBillingStatus) {
// Check if language changed (compare with original language from database)
boolean languageChanged = originalLanguage != currentUser.getLanguage();
// Reload if billing status changed or language changed
if (oldBillingStatus != newBillingStatus || languageChanged) {
UI.getCurrent().getPage().reload();
}
} catch (Exception ex) {
Notification.show("Fehler beim Speichern: " + ex.getMessage(), 4000, Notification.Position.MIDDLE);
Notification.show(getTranslation("profile.save.error", ex.getMessage()), 4000, Notification.Position.MIDDLE);
}
}
});
@@ -594,7 +639,7 @@ public class EditProfileView extends HorizontalLayout {
}
} catch (Exception e) {
// Log error or show notification
Notification.show("Fehler bei PDF-Generierung: " + e.getMessage(), 3000, Notification.Position.BOTTOM_END);
Notification.show(getTranslation("profile.pdf.error", e.getMessage()), 3000, Notification.Position.BOTTOM_END);
}
}
@@ -753,10 +798,10 @@ public class EditProfileView extends HorizontalLayout {
String value = emailField.getValue();
if (value == null || value.trim().isEmpty()) {
emailField.setInvalid(true);
emailField.setErrorMessage("E-Mail-Adresse ist ein Pflichtfeld");
emailField.setErrorMessage(getTranslation("profile.validation.email.required"));
} else if (!value.contains("@") || !value.contains(".")) {
emailField.setInvalid(true);
emailField.setErrorMessage("Bitte geben Sie eine gültige E-Mail-Adresse ein");
emailField.setErrorMessage(getTranslation("profile.validation.email.invalid"));
} else {
emailField.setInvalid(false);
emailField.setErrorMessage("");
@@ -766,15 +811,15 @@ public class EditProfileView extends HorizontalLayout {
private boolean validateAllProfileFields(TextField companyField, TextField firstnameField, TextField lastnameField,
TextField phoneField, EmailField emailField, TextField streetField, TextField houseNumberField,
TextField zipField, TextField cityField) {
validateField(companyField, "Firma ist ein Pflichtfeld");
validateField(firstnameField, "Vorname ist ein Pflichtfeld");
validateField(lastnameField, "Nachname ist ein Pflichtfeld");
validateField(phoneField, "Telefonnummer ist ein Pflichtfeld");
validateField(companyField, getTranslation("profile.validation.company"));
validateField(firstnameField, getTranslation("profile.validation.firstname"));
validateField(lastnameField, getTranslation("profile.validation.lastname"));
validateField(phoneField, getTranslation("profile.validation.phone"));
validateEmailField(emailField);
validateField(streetField, "Straße ist ein Pflichtfeld");
validateField(houseNumberField, "Hausnummer ist ein Pflichtfeld");
validateField(zipField, "Postleitzahl ist ein Pflichtfeld");
validateField(cityField, "Stadt ist ein Pflichtfeld");
validateField(streetField, getTranslation("profile.validation.street"));
validateField(houseNumberField, getTranslation("profile.validation.housenr"));
validateField(zipField, getTranslation("profile.validation.zip"));
validateField(cityField, getTranslation("profile.validation.city"));
return !companyField.isInvalid() && !firstnameField.isInvalid() && !lastnameField.isInvalid()
&& !phoneField.isInvalid() && !emailField.isInvalid() && !streetField.isInvalid()
@@ -788,7 +833,7 @@ public class EditProfileView extends HorizontalLayout {
"if (window.getProfileCanvasData) { return window.getProfileCanvasData(); } else { return null; }")
.then(result -> {
if (result == null) {
Notification.show("Fehler: Canvas-Daten konnten nicht gelesen werden", 3000,
Notification.show(getTranslation("profile.canvas.read.error"), 3000,
Notification.Position.BOTTOM_CENTER);
return;
}
@@ -804,12 +849,12 @@ public class EditProfileView extends HorizontalLayout {
currentUser);
showPdfInDialog(pdfBytes);
} catch (Exception ex) {
Notification.show("Fehler beim Generieren der Vorschau: " + ex.getMessage(), 3000,
Notification.show(getTranslation("profile.pdf.preview.error", ex.getMessage()), 3000,
Notification.Position.BOTTOM_CENTER);
}
});
} catch (Exception ex) {
Notification.show("Fehler: " + ex.getMessage(), 3000, Notification.Position.BOTTOM_CENTER);
Notification.show(getTranslation("profile.error", ex.getMessage()), 3000, Notification.Position.BOTTOM_CENTER);
}
}
@@ -820,7 +865,7 @@ public class EditProfileView extends HorizontalLayout {
// Create dialog
Dialog pdfDialog = new Dialog();
pdfDialog.setHeaderTitle("PDF Vorschau");
pdfDialog.setHeaderTitle(getTranslation("profile.pdf.preview"));
pdfDialog.setWidth("90vw");
pdfDialog.setHeight("90vh");
@@ -840,11 +885,11 @@ public class EditProfileView extends HorizontalLayout {
pdfContainer.add(pdfFrame);
// Close button
Button closeButton = new Button("Schließen", e -> pdfDialog.close());
Button closeButton = new Button(getTranslation("button.close"), e -> pdfDialog.close());
closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
// Download button
Button downloadButton = new Button("Herunterladen", e -> {
Button downloadButton = new Button(getTranslation("button.download"), e -> {
getElement()
.executeJs("const link = document.createElement('a');" + "link.href = 'data:application/pdf;base64,"
+ base64Pdf + "';" + "link.download = 'vorschau.pdf';" + "link.click();");
@@ -864,7 +909,7 @@ public class EditProfileView extends HorizontalLayout {
panel.setHeightFull();
// Bereich 1: Meine Stammdaten
Span invoiceHeader = new Span("Meine Stammdaten");
Span invoiceHeader = new Span(getTranslation("profile.invoice.masterdata"));
invoiceHeader.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-m)")
.set("margin-top", "var(--lumo-space-s)");
@@ -876,67 +921,67 @@ public class EditProfileView extends HorizontalLayout {
String email = safe(currentUser.getEmail());
String phone = safe(currentUser.getPhone());
Div senderCompany = createVariableTemplate("Firma", VaadinIcon.OFFICE, "masterdata.company_name",
company.isEmpty() ? "Ihre Firma" : company);
Div senderName = createVariableTemplate("Name", VaadinIcon.USER, "masterdata.contact_name",
fullName.trim().isEmpty() ? "Ihr Name" : fullName.trim());
Div senderAddress = createVariableTemplate("Straße", VaadinIcon.MAP_MARKER, "masterdata.street",
street.trim().isEmpty() ? "Ihre Straße" : street.trim());
Div senderCity = createVariableTemplate("Ort", VaadinIcon.BUILDING, "masterdata.city",
city.trim().isEmpty() ? "PLZ Ort" : city.trim());
Div senderEmail = createVariableTemplate("E-Mail", VaadinIcon.ENVELOPE, "masterdata.email",
email.isEmpty() ? "ihre@email.de" : email);
Div senderPhone = createVariableTemplate("Telefon", VaadinIcon.PHONE, "masterdata.phone",
phone.isEmpty() ? "Ihre Telefonnummer" : phone);
Div senderCompany = createVariableTemplate(getTranslation("profile.company"), VaadinIcon.OFFICE, "masterdata.company_name",
company.isEmpty() ? getTranslation("profile.invoice.placeholder.company") : company);
Div senderName = createVariableTemplate(getTranslation("profile.invoice.name"), VaadinIcon.USER, "masterdata.contact_name",
fullName.trim().isEmpty() ? getTranslation("profile.invoice.placeholder.name") : fullName.trim());
Div senderAddress = createVariableTemplate(getTranslation("profile.street"), VaadinIcon.MAP_MARKER, "masterdata.street",
street.trim().isEmpty() ? getTranslation("profile.invoice.placeholder.street") : street.trim());
Div senderCity = createVariableTemplate(getTranslation("profile.invoice.city"), VaadinIcon.BUILDING, "masterdata.city",
city.trim().isEmpty() ? getTranslation("profile.invoice.placeholder.city") : city.trim());
Div senderEmail = createVariableTemplate(getTranslation("profile.invoice.email"), VaadinIcon.ENVELOPE, "masterdata.email",
email.isEmpty() ? getTranslation("profile.invoice.placeholder.email") : email);
Div senderPhone = createVariableTemplate(getTranslation("profile.invoice.phone"), VaadinIcon.PHONE, "masterdata.phone",
phone.isEmpty() ? getTranslation("profile.invoice.placeholder.phone") : phone);
// Bereich 2: Leistungen
Span servicesHeader = new Span("Leistungen");
Span servicesHeader = new Span(getTranslation("profile.services.label"));
servicesHeader.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-m)")
.set("margin-top", "var(--lumo-space-m)");
// Leistungen als draggable Variable
Div servicesListBlock = createServicesVariableTemplate("Leistungen auflisten", VaadinIcon.LIST, "services.list",
Div servicesListBlock = createServicesVariableTemplate(getTranslation("profile.invoice.services.list"), VaadinIcon.LIST, "services.list",
"Artikel 1: 100,00 €\nArtikel 2: 50,00 €");
Div servicesNetBlock = createServicesVariableTemplate("Nettosumme", VaadinIcon.COIN_PILES, "services.net_total",
Div servicesNetBlock = createServicesVariableTemplate(getTranslation("profile.invoice.net"), VaadinIcon.COIN_PILES, "services.net_total",
"150,00 €");
Div servicesVatBlock = createServicesVariableTemplate("Umsatzsteuer", VaadinIcon.COIN_PILES,
Div servicesVatBlock = createServicesVariableTemplate(getTranslation("profile.invoice.vat"), VaadinIcon.COIN_PILES,
"services.vat_total", "28,50 €");
Div servicesGrossBlock = createServicesVariableTemplate("Bruttosumme", VaadinIcon.MONEY, "services.gross_total",
Div servicesGrossBlock = createServicesVariableTemplate(getTranslation("profile.invoice.gross"), VaadinIcon.MONEY, "services.gross_total",
"178,50 €");
// Bereich 3: Kundendaten
Span customerHeader = new Span("Kundendaten");
Span customerHeader = new Span(getTranslation("profile.invoice.customerdata"));
customerHeader.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-m)")
.set("margin-top", "var(--lumo-space-m)");
// Kundendaten als Variablen (grün hinterlegt)
Div customerCompany = createCustomerVariableTemplate("Kunde Firma", VaadinIcon.OFFICE, "customer.company_name",
Div customerCompany = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.company"), VaadinIcon.OFFICE, "customer.company_name",
"Kundenfirma GmbH");
Div customerName = createCustomerVariableTemplate("Kunde Name", VaadinIcon.USER, "customer.contact_name",
Div customerName = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.name"), VaadinIcon.USER, "customer.contact_name",
"Erika Mustermann");
Div customerAddress = createCustomerVariableTemplate("Kunde Straße", VaadinIcon.MAP_MARKER, "customer.street",
Div customerAddress = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.street"), VaadinIcon.MAP_MARKER, "customer.street",
"Kundenstraße 456");
Div customerCity = createCustomerVariableTemplate("Kunde Ort", VaadinIcon.BUILDING, "customer.city",
Div customerCity = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.city"), VaadinIcon.BUILDING, "customer.city",
"54321 Kundenstadt");
Div customerEmail = createCustomerVariableTemplate("Kunde E-Mail", VaadinIcon.ENVELOPE, "customer.email",
Div customerEmail = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.email"), VaadinIcon.ENVELOPE, "customer.email",
"kunde@beispiel.de");
Div customerPhone = createCustomerVariableTemplate("Kunde Telefon", VaadinIcon.PHONE, "customer.phone",
Div customerPhone = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.phone"), VaadinIcon.PHONE, "customer.phone",
"0987 654321");
// Bereich 2: Freie Elemente
Span freeHeader = new Span("Freie Elemente");
Span freeHeader = new Span(getTranslation("profile.invoice.free.elements"));
freeHeader.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-m)").set("margin-top",
"var(--lumo-space-m)");
// Draggable Templates
Div textBlock = createDraggableTemplate("Textfeld", VaadinIcon.TEXT_LABEL, "text");
Div headerBlock = createDraggableTemplate("Überschrift", VaadinIcon.HEADER, "header");
Div dateBlock = createDraggableTemplate("Datum", VaadinIcon.CALENDAR, "date");
Div customerBlock = createDraggableTemplate("Kundeninfo", VaadinIcon.USER, "customer");
Div companyBlock = createDraggableTemplate("Firmeninfo", VaadinIcon.WORKPLACE, "company");
Div amountBlock = createDraggableTemplate("Betrag", VaadinIcon.COIN_PILES, "amount");
Div lineBlock = createDraggableTemplate("Linie", VaadinIcon.LINE_V, "line");
Div imageBlock = createDraggableTemplate("Bild", VaadinIcon.PICTURE, "image");
Div textBlock = createDraggableTemplate(getTranslation("profile.invoice.element.text"), VaadinIcon.TEXT_LABEL, "text");
Div headerBlock = createDraggableTemplate(getTranslation("profile.invoice.element.header"), VaadinIcon.HEADER, "header");
Div dateBlock = createDraggableTemplate(getTranslation("profile.invoice.element.date"), VaadinIcon.CALENDAR, "date");
Div customerBlock = createDraggableTemplate(getTranslation("profile.invoice.element.customer"), VaadinIcon.USER, "customer");
Div companyBlock = createDraggableTemplate(getTranslation("profile.invoice.element.company"), VaadinIcon.WORKPLACE, "company");
Div amountBlock = createDraggableTemplate(getTranslation("profile.invoice.element.amount"), VaadinIcon.COIN_PILES, "amount");
Div lineBlock = createDraggableTemplate(getTranslation("profile.invoice.element.line"), VaadinIcon.LINE_V, "line");
Div imageBlock = createDraggableTemplate(getTranslation("profile.invoice.element.image"), VaadinIcon.PICTURE, "image");
panel.add(invoiceHeader, senderCompany, senderName, senderAddress, senderCity, senderEmail, senderPhone,
servicesHeader, servicesListBlock, servicesNetBlock, servicesVatBlock, servicesGrossBlock,
@@ -1085,11 +1130,11 @@ public class EditProfileView extends HorizontalLayout {
panel.setSpacing(true);
panel.setHeightFull();
Span header = new Span("Eigenschaften");
Span header = new Span(getTranslation("profile.invoice.properties"));
header.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-l)");
Div infoText = new Div();
infoText.setText("Klicken Sie auf ein Element im Canvas, um dessen Eigenschaften zu bearbeiten.");
infoText.setText(getTranslation("profile.invoice.properties.info"));
infoText.getStyle().set("color", "var(--lumo-secondary-text-color)").set("font-size",
"var(--lumo-font-size-s)");
@@ -1104,15 +1149,15 @@ public class EditProfileView extends HorizontalLayout {
getUI().ifPresent(ui -> ui.access(() -> {
propertiesPanelProfile.removeAll();
Span header = new Span("Eigenschaften");
Span header = new Span(getTranslation("profile.invoice.properties"));
header.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-l)");
// Element Typ Anzeige
String typeDisplay = "Typ: " + elementType;
String typeDisplay = getTranslation("profile.invoice.type") + ": " + elementType;
if (variable != null && !variable.isEmpty()) {
typeDisplay += " (Variable)";
typeDisplay += " (" + getTranslation("profile.invoice.variable") + ")";
} else if (Boolean.TRUE.equals(isStatic)) {
typeDisplay += " (Stammdaten)";
typeDisplay += " (" + getTranslation("profile.basicdata") + ")";
}
Span typeLabel = new Span(typeDisplay);
typeLabel.getStyle().set("font-size", "var(--lumo-font-size-s)");
@@ -1121,7 +1166,7 @@ public class EditProfileView extends HorizontalLayout {
// Variable anzeigen wenn vorhanden
if (variable != null && !variable.isEmpty()) {
TextField variableField = new TextField("Variable");
TextField variableField = new TextField(getTranslation("profile.invoice.variable"));
variableField.setValue(variable);
variableField.setReadOnly(true);
variableField.setWidthFull();
@@ -1139,7 +1184,7 @@ public class EditProfileView extends HorizontalLayout {
Upload upload = new Upload(buffer);
upload.setAcceptedFileTypes("image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp");
upload.setMaxFileSize(5 * 1024 * 1024); // 5 MB
upload.setDropLabel(new Span("Bild hierher ziehen oder klicken"));
upload.setDropLabel(new Span(getTranslation("profile.invoice.image.drop")));
upload.setWidthFull();
upload.addSucceededListener(event -> {
@@ -1154,15 +1199,15 @@ public class EditProfileView extends HorizontalLayout {
getElement()
.executeJs("if (window.updateProfileElementImage) { window.updateProfileElementImage('"
+ elementId + "', $0); }", dataUrl);
Notification.show("Bild erfolgreich hochgeladen", 3000, Notification.Position.BOTTOM_CENTER);
Notification.show(getTranslation("profile.invoice.image.uploaded"), 3000, Notification.Position.BOTTOM_CENTER);
} catch (Exception ex) {
Notification.show("Fehler beim Hochladen: " + ex.getMessage(), 3000,
Notification.show(getTranslation("profile.invoice.image.upload.error", ex.getMessage()), 3000,
Notification.Position.BOTTOM_CENTER);
}
});
upload.addFileRejectedListener(event -> {
Notification.show("Datei abgelehnt: " + event.getErrorMessage(), 3000,
Notification.show(getTranslation("profile.invoice.file.rejected", event.getErrorMessage()), 3000,
Notification.Position.BOTTOM_CENTER);
});
@@ -1171,13 +1216,13 @@ public class EditProfileView extends HorizontalLayout {
// Text Feld (nur für Text-Elemente)
if (!"line".equals(elementType) && !"image".equals(elementType)) {
TextField textField = new TextField("Text");
TextField textField = new TextField(getTranslation("profile.invoice.element.text"));
textField.setValue(text != null ? text : "");
textField.setWidthFull();
// Statische Elemente können nicht editiert werden
if (Boolean.TRUE.equals(isStatic)) {
textField.setReadOnly(true);
textField.setHelperText("Text kommt aus Ihren Stammdaten");
textField.setHelperText(getTranslation("profile.invoice.text.from.masterdata"));
} else {
textField.addValueChangeListener(e -> {
getElement()
@@ -1189,7 +1234,7 @@ public class EditProfileView extends HorizontalLayout {
}
// X Position
TextField xField = new TextField("X Position");
TextField xField = new TextField(getTranslation("profile.invoice.xposition"));
xField.setValue(x != null ? String.valueOf(Math.round(x)) : "0");
xField.setWidthFull();
xField.addValueChangeListener(e -> {
@@ -1205,7 +1250,7 @@ public class EditProfileView extends HorizontalLayout {
propertiesPanelProfile.add(xField);
// Y Position
TextField yField = new TextField("Y Position");
TextField yField = new TextField(getTranslation("profile.invoice.yposition"));
yField.setValue(y != null ? String.valueOf(Math.round(y)) : "0");
yField.setWidthFull();
yField.addValueChangeListener(e -> {
@@ -1222,7 +1267,7 @@ public class EditProfileView extends HorizontalLayout {
// Font Size (nur für Text-Elemente)
if (!"line".equals(elementType) && !"image".equals(elementType)) {
TextField fontSizeField = new TextField("Schriftgröße");
TextField fontSizeField = new TextField(getTranslation("profile.invoice.fontsize"));
fontSizeField.setValue(fontSize != null ? String.valueOf(fontSize) : "16");
fontSizeField.setWidthFull();
fontSizeField.addValueChangeListener(e -> {
@@ -1242,7 +1287,7 @@ public class EditProfileView extends HorizontalLayout {
colorContainer.getStyle().set("display", "flex").set("align-items", "center")
.set("gap", "var(--lumo-space-s)").set("margin-top", "var(--lumo-space-s)");
Span colorLabel = new Span("Farbe");
Span colorLabel = new Span(getTranslation("profile.invoice.color"));
colorLabel.getStyle().set("font-size", "var(--lumo-font-size-s)");
Input colorPicker = new Input();
@@ -1279,7 +1324,7 @@ public class EditProfileView extends HorizontalLayout {
}
// Löschen Button
Button deleteButton = new Button("Element löschen", new Icon(VaadinIcon.TRASH));
Button deleteButton = new Button(getTranslation("profile.invoice.element.delete"), new Icon(VaadinIcon.TRASH));
deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
deleteButton.setWidthFull();
deleteButton.addClickListener(e -> {
@@ -1299,11 +1344,11 @@ public class EditProfileView extends HorizontalLayout {
propertiesPanelProfile.removeAll();
Span header = new Span("Eigenschaften");
Span header = new Span(getTranslation("profile.invoice.properties"));
header.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-l)");
Div infoText = new Div();
infoText.setText("Klicken Sie auf ein Element im Canvas, um dessen Eigenschaften zu bearbeiten.");
infoText.setText(getTranslation("profile.invoice.properties.info"));
infoText.getStyle().set("color", "var(--lumo-secondary-text-color)").set("font-size",
"var(--lumo-font-size-s)");
@@ -1401,12 +1446,12 @@ public class EditProfileView extends HorizontalLayout {
servicesTab.setSpacing(true);
// Header
Span header = new Span("Leistungskatalog");
Span header = new Span(getTranslation("profile.services"));
header.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-l)").set("margin-bottom",
"var(--lumo-space-m)");
// Description
Span description = new Span("Verwalten Sie hier Ihre Leistungen, die Sie Ihren Kunden anbieten.");
Span description = new Span(getTranslation("profile.services.description"));
description.getStyle().set("color", "var(--lumo-secondary-text-color)").set("font-size",
"var(--lumo-font-size-s)");
@@ -1418,33 +1463,33 @@ public class EditProfileView extends HorizontalLayout {
servicesGrid.setHeight("300px");
// Configure grid columns
servicesGrid.addColumn(Service::getName).setHeader("Name").setSortable(true);
servicesGrid.addColumn(Service::getName).setHeader(getTranslation("common.name")).setSortable(true);
servicesGrid.addColumn(service -> {
if (service.getCalculationBasis() != null) {
return switch (service.getCalculationBasis()) {
case DISTANCE -> "Gefahrene Kilometer";
case TIME -> "Zeit";
case FLAT_RATE -> "Pauschal";
case DISTANCE -> getTranslation("profile.services.basis.distance");
case TIME -> getTranslation("profile.services.basis.time");
case FLAT_RATE -> getTranslation("profile.services.basis.flatrate");
};
}
return "";
}).setHeader("Berechnungsgrundlage").setSortable(true);
}).setHeader(getTranslation("profile.services.basis")).setSortable(true);
servicesGrid.addColumn(service -> {
if (service.getCalculationBasis() == Service.CalculationBasis.FLAT_RATE && service.getPrice() != null) {
return service.getPrice().setScale(2, RoundingMode.HALF_UP) + "";
}
return "Wird berechnet";
}).setHeader("Preis").setSortable(true);
return getTranslation("profile.services.calculated");
}).setHeader(getTranslation("common.price")).setSortable(true);
servicesGrid.addColumn(service -> {
if (service.getVatRate() != null) {
return service.getVatRate().multiply(new BigDecimal("100")) + " %";
}
return "";
}).setHeader("Mehrwertsteuersatz").setSortable(true);
}).setHeader(getTranslation("profile.services.vatrate")).setSortable(true);
servicesGrid.addColumn(service -> service.isMandatory() ? "Ja" : "Nein").setHeader("Verpflichtend")
servicesGrid.addColumn(service -> service.isMandatory() ? getTranslation("common.yes") : getTranslation("common.no")).setHeader(getTranslation("profile.services.mandatory"))
.setSortable(true);
// Actions column with edit and delete buttons
@@ -1465,12 +1510,12 @@ public class EditProfileView extends HorizontalLayout {
actionsLayout.add(editButton, deleteButton);
return actionsLayout;
}).setHeader("Aktionen").setFlexGrow(0).setWidth("120px");
}).setHeader(getTranslation("common.actions")).setFlexGrow(0).setWidth("120px");
servicesTab.add(servicesGrid);
// Add service button
Button addServiceButton = new Button("Neue Leistung hinzufügen", new Icon(VaadinIcon.PLUS));
Button addServiceButton = new Button(getTranslation("profile.services.add"), new Icon(VaadinIcon.PLUS));
addServiceButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addServiceButton.addClickListener(e -> openServiceDialog(null));
@@ -1490,7 +1535,7 @@ public class EditProfileView extends HorizontalLayout {
List<Service> userServices = serviceRepository.findByUserId(currentUser.getId().toString());
servicesGrid.setItems(userServices);
} catch (Exception e) {
Notification.show("Fehler beim Laden der Leistungen: " + e.getMessage(), 3000,
Notification.show(getTranslation("profile.services.load.error", e.getMessage()), 3000,
Notification.Position.BOTTOM_CENTER);
}
}
@@ -1500,7 +1545,7 @@ public class EditProfileView extends HorizontalLayout {
*/
private void openServiceDialog(Service service) {
Dialog dialog = new Dialog();
dialog.setHeaderTitle(service == null ? "Neue Leistung erstellen" : "Leistung bearbeiten");
dialog.setHeaderTitle(service == null ? getTranslation("profile.services.dialog.create") : getTranslation("profile.services.dialog.edit"));
dialog.setWidth("500px");
// Form layout
@@ -1510,27 +1555,27 @@ public class EditProfileView extends HorizontalLayout {
formLayout.setWidthFull();
// Name field
TextField nameField = new TextField("Name");
TextField nameField = new TextField(getTranslation("common.name"));
nameField.setWidthFull();
nameField.setRequired(true);
nameField.setRequiredIndicatorVisible(true);
// Calculation basis combo box
ComboBox<Service.CalculationBasis> calculationBasisCombo = new ComboBox<>("Berechnungsgrundlage");
ComboBox<Service.CalculationBasis> calculationBasisCombo = new ComboBox<>(getTranslation("profile.services.basis"));
calculationBasisCombo.setWidthFull();
calculationBasisCombo.setItems(Service.CalculationBasis.values());
calculationBasisCombo.setItemLabelGenerator(basis -> {
return switch (basis) {
case DISTANCE -> "Gefahrene Kilometer";
case TIME -> "Zeit";
case FLAT_RATE -> "Pauschal";
case DISTANCE -> getTranslation("profile.services.basis.distance");
case TIME -> getTranslation("profile.services.basis.time");
case FLAT_RATE -> getTranslation("profile.services.basis.flatrate");
};
});
calculationBasisCombo.setRequired(true);
calculationBasisCombo.setRequiredIndicatorVisible(true);
// VAT rate field
NumberField vatRateField = new NumberField("Mehrwertsteuersatz (%)");
NumberField vatRateField = new NumberField(getTranslation("profile.services.vatrate.percent"));
vatRateField.setWidthFull();
vatRateField.setMin(0);
vatRateField.setMax(100);
@@ -1540,7 +1585,7 @@ public class EditProfileView extends HorizontalLayout {
vatRateField.setRequiredIndicatorVisible(true);
// Mandatory checkbox
Checkbox mandatoryCheckbox = new Checkbox("Verpflichtend");
Checkbox mandatoryCheckbox = new Checkbox(getTranslation("profile.services.mandatory"));
mandatoryCheckbox.setValue(false);
// Set values if editing existing service
@@ -1554,7 +1599,7 @@ public class EditProfileView extends HorizontalLayout {
}
// Price fields for different calculation bases
NumberField flatRatePriceField = new NumberField("Pauschalpreis (€)");
NumberField flatRatePriceField = new NumberField(getTranslation("profile.services.price.flatrate"));
flatRatePriceField.setWidthFull();
flatRatePriceField.setMin(0);
flatRatePriceField.setStep(0.01);
@@ -1562,7 +1607,7 @@ public class EditProfileView extends HorizontalLayout {
flatRatePriceField.setRequired(true);
flatRatePriceField.setRequiredIndicatorVisible(true);
NumberField distancePriceField = new NumberField("Preis pro Kilometer (€)");
NumberField distancePriceField = new NumberField(getTranslation("profile.services.price.distance"));
distancePriceField.setWidthFull();
distancePriceField.setMin(0);
distancePriceField.setStep(0.01);
@@ -1570,7 +1615,7 @@ public class EditProfileView extends HorizontalLayout {
distancePriceField.setRequired(true);
distancePriceField.setRequiredIndicatorVisible(true);
NumberField timePriceField = new NumberField("Preis pro 15 Minuten (€)");
NumberField timePriceField = new NumberField(getTranslation("profile.services.price.time"));
timePriceField.setWidthFull();
timePriceField.setMin(0);
timePriceField.setStep(0.01);
@@ -1632,10 +1677,10 @@ public class EditProfileView extends HorizontalLayout {
buttonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
buttonLayout.setSpacing(true);
Button cancelButton = new Button("Abbrechen", e -> dialog.close());
Button cancelButton = new Button(getTranslation("button.cancel"), e -> dialog.close());
cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
Button saveButton = new Button("Speichern", e -> {
Button saveButton = new Button(getTranslation("button.savechanges"), e -> {
if (validateServiceForm(nameField, calculationBasisCombo, flatRatePriceField, distancePriceField,
timePriceField, vatRateField, mandatoryCheckbox)) {
// Get the appropriate price based on calculation basis
@@ -1677,7 +1722,7 @@ public class EditProfileView extends HorizontalLayout {
if (nameField.isEmpty()) {
nameField.setInvalid(true);
nameField.setErrorMessage("Name ist erforderlich");
nameField.setErrorMessage(getTranslation("profile.services.validation.name"));
isValid = false;
} else {
nameField.setInvalid(false);
@@ -1685,7 +1730,7 @@ public class EditProfileView extends HorizontalLayout {
if (calculationBasisCombo.isEmpty()) {
calculationBasisCombo.setInvalid(true);
calculationBasisCombo.setErrorMessage("Berechnungsgrundlage ist erforderlich");
calculationBasisCombo.setErrorMessage(getTranslation("profile.services.validation.basis"));
isValid = false;
} else {
calculationBasisCombo.setInvalid(false);
@@ -1696,7 +1741,7 @@ public class EditProfileView extends HorizontalLayout {
if (selectedBasis == Service.CalculationBasis.FLAT_RATE && flatRatePriceField.isVisible()) {
if (flatRatePriceField.isEmpty() || flatRatePriceField.getValue() == null) {
flatRatePriceField.setInvalid(true);
flatRatePriceField.setErrorMessage("Pauschalpreis ist erforderlich");
flatRatePriceField.setErrorMessage(getTranslation("profile.services.validation.flatrate"));
isValid = false;
} else {
flatRatePriceField.setInvalid(false);
@@ -1704,7 +1749,7 @@ public class EditProfileView extends HorizontalLayout {
} else if (selectedBasis == Service.CalculationBasis.DISTANCE && distancePriceField.isVisible()) {
if (distancePriceField.isEmpty() || distancePriceField.getValue() == null) {
distancePriceField.setInvalid(true);
distancePriceField.setErrorMessage("Preis pro Kilometer ist erforderlich");
distancePriceField.setErrorMessage(getTranslation("profile.services.validation.distance"));
isValid = false;
} else {
distancePriceField.setInvalid(false);
@@ -1712,7 +1757,7 @@ public class EditProfileView extends HorizontalLayout {
} else if (selectedBasis == Service.CalculationBasis.TIME && timePriceField.isVisible()) {
if (timePriceField.isEmpty() || timePriceField.getValue() == null) {
timePriceField.setInvalid(true);
timePriceField.setErrorMessage("Preis pro 15 Minuten ist erforderlich");
timePriceField.setErrorMessage(getTranslation("profile.services.validation.time"));
isValid = false;
} else {
timePriceField.setInvalid(false);
@@ -1721,7 +1766,7 @@ public class EditProfileView extends HorizontalLayout {
if (vatRateField.isEmpty() || vatRateField.getValue() == null) {
vatRateField.setInvalid(true);
vatRateField.setErrorMessage("Mehrwertsteuersatz ist erforderlich");
vatRateField.setErrorMessage(getTranslation("profile.services.validation.vatrate"));
isValid = false;
} else {
vatRateField.setInvalid(false);
@@ -1768,7 +1813,7 @@ public class EditProfileView extends HorizontalLayout {
}
serviceRepository.save(service);
Notification.show("Leistung erfolgreich gespeichert", 3000, Notification.Position.BOTTOM_CENTER);
Notification.show(getTranslation("profile.services.saved"), 3000, Notification.Position.BOTTOM_CENTER);
// Refresh the grid by reloading services
if (servicesGrid != null) {
@@ -1776,7 +1821,7 @@ public class EditProfileView extends HorizontalLayout {
}
} catch (Exception e) {
Notification.show("Fehler beim Speichern der Leistung: " + e.getMessage(), 5000,
Notification.show(getTranslation("profile.services.save.error", e.getMessage()), 5000,
Notification.Position.BOTTOM_CENTER);
}
}
@@ -1787,7 +1832,7 @@ public class EditProfileView extends HorizontalLayout {
private void deleteService(Service service) {
try {
serviceRepository.delete(service);
Notification.show("Leistung erfolgreich gelöscht", 3000, Notification.Position.BOTTOM_CENTER);
Notification.show(getTranslation("profile.services.deleted"), 3000, Notification.Position.BOTTOM_CENTER);
// Refresh the grid by reloading services
if (servicesGrid != null) {
@@ -1795,8 +1840,13 @@ public class EditProfileView extends HorizontalLayout {
}
} catch (Exception e) {
Notification.show("Fehler beim Löschen der Leistung: " + e.getMessage(), 5000,
Notification.show(getTranslation("profile.services.delete.error", e.getMessage()), 5000,
Notification.Position.BOTTOM_CENTER);
}
}
@Override
public String getPageTitle() {
return getTranslation("page.title.profile.edit");
}
}

View File

@@ -9,7 +9,7 @@ import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.PasswordField;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import de.assecutor.votianlt.pages.service.PasswordResetService;
@@ -17,9 +17,8 @@ import de.assecutor.votianlt.pages.service.PasswordResetService;
import java.util.Map;
@Route("forget-password")
@PageTitle("Passwort zurücksetzen")
@AnonymousAllowed
public class ForgetPasswordView extends VerticalLayout implements BeforeEnterObserver {
public class ForgetPasswordView extends VerticalLayout implements BeforeEnterObserver, HasDynamicTitle {
private final PasswordResetService passwordResetService;
@@ -91,4 +90,9 @@ public class ForgetPasswordView extends VerticalLayout implements BeforeEnterObs
Notification.show("Token ungültig oder abgelaufen.", 3000, Notification.Position.MIDDLE);
}
}
@Override
public String getPageTitle() {
return getTranslation("page.title.password.forget");
}
}

View File

@@ -8,16 +8,15 @@ import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.EmailField;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import de.assecutor.votianlt.pages.service.PasswordResetService;
@Route("forgot-password-request")
@PageTitle("Passwort zurücksetzen E-Mail angeben")
@AnonymousAllowed
public class ForgotPasswordRequestView extends VerticalLayout {
public class ForgotPasswordRequestView extends VerticalLayout implements HasDynamicTitle {
public ForgotPasswordRequestView(PasswordResetService passwordResetService) {
@@ -76,4 +75,9 @@ public class ForgotPasswordRequestView extends VerticalLayout {
}
return "";
}
@Override
public String getPageTitle() {
return getTranslation("page.title.password.reset");
}
}

View File

@@ -3,14 +3,13 @@ package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import org.springframework.core.io.ClassPathResource;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import jakarta.annotation.security.PermitAll;
@PageTitle("Impressum")
@Route(value = "impressum", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@PermitAll
public class ImprintView extends VerticalLayout {
public class ImprintView extends VerticalLayout implements HasDynamicTitle {
public ImprintView() {
setSizeFull();
setPadding(true);
@@ -31,8 +30,13 @@ public class ImprintView extends VerticalLayout {
} catch (Exception e) {
// Fallback content in case of error
Div errorDiv = new Div();
errorDiv.setText("Fehler beim Laden des Impressums: " + e.getMessage());
errorDiv.setText(getTranslation("imprint.error", e.getMessage()));
add(errorDiv);
}
}
@Override
public String getPageTitle() {
return getTranslation("page.title.imprint");
}
}

View File

@@ -21,17 +21,16 @@ import elemental.json.JsonValue;
import elemental.json.JsonType;
import com.vaadin.flow.component.upload.Upload;
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
import de.assecutor.votianlt.service.CustomerInvoiceService;
import jakarta.annotation.security.RolesAllowed;
@Route(value = "invoice-generator", layout = AdminLayout.class)
@PageTitle("Rechnungsgenerator")
@RolesAllowed("ADMIN")
@JsModule("./invoice-generator/invoice-generator.js")
public class InvoiceGeneratorView extends VerticalLayout {
public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTitle {
private final CustomerInvoiceService customerInvoiceService;
private Div canvasContainer;
@@ -106,18 +105,18 @@ public class InvoiceGeneratorView extends VerticalLayout {
panel.getStyle().set("background-color", "var(--lumo-contrast-5pct)")
.set("border-radius", "var(--lumo-border-radius-m)").set("overflow", "auto");
Span header = new Span("Textbausteine");
Span header = new Span(getTranslation("invoicegenerator.panel.templates"));
header.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-l)");
// Draggable Templates
Div textBlock = createDraggableTemplate("Textfeld", VaadinIcon.TEXT_LABEL, "text");
Div headerBlock = createDraggableTemplate("Überschrift", VaadinIcon.HEADER, "header");
Div dateBlock = createDraggableTemplate("Datum", VaadinIcon.CALENDAR, "date");
Div customerBlock = createDraggableTemplate("Kundeninfo", VaadinIcon.USER, "customer");
Div companyBlock = createDraggableTemplate("Firmeninfo", VaadinIcon.OFFICE, "company");
Div amountBlock = createDraggableTemplate("Betrag", VaadinIcon.COIN_PILES, "amount");
Div lineBlock = createDraggableTemplate("Linie", VaadinIcon.LINE_V, "line");
Div imageBlock = createDraggableTemplate("Bild", VaadinIcon.PICTURE, "image");
Div textBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.text"), VaadinIcon.TEXT_LABEL, "text");
Div headerBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.header"), VaadinIcon.HEADER, "header");
Div dateBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.date"), VaadinIcon.CALENDAR, "date");
Div customerBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.customerinfo"), VaadinIcon.USER, "customer");
Div companyBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.companyinfo"), VaadinIcon.OFFICE, "company");
Div amountBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.amount"), VaadinIcon.COIN_PILES, "amount");
Div lineBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.line"), VaadinIcon.LINE_V, "line");
Div imageBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.image"), VaadinIcon.PICTURE, "image");
panel.add(header, textBlock, headerBlock, dateBlock, customerBlock, companyBlock, amountBlock, lineBlock,
imageBlock);
@@ -200,12 +199,12 @@ public class InvoiceGeneratorView extends VerticalLayout {
panel.getStyle().set("background-color", "var(--lumo-contrast-5pct)")
.set("border-radius", "var(--lumo-border-radius-m)").set("overflow", "auto");
Span header = new Span("Eigenschaften");
Span header = new Span(getTranslation("invoicegenerator.panel.properties"));
header.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-l)");
// Info-Text wenn kein Element ausgewählt
selectedElementInfo = new Div();
selectedElementInfo.setText("Klicken Sie auf ein Element im Canvas, um dessen Eigenschaften zu bearbeiten.");
selectedElementInfo.setText(getTranslation("invoicegenerator.properties.info"));
selectedElementInfo.getStyle().set("color", "var(--lumo-secondary-text-color)").set("font-size",
"var(--lumo-font-size-s)");
@@ -222,24 +221,24 @@ public class InvoiceGeneratorView extends VerticalLayout {
layout.setSpacing(true);
layout.setAlignItems(Alignment.CENTER);
Button clearButton = new Button("Canvas leeren", new Icon(VaadinIcon.TRASH));
Button clearButton = new Button(getTranslation("invoicegenerator.button.clear"), new Icon(VaadinIcon.TRASH));
clearButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
clearButton.addClickListener(e -> {
getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.clearCanvas(); }");
showNotification("Canvas wurde geleert");
showNotification(getTranslation("invoicegenerator.notification.cleared"));
});
Button previewButton = new Button("Vorschau", new Icon(VaadinIcon.EYE));
Button previewButton = new Button(getTranslation("button.preview"), new Icon(VaadinIcon.EYE));
previewButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST);
previewButton.addClickListener(e -> generatePreviewPdf());
Button saveTemplateButton = new Button("Template speichern", new Icon(VaadinIcon.DOWNLOAD));
Button saveTemplateButton = new Button(getTranslation("button.savetemplate"), new Icon(VaadinIcon.DOWNLOAD));
saveTemplateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
saveTemplateButton.addClickListener(e -> {
getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.exportTemplate(); }");
});
Button generatePdfButton = new Button("PDF generieren", new Icon(VaadinIcon.FILE_TEXT_O));
Button generatePdfButton = new Button(getTranslation("invoicegenerator.button.generatepdf"), new Icon(VaadinIcon.FILE_TEXT_O));
generatePdfButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
generatePdfButton.addClickListener(e -> generatePdf());
@@ -255,14 +254,14 @@ public class InvoiceGeneratorView extends VerticalLayout {
"if (window.invoiceGenerator) { return window.invoiceGenerator.getCanvasData(); } else { return null; }")
.then(json -> {
if (json == null) {
showNotification("Fehler: Canvas-Daten konnten nicht gelesen werden");
showNotification(getTranslation("invoicegenerator.notification.canvas.error"));
return;
}
// Hier würde die PDF-Generierung erfolgen
showNotification("PDF wird generiert... (Demo)");
showNotification(getTranslation("invoicegenerator.notification.generating"));
});
} catch (Exception ex) {
showNotification("Fehler beim Generieren des PDFs: " + ex.getMessage());
showNotification(getTranslation("invoicegenerator.notification.pdf.error", ex.getMessage()));
}
}
@@ -272,7 +271,7 @@ public class InvoiceGeneratorView extends VerticalLayout {
"if (window.invoiceGenerator) { return window.invoiceGenerator.exportTemplateJson(); } else { return null; }")
.then(result -> {
if (result == null) {
showNotification("Fehler: Canvas-Daten konnten nicht gelesen werden");
showNotification(getTranslation("invoicegenerator.notification.canvas.error"));
return;
}
try {
@@ -290,11 +289,11 @@ public class InvoiceGeneratorView extends VerticalLayout {
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData);
showPdfInDialog(pdfBytes);
} catch (Exception ex) {
showNotification("Fehler beim Generieren der Vorschau: " + ex.getMessage());
showNotification(getTranslation("invoicegenerator.notification.preview.error", ex.getMessage()));
}
});
} catch (Exception ex) {
showNotification("Fehler: " + ex.getMessage());
showNotification(getTranslation("invoicegenerator.notification.error", ex.getMessage()));
}
}
@@ -306,7 +305,7 @@ public class InvoiceGeneratorView extends VerticalLayout {
// Create dialog
Dialog pdfDialog = new Dialog();
pdfDialog.setHeaderTitle("PDF Vorschau");
pdfDialog.setHeaderTitle(getTranslation("invoicegenerator.pdf.preview.title"));
pdfDialog.setWidth("90vw");
pdfDialog.setHeight("90vh");
@@ -329,11 +328,11 @@ public class InvoiceGeneratorView extends VerticalLayout {
pdfContainer.add(pdfFrame);
// Close button
Button closeButton = new Button("Schließen", e -> pdfDialog.close());
Button closeButton = new Button(getTranslation("button.close"), e -> pdfDialog.close());
closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
// Download button
Button downloadButton = new Button("Herunterladen", e -> {
Button downloadButton = new Button(getTranslation("button.download"), e -> {
getElement()
.executeJs("const link = document.createElement('a');" + "link.href = 'data:application/pdf;base64,"
+ base64Pdf + "';" + "link.download = 'vorschau.pdf';" + "link.click();");
@@ -356,11 +355,11 @@ public class InvoiceGeneratorView extends VerticalLayout {
getUI().ifPresent(ui -> ui.access(() -> {
propertiesPanel.removeAll();
Span header = new Span("Eigenschaften");
Span header = new Span(getTranslation("invoicegenerator.properties.title"));
header.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-l)");
// Element Typ Anzeige
Span typeLabel = new Span("Typ: " + elementType);
Span typeLabel = new Span(getTranslation("invoicegenerator.properties.type") + ": " + elementType);
typeLabel.getStyle().set("font-size", "var(--lumo-font-size-s)");
propertiesPanel.add(header, typeLabel);
@@ -371,7 +370,7 @@ public class InvoiceGeneratorView extends VerticalLayout {
Upload upload = new Upload(buffer);
upload.setAcceptedFileTypes("image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp");
upload.setMaxFileSize(5 * 1024 * 1024); // 5 MB
upload.setDropLabel(new Span("Bild hierher ziehen oder klicken"));
upload.setDropLabel(new Span(getTranslation("invoicegenerator.upload.drop")));
upload.setWidthFull();
upload.addSucceededListener(event -> {
@@ -386,14 +385,14 @@ public class InvoiceGeneratorView extends VerticalLayout {
getElement()
.executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.updateElementImage('"
+ elementId + "', $0); }", dataUrl);
showNotification("Bild erfolgreich hochgeladen");
showNotification(getTranslation("invoicegenerator.upload.success"));
} catch (Exception ex) {
showNotification("Fehler beim Hochladen: " + ex.getMessage());
showNotification(getTranslation("invoicegenerator.upload.error", ex.getMessage()));
}
});
upload.addFileRejectedListener(event -> {
showNotification("Datei abgelehnt: " + event.getErrorMessage());
showNotification(getTranslation("invoicegenerator.file.rejected", event.getErrorMessage()));
});
propertiesPanel.add(upload);
@@ -443,7 +442,7 @@ public class InvoiceGeneratorView extends VerticalLayout {
// Font Size (nur für Text-Elemente)
if (!"line".equals(elementType) && !"image".equals(elementType)) {
TextField fontSizeField = new TextField("Schriftgröße");
TextField fontSizeField = new TextField(getTranslation("invoicegenerator.fontsize.label"));
fontSizeField.setValue(fontSize != null ? String.valueOf(fontSize) : "16");
fontSizeField.setWidthFull();
fontSizeField.addValueChangeListener(e -> {
@@ -459,7 +458,7 @@ public class InvoiceGeneratorView extends VerticalLayout {
propertiesPanel.add(fontSizeField);
// Schriftfarbe mit Dialog
Span colorLabel = new Span("Schriftfarbe");
Span colorLabel = new Span(getTranslation("invoicegenerator.color.label"));
colorLabel.getStyle().set("font-size", "var(--lumo-font-size-s)");
propertiesPanel.add(colorLabel);
@@ -485,7 +484,7 @@ public class InvoiceGeneratorView extends VerticalLayout {
// Color Picker Dialog
Dialog colorDialog = new Dialog();
colorDialog.setHeaderTitle("Schriftfarbe wählen");
colorDialog.setHeaderTitle(getTranslation("invoicegenerator.color.dialog.title"));
VerticalLayout dialogLayout = new VerticalLayout();
dialogLayout.setSpacing(true);
@@ -498,7 +497,7 @@ public class InvoiceGeneratorView extends VerticalLayout {
dialogColorPicker.getStyle().set("width", "100%").set("height", "50px").set("padding", "0");
// Hex-Eingabe im Dialog
TextField dialogHexField = new TextField("Hex-Farbwert");
TextField dialogHexField = new TextField(getTranslation("invoicegenerator.color.dialog.hex"));
dialogHexField.setValue(currentColor);
dialogHexField.setWidthFull();
@@ -518,7 +517,7 @@ public class InvoiceGeneratorView extends VerticalLayout {
colorDialog.add(dialogLayout);
// Dialog Buttons
Button dialogCancelButton = new Button("Abbrechen", e -> {
Button dialogCancelButton = new Button(getTranslation("invoicegenerator.button.cancel"), e -> {
colorDialog.close();
// Reset to original values
dialogColorPicker.setValue(currentColor);
@@ -526,7 +525,7 @@ public class InvoiceGeneratorView extends VerticalLayout {
});
dialogCancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
Button dialogApplyButton = new Button("Übernehmen", e -> {
Button dialogApplyButton = new Button(getTranslation("invoicegenerator.button.apply"), e -> {
String newColor = dialogColorPicker.getValue();
// Update preview
colorPreview.getStyle().set("background-color", newColor);
@@ -535,7 +534,7 @@ public class InvoiceGeneratorView extends VerticalLayout {
getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.updateElementColor('"
+ elementId + "', $0); }", newColor);
colorDialog.close();
showNotification("Farbe übernommen");
showNotification(getTranslation("invoicegenerator.notification.color.applied"));
});
dialogApplyButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
@@ -558,7 +557,7 @@ public class InvoiceGeneratorView extends VerticalLayout {
}
// Löschen Button
Button deleteButton = new Button("Element löschen", new Icon(VaadinIcon.TRASH));
Button deleteButton = new Button(getTranslation("invoicegenerator.button.delete"), new Icon(VaadinIcon.TRASH));
deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
deleteButton.setWidthFull();
deleteButton.addClickListener(e -> {
@@ -590,4 +589,9 @@ public class InvoiceGeneratorView extends VerticalLayout {
propertiesPanel.add(header, selectedElementInfo);
}));
}
@Override
public String getPageTitle() {
return getTranslation("page.title.invoice.generator");
}
}

View File

@@ -5,7 +5,7 @@ import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.component.UI;
import de.assecutor.votianlt.model.invoices.SystemInvoice;
@@ -25,10 +25,9 @@ import java.util.Locale;
import com.vaadin.flow.server.StreamResource;
import com.vaadin.flow.server.StreamRegistration;
@PageTitle("Rechnungen")
@Route(value = "invoices", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({ "USER", "ADMIN" })
public class InvoicesView extends VerticalLayout {
public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
private final Grid<SystemInvoice> invoiceGrid;
@@ -43,15 +42,15 @@ public class InvoicesView extends VerticalLayout {
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
H2 title = new H2("Rechnungen");
H2 title = new H2(getTranslation("invoices.title"));
add(title);
invoiceGrid = new Grid<>(SystemInvoice.class, false);
invoiceGrid.addColumn(SystemInvoice::getId).setHeader("Rechnungsnummer").setAutoWidth(true);
invoiceGrid.addColumn(SystemInvoice::getKunde).setHeader("Kunde").setAutoWidth(true);
invoiceGrid.addColumn(SystemInvoice::getDatum).setHeader("Datum").setAutoWidth(true);
invoiceGrid.addColumn(SystemInvoice::getBetrag).setHeader("Betrag").setAutoWidth(true);
invoiceGrid.addColumn(SystemInvoice::getBeschreibung).setHeader("Beschreibung").setAutoWidth(true);
invoiceGrid.addColumn(SystemInvoice::getId).setHeader(getTranslation("invoices.column.number")).setAutoWidth(true);
invoiceGrid.addColumn(SystemInvoice::getKunde).setHeader(getTranslation("invoices.column.customer")).setAutoWidth(true);
invoiceGrid.addColumn(SystemInvoice::getDatum).setHeader(getTranslation("invoices.column.date")).setAutoWidth(true);
invoiceGrid.addColumn(SystemInvoice::getBetrag).setHeader(getTranslation("invoices.column.amount")).setAutoWidth(true);
invoiceGrid.addColumn(SystemInvoice::getBeschreibung).setHeader(getTranslation("invoices.column.description")).setAutoWidth(true);
invoiceGrid.setSelectionMode(Grid.SelectionMode.SINGLE);
invoiceGrid.getStyle().set("cursor", "pointer");
@@ -89,7 +88,7 @@ public class InvoicesView extends VerticalLayout {
UI.getCurrent().getPage().open(registration.getResourceUri().toString());
} catch (Exception e) {
Notification.show("Fehler beim Erstellen der PDF: " + e.getMessage(), 5000, Notification.Position.MIDDLE);
Notification.show(getTranslation("invoices.notification.pdf.error", e.getMessage()), 5000, Notification.Position.MIDDLE);
}
}
@@ -123,4 +122,9 @@ public class InvoicesView extends VerticalLayout {
return systemInvoiceService.generateInvoicePdfFromHtml(data);
}
@Override
public String getPageTitle() {
return getTranslation("page.title.invoices");
}
}

View File

@@ -11,7 +11,7 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.BeforeEvent;
import com.vaadin.flow.router.HasUrlParameter;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.votianlt.model.Job;
@@ -32,10 +32,9 @@ import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
@Route(value = "job_history", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@PageTitle("Job Historie")
@RolesAllowed("USER")
@Slf4j
public class JobHistoryView extends Main implements HasUrlParameter<String> {
public class JobHistoryView extends Main implements HasUrlParameter<String>, HasDynamicTitle {
private final JobRepository jobRepository;
private final JobHistoryService jobHistoryService;
@@ -57,7 +56,7 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
add(new ViewToolbar("Job Historie"));
add(new ViewToolbar(getTranslation("jobhistory.title")));
content = new VerticalLayout();
content.setSpacing(true);
@@ -71,7 +70,7 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
content.removeAll();
if (parameter == null || parameter.isBlank()) {
content.add(new Span("Fehler: Keine Job-ID angegeben"));
content.add(new Span(getTranslation("jobhistory.error.no.id")));
return;
}
@@ -79,13 +78,13 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
try {
jobId = new ObjectId(parameter);
} catch (Exception e) {
content.add(new Span("Fehler: Ungültige Job-ID Format: " + parameter));
content.add(new Span(getTranslation("jobhistory.error.invalid.id", parameter)));
return;
}
Job job = jobRepository.findById(jobId).orElse(null);
if (job == null) {
content.add(new Span("Fehler: Job mit ID " + parameter + " nicht gefunden"));
content.add(new Span(getTranslation("jobhistory.error.not.found", parameter)));
return;
}
@@ -96,8 +95,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
content.removeAll();
// Header mit Job-Informationen
H2 header = new H2(
"Job Historie - " + (job.getJobNumber() != null ? job.getJobNumber() : "Unbekannte Auftragsnummer"));
H2 header = new H2(getTranslation("jobhistory.header",
job.getJobNumber() != null ? job.getJobNumber() : getTranslation("jobhistory.unknown.jobnumber")));
content.add(header);
// Job basic info for context
@@ -110,12 +109,12 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
long historyCount = jobHistoryService.getJobHistoryCount(job.getId());
if (historyEntries.isEmpty()) {
Span noHistory = new Span("Noch keine History-Einträge für diesen Job vorhanden.");
Span noHistory = new Span(getTranslation("jobhistory.no.entries"));
noHistory.getStyle().set("color", "var(--lumo-secondary-text-color)");
content.add(noHistory);
} else {
// History section header
H2 historyHeader = new H2("Verlauf (" + historyCount + " Einträge)");
H2 historyHeader = new H2(getTranslation("jobhistory.count", historyCount));
historyHeader.getStyle().set("margin-top", "var(--lumo-space-l)");
content.add(historyHeader);
@@ -124,7 +123,7 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
content.add(timeline);
}
} catch (Exception e) {
Span errorMessage = new Span("Fehler beim Laden der Job Historie: " + e.getMessage());
Span errorMessage = new Span(getTranslation("jobhistory.error.load", e.getMessage()));
errorMessage.getStyle().set("color", "var(--lumo-error-text-color)");
content.add(errorMessage);
}
@@ -141,13 +140,13 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
infoContent.setSpacing(false);
if (job.getDeliveryCompany() != null) {
infoContent.add(new Span("Kunde: " + job.getDeliveryCompany()));
infoContent.add(new Span(getTranslation("jobhistory.info.customer", job.getDeliveryCompany())));
}
if (job.getCreatedAt() != null) {
infoContent.add(new Span("Erstellt am: " + formatDateTime(job.getCreatedAt())));
infoContent.add(new Span(getTranslation("jobhistory.info.createdat", formatDateTime(job.getCreatedAt()))));
}
if (job.getStatus() != null) {
infoContent.add(new Span("Status: " + formatStatus(job.getStatus())));
infoContent.add(new Span(getTranslation("jobhistory.info.status", formatStatus(job.getStatus()))));
}
infoBox.add(infoContent);
@@ -184,7 +183,7 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
Icon typeIcon = getTypeIcon(entry.getChangeType());
typeIcon.getStyle().set("color", getTypeColor(entry.getChangeType()));
Span reason = new Span(entry.getReason() != null ? entry.getReason() : "Unbekannt");
Span reason = new Span(entry.getReason() != null ? entry.getReason() : getTranslation("jobhistory.entry.unknown"));
reason.getStyle().set("font-weight", "500");
Span timestamp = new Span(formatDateTime(entry.getTimestamp()));
@@ -243,7 +242,7 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
// Changed by (if available)
if (entry.getChangedBy() != null && !entry.getChangedBy().isBlank()) {
Span changedBy = new Span("von: " + entry.getChangedBy());
Span changedBy = new Span(getTranslation("jobhistory.changedby", entry.getChangedBy()));
changedBy.getStyle().set("color", "var(--lumo-secondary-text-color)")
.set("font-size", "var(--lumo-font-size-xs)").set("margin-top", "var(--lumo-space-xs)")
.set("display", "block");
@@ -303,17 +302,17 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
private String formatStatus(de.assecutor.votianlt.model.JobStatus status) {
if (status == null)
return "Unbekannt";
return getTranslation("jobhistory.entry.unknown");
return switch (status) {
case CREATED -> "Erstellt";
case IN_PROGRESS -> "In Bearbeitung";
case CREATED -> getTranslation("jobstatus.CREATED");
case IN_PROGRESS -> getTranslation("jobstatus.IN_PROGRESS");
case PICKUP_SCHEDULED -> "Abholung geplant";
case PICKED_UP -> "Abgeholt";
case IN_TRANSIT -> "Unterwegs";
case DELIVERED -> "Zugestellt";
case COMPLETED -> "Abgeschlossen";
case CANCELLED -> "Storniert";
case COMPLETED -> getTranslation("jobstatus.COMPLETED");
case CANCELLED -> getTranslation("jobstatus.CANCELLED");
};
}
@@ -549,6 +548,11 @@ public class JobHistoryView extends Main implements HasUrlParameter<String> {
"<div style=\"width: " + width + "; height: " + height + ";\">" + responsiveSvg + "</div>");
}
@Override
public String getPageTitle() {
return getTranslation("page.title.job.history");
}
private void showEnlargedSignature(String svgContent) {
Dialog signatureDialog = new Dialog();
signatureDialog.setWidth("60vw");

View File

@@ -16,7 +16,7 @@ import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.router.BeforeEvent;
import com.vaadin.flow.router.HasUrlParameter;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.votianlt.model.CargoItem;
@@ -64,10 +64,9 @@ import java.util.List;
import java.util.Locale;
@Route(value = "job_summary", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@PageTitle("Zusammenfassung")
@RolesAllowed("USER")
@Slf4j
public class JobSummaryView extends Main implements HasUrlParameter<String> {
public class JobSummaryView extends Main implements HasUrlParameter<String>, HasDynamicTitle {
private final JobRepository jobRepository;
private final CargoItemRepository cargoItemRepository;
@@ -145,12 +144,12 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
}
// Create Send Message Button for toolbar
Button sendMessageButton = new Button("Nachricht senden");
Button sendMessageButton = new Button(getTranslation("jobsummary.button.sendmessage"));
sendMessageButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
sendMessageButton.addClickListener(e -> {
// Check if job has an app user assigned
if (job.getAppUser() == null || job.getAppUser().isBlank()) {
Notification.show("Diesem Auftrag ist kein App-Nutzer zugeordnet", 3000, Notification.Position.MIDDLE)
Notification.show(getTranslation("jobsummary.notification.noappuser"), 3000, Notification.Position.MIDDLE)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
return;
}
@@ -165,7 +164,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
});
// Create Job History Button for toolbar
Button jobHistoryButton = new Button("Job Historie");
Button jobHistoryButton = new Button(getTranslation("jobsummary.button.jobhistory"));
jobHistoryButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
jobHistoryButton.addClickListener(e -> {
getUI().ifPresent(ui -> ui.navigate("job_history/" + job.getId().toHexString()));
@@ -190,7 +189,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
topRow.setSpacing(true);
VerticalLayout pickupBox = borderedBox();
pickupBox.add(new H3("Abholung " + formatDateWithTime(job.getPickupDate(), job.getPickupTime())));
pickupBox.add(new H3(getTranslation("jobsummary.section.pickup") + " " + formatDateWithTime(job.getPickupDate(), job.getPickupTime())));
pickupBox.add(new Span(valueOrEmpty(job.getPickupCompany())));
pickupBox.add(new Span(valueOrEmpty(job.getPickupSalutation()) + (job.getPickupSalutation() != null ? " " : "")
+ valueOrEmpty(job.getPickupFirstName()) + (job.getPickupFirstName() != null ? " " : "")
@@ -199,7 +198,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
pickupBox.add(new Span(concatZipCity(job.getPickupZip(), job.getPickupCity())));
VerticalLayout deliveryBox = borderedBox();
deliveryBox.add(new H3("Lieferung " + formatDateWithTime(job.getDeliveryDate(), job.getDeliveryTime())));
deliveryBox.add(new H3(getTranslation("jobsummary.section.delivery") + " " + formatDateWithTime(job.getDeliveryDate(), job.getDeliveryTime())));
deliveryBox.add(new Span(valueOrEmpty(job.getDeliveryCompany())));
deliveryBox.add(new Span(valueOrEmpty(job.getDeliverySalutation())
+ (job.getDeliverySalutation() != null ? " " : "") + valueOrEmpty(job.getDeliveryFirstName())
@@ -214,7 +213,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
// Aufgaben
VerticalLayout tasksBox = borderedBox();
tasksBox.add(new H3("Zu quittierende Aufgaben"));
tasksBox.add(new H3(getTranslation("jobsummary.section.tasks")));
// Ensure consistent spacing and width for task cards
tasksBox.setSpacing(false);
@@ -224,7 +223,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
taskCards.clear();
if (tasks == null || tasks.isEmpty()) {
tasksBox.add(new Span("Keine Aufgaben"));
tasksBox.add(new Span(getTranslation("jobsummary.tasks.none")));
} else {
for (BaseTask task : tasks) {
if (task != null) {
@@ -246,9 +245,9 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
midRow.setSpacing(true);
VerticalLayout cargoBox = borderedBox();
cargoBox.add(new H3("Zu transportierende Fracht"));
cargoBox.add(new H3(getTranslation("jobsummary.section.cargo")));
if (cargoItems == null || cargoItems.isEmpty()) {
cargoBox.add(new Span("Keine Frachtangaben"));
cargoBox.add(new Span(getTranslation("jobsummary.cargo.none")));
} else {
for (CargoItem ci : cargoItems) {
if (ci == null)
@@ -265,22 +264,22 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
}
VerticalLayout infoBox = borderedBox();
infoBox.add(new H3("Weitere Informationen"));
infoBox.add(new H3(getTranslation("jobsummary.section.info")));
// Preis basierend auf den hinterlegten Leistungen berechnen
PriceCalculationResult priceResult = calculatePriceFromServices(job);
infoBox.add(new Span("Netto: " + formatPrice(priceResult.netAmount())));
infoBox.add(new Span("USt: " + formatPrice(priceResult.vatAmount())));
infoBox.add(new Span("Gesamt: " + formatPrice(priceResult.totalAmount())));
infoBox.add(new Span(getTranslation("jobsummary.info.netto") + ": " + formatPrice(priceResult.netAmount())));
infoBox.add(new Span(getTranslation("jobsummary.info.ust") + ": " + formatPrice(priceResult.vatAmount())));
infoBox.add(new Span(getTranslation("jobsummary.info.gesamt") + ": " + formatPrice(priceResult.totalAmount())));
if (job.getRemark() != null && !job.getRemark().isBlank()) {
infoBox.add(new Span("Bemerkung: " + job.getRemark()));
infoBox.add(new Span(getTranslation("jobsummary.info.bemerkung") + ": " + job.getRemark()));
}
if (job.isDigitalProcessing()) {
infoBox.add(new Span("Digitale Abwicklung per App: aktiviert"));
infoBox.add(new Span(getTranslation("jobsummary.info.digital")));
}
if (job.getAppUser() != null && !job.getAppUser().isBlank()) {
infoBox.add(new Span("App-Nutzer: " + resolveAppUserName(job.getAppUser())));
infoBox.add(new Span(getTranslation("jobsummary.info.appuser") + ": " + resolveAppUserName(job.getAppUser())));
}
cargoBox.setWidth("50%");
@@ -299,15 +298,15 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
buttonRow.setJustifyContentMode(HorizontalLayout.JustifyContentMode.CENTER);
buttonRow.getStyle().set("margin-top", "var(--lumo-space-l)");
Button completeButton = new Button("Auftrag manuell abschließen", new Icon(VaadinIcon.CHECK_CIRCLE));
Button completeButton = new Button(getTranslation("jobsummary.button.complete"), new Icon(VaadinIcon.CHECK_CIRCLE));
completeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
completeButton.addClickListener(e -> {
ConfirmDialog dialog = new ConfirmDialog();
dialog.setHeader("Auftrag abschließen");
dialog.setText("Möchten Sie den Auftrag " + job.getJobNumber() + " manuell abschließen?");
dialog.setHeader(getTranslation("jobsummary.dialog.complete.title"));
dialog.setText(getTranslation("jobsummary.dialog.complete.text", job.getJobNumber()));
dialog.setCancelable(true);
dialog.setCancelText("Abbrechen");
dialog.setConfirmText("Abschließen");
dialog.setCancelText(getTranslation("jobsummary.dialog.complete.cancel"));
dialog.setConfirmText(getTranslation("jobsummary.dialog.complete.confirm"));
dialog.setConfirmButtonTheme("primary");
dialog.addConfirmListener(ev -> {
try {
@@ -317,14 +316,14 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
jobRepository.save(job);
jobHistoryService.logStatusChange(job, oldStatus, JobStatus.COMPLETED, "Manuell");
Notification
.show("Auftrag " + job.getJobNumber() + " wurde abgeschlossen.", 3000,
.show(getTranslation("jobsummary.notification.completed", job.getJobNumber()), 3000,
Notification.Position.BOTTOM_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
// Re-render the page
getUI().ifPresent(ui -> ui.navigate("job_summary/" + job.getId().toHexString()));
} catch (Exception ex) {
Notification
.show("Fehler beim Abschließen: " + ex.getMessage(), 5000,
.show(getTranslation("jobsummary.notification.complete.error", ex.getMessage()), 5000,
Notification.Position.BOTTOM_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
}
@@ -666,6 +665,11 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
return s.replace("\\", "\\\\").replace("'", "\\'").replace("\n", " ").replace("\r", " ");
}
@Override
public String getPageTitle() {
return getTranslation("page.title.job.summary");
}
private void showTaskDetailsDialog(BaseTask task) {
Dialog dialog = new Dialog();
dialog.setWidth("500px");

View File

@@ -15,7 +15,7 @@ import com.vaadin.flow.router.AfterNavigationEvent;
import com.vaadin.flow.router.AfterNavigationObserver;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import de.assecutor.votianlt.security.totp.TwoFactorService;
@@ -33,13 +33,12 @@ import org.springframework.core.env.Environment;
import jakarta.annotation.PostConstruct;
@Route("login")
@PageTitle("Bei VotianLT anmelden")
@AnonymousAllowed
public class LoginView extends VerticalLayout implements BeforeEnterObserver, AfterNavigationObserver {
public class LoginView extends VerticalLayout implements BeforeEnterObserver, AfterNavigationObserver, HasDynamicTitle {
private final LoginForm loginForm = new LoginForm();
private final TextField twoFaField = new TextField("2FA Code");
private final Button verify2faButton = new Button("Code prüfen");
private final TextField twoFaField = new TextField(getTranslation("login.2fa.title"));
private final Button verify2faButton = new Button(getTranslation("login.2fa.button"));
private final Div flashBox = new Div();
@Autowired
@@ -76,7 +75,7 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
twoFaField.setVisible(false);
twoFaField.setMaxLength(6);
twoFaField.setPattern("[0-9]{6}");
twoFaField.setHelperText("Bitte 6-stelligen Code aus der E-Mail eingeben");
twoFaField.setHelperText(getTranslation("login.2fa.helper"));
verify2faButton.setVisible(false);
verify2faButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
verify2faButton.addClickListener(e -> handleVerify2fa());
@@ -84,10 +83,10 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
loginForm.setForgotPasswordButtonVisible(true);
loginForm.addForgotPasswordListener(e -> UI.getCurrent().navigate(ForgotPasswordRequestView.class));
H1 title = new H1("VotianLT");
H1 title = new H1(getTranslation("login.votianlt"));
title.getStyle().set("color", "var(--lumo-primary-color)");
Button registerButton = new Button("Noch kein Konto? Registrieren", e -> UI.getCurrent().navigate("register"));
Button registerButton = new Button(getTranslation("login.register"), e -> UI.getCurrent().navigate("register"));
registerButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
// Version display - will be set in @PostConstruct
@@ -147,7 +146,7 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
twoFaField.setVisible(true);
verify2faButton.setVisible(true);
twoFactorService.initiateTwoFactorFor(username);
Notification.show("2FA-Code per E-Mail gesendet.", 3000, Notification.Position.BOTTOM_CENTER);
Notification.show(getTranslation("login.2fa.sent"), 3000, Notification.Position.BOTTOM_CENTER);
} else {
// 2FA deaktiviert: Direkt anmelden
SecurityContextHolder.getContext().setAuthentication(auth);
@@ -173,19 +172,19 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
private void handleVerify2fa() {
if (pendingAuth == null) {
Notification.show("Bitte zuerst Benutzername und Passwort eingeben.", 3000,
Notification.show(getTranslation("login.2fa.no.credentials"), 3000,
Notification.Position.BOTTOM_CENTER);
return;
}
String username = pendingAuth.getName();
String code = twoFaField.getValue();
if (code == null || !code.matches("[0-9]{6}")) {
Notification.show("Ungültiger Code.", 3000, Notification.Position.BOTTOM_CENTER);
Notification.show(getTranslation("login.2fa.invalid.code"), 3000, Notification.Position.BOTTOM_CENTER);
return;
}
boolean ok = twoFactorService.verifyTwoFactorCode(username, code);
if (!ok) {
Notification.show("Falscher oder abgelaufener Code.", 3000, Notification.Position.BOTTOM_CENTER);
Notification.show(getTranslation("login.2fa.wrong.code"), 3000, Notification.Position.BOTTOM_CENTER);
return;
}
// 2FA korrekt: Benutzer nun anmelden
@@ -234,4 +233,9 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
}
}
}
@Override
public String getPageTitle() {
return getTranslation("page.title.login");
}
}

View File

@@ -24,7 +24,7 @@ import com.vaadin.flow.component.upload.UploadI18N;
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.RouteParameters;
import com.vaadin.flow.shared.Registration;
@@ -67,10 +67,9 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
@Route(value = "message-details/:clientId/:conversationId", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@PageTitle("Nachrichtenverlauf")
@RolesAllowed("USER")
@Slf4j
public class MessageDetailsView extends Main implements BeforeEnterObserver {
public class MessageDetailsView extends Main implements BeforeEnterObserver, HasDynamicTitle {
private final AppUserService appUserService;
private final MessageService messageService;
@@ -291,7 +290,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
AtomicReference<String> base64Ref = new AtomicReference<>();
Button confirmButton = new Button("Senden");
Button confirmButton = new Button(getTranslation("messagedetails.button.send"));
confirmButton.addThemeVariants(com.vaadin.flow.component.button.ButtonVariant.LUMO_PRIMARY);
confirmButton.setEnabled(false);
@@ -1011,4 +1010,9 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver {
}
});
}
@Override
public String getPageTitle() {
return getTranslation("page.title.message.history");
}
}

View File

@@ -12,8 +12,8 @@ import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Menu;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.dto.ClientMessageSummary;
import de.assecutor.votianlt.model.AppUser;
@@ -38,11 +38,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
import com.vaadin.flow.shared.Registration;
@Route(value = "messages", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@PageTitle("Nachrichten")
@Menu(order = 1, icon = "vaadin:envelope", title = "Nachrichten")
@RolesAllowed("USER")
@Slf4j
public class MessagesView extends Main {
public class MessagesView extends Main implements HasDynamicTitle {
private static final int POLL_INTERVAL_MS = 5000;
@@ -85,7 +84,7 @@ public class MessagesView extends Main {
}
private HorizontalLayout createHeaderLayout() {
H2 title = new H2("Nachrichten");
H2 title = new H2(getTranslation("messages.title"));
HorizontalLayout layout = new HorizontalLayout(title);
layout.setWidthFull();
layout.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER);
@@ -105,15 +104,15 @@ public class MessagesView extends Main {
span.getStyle().set("color", "var(--lumo-primary-color)");
}
return span;
})).setHeader("Status").setWidth("80px").setFlexGrow(0);
})).setHeader(getTranslation("messages.column.status")).setWidth("80px").setFlexGrow(0);
grid.addColumn(ClientMessageSummary::getClientName).setHeader("Client").setAutoWidth(true);
grid.addColumn(ClientMessageSummary::getClientEmail).setHeader("E-Mail").setAutoWidth(true);
grid.addColumn(ClientMessageSummary::getClientName).setHeader(getTranslation("messages.column.client")).setAutoWidth(true);
grid.addColumn(ClientMessageSummary::getClientEmail).setHeader(getTranslation("messages.column.email")).setAutoWidth(true);
grid.addColumn(new ComponentRenderer<>(summary -> {
Span span = new Span(String.valueOf(summary.getTotalMessages()));
return span;
})).setHeader("Nachrichten").setWidth("120px").setFlexGrow(0);
})).setHeader(getTranslation("messages.column.total")).setWidth("120px").setFlexGrow(0);
grid.addColumn(new ComponentRenderer<>(summary -> {
if (summary.getUnreadCount() > 0) {
@@ -123,11 +122,11 @@ public class MessagesView extends Main {
return span;
}
return new Span("0");
})).setHeader("Ungelesen").setWidth("100px").setFlexGrow(0);
})).setHeader(getTranslation("messages.column.unread")).setWidth("100px").setFlexGrow(0);
grid.addColumn(summary -> summary.getLastMessageDate() != null
? DateTimeFormatUtil.formatDateTime(summary.getLastMessageDate())
: "-").setHeader("Letzte Nachricht").setAutoWidth(true);
: "-").setHeader(getTranslation("messages.column.lastmessage")).setAutoWidth(true);
grid.addColumn(new ComponentRenderer<>(summary -> {
String preview = summary.getLastMessagePreview();
@@ -135,7 +134,7 @@ public class MessagesView extends Main {
preview = preview.substring(0, 47) + "...";
}
return new Span(preview != null ? preview : "-");
})).setHeader("Vorschau").setAutoWidth(true);
})).setHeader(getTranslation("messages.column.preview")).setAutoWidth(true);
// Add click listener to navigate to UserMessagesView
grid.addItemClickListener(event -> {
@@ -168,7 +167,7 @@ public class MessagesView extends Main {
} catch (Exception e) {
log.error("Error loading client summaries: {}", e.getMessage(), e);
Notification.show("Fehler beim Laden der Nachrichten", 3000, Notification.Position.MIDDLE)
Notification.show(getTranslation("messages.notification.error"), 3000, Notification.Position.MIDDLE)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
} finally {
loading.set(false);
@@ -252,14 +251,15 @@ public class MessagesView extends Main {
private String resolvePreview(Message message) {
if (message == null) {
return "(kein Inhalt)";
return getTranslation("messages.preview.empty");
}
if (message.getContentType() == MessageContentType.IMAGE) {
return "[Bildnachricht]";
return getTranslation("messages.preview.image");
}
return Optional.ofNullable(message.getContent()).filter(content -> !content.isBlank()).orElse("(kein Inhalt)");
return Optional.ofNullable(message.getContent()).filter(content -> !content.isBlank())
.orElse(getTranslation("messages.preview.empty"));
}
private String resolveParticipantKey(Message message) {
@@ -384,7 +384,8 @@ public class MessagesView extends Main {
+ "} catch(e) { console.warn('Notification sound failed:', e); }");
// Show notification
Notification notification = Notification.show("Neue Nachricht von " + senderName + ": " + preview, 4000,
Notification notification = Notification.show(
getTranslation("messages.notification.new", senderName, preview), 4000,
Notification.Position.TOP_END);
notification.addThemeVariants(NotificationVariant.LUMO_PRIMARY);
@@ -394,11 +395,16 @@ public class MessagesView extends Main {
private String resolveSenderName(String clientId) {
if (clientId == null || clientId.isBlank()) {
return "Unbekannt";
return getTranslation("messages.sender.unknown");
}
List<AppUser> appUsers = cachedAppUsers != null ? cachedAppUsers : List.of();
return appUsers.stream().filter(user -> clientId.equals(user.getIdAsString())
|| clientId.equals(user.getEmail()) || clientId.equals(user.getAppCode())).findFirst()
.map(this::buildClientName).orElse(clientId);
}
@Override
public String getPageTitle() {
return getTranslation("page.title.messages");
}
}

View File

@@ -14,7 +14,7 @@ import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import de.assecutor.votianlt.pages.base.ui.view.MainLayout;
@@ -40,10 +40,9 @@ import java.util.Locale;
* Modernisierte Optik: Responsive Karten, Lumo-Theme-Varianten, Status-Badges,
* Suche und leere Zustandsanzeige.
*/
@PageTitle("Meine Rechnungen")
@Route(value = "my-invoices", layout = MainLayout.class)
@RolesAllowed("USER")
public class MyInvoicesView extends Main {
public class MyInvoicesView extends Main implements HasDynamicTitle {
private final Grid<MyInvoiceRow> grid = new Grid<>(MyInvoiceRow.class, false);
private final List<MyInvoiceRow> allRows = new ArrayList<>(); // zunächst leer
@@ -60,7 +59,7 @@ public class MyInvoicesView extends Main {
getStyle().set("padding", "var(--lumo-space-m)");
// Toolbar / Titel
add(new ViewToolbar("Meine Rechnungen"));
add(new ViewToolbar(getTranslation("myinvoices.title")));
// Kartenbereich oben
add(createTopCards());
@@ -79,18 +78,19 @@ public class MyInvoicesView extends Main {
.set("gap", "var(--lumo-space-m)");
// Karte: Offene Rechnungen
Paragraph hint = new Paragraph("Momentan sind keine neuen Rechnungen für Sie im System gespeichert.");
Paragraph hint = new Paragraph(getTranslation("myinvoices.hint.noopen"));
hint.getStyle().set("color", "var(--lumo-success-text-color)");
Div openInvoicesCard = createCard("Offene Rechnungen", hint);
Div openInvoicesCard = createCard(getTranslation("myinvoices.card.open"), hint);
// Karte: Bankverbindung
VerticalLayout bankData = new VerticalLayout();
bankData.setPadding(false);
bankData.setSpacing(false);
bankData.add(labeledValue("Kreditinstitut", "Hamburger Sparkasse"),
labeledValue("Begünstigter", "Assecutor Data Service GmbH"),
labeledValue("IBAN", "DE67200505501217139888"), labeledValue("Verwendungszweck", "vlt-00000610"));
Div bankCard = createCard("Bankverbindung", bankData);
bankData.add(labeledValue(getTranslation("myinvoices.bank.institute"), "Hamburger Sparkasse"),
labeledValue(getTranslation("myinvoices.bank.beneficiary"), "Assecutor Data Service GmbH"),
labeledValue(getTranslation("myinvoices.bank.iban"), "DE67200505501217139888"),
labeledValue(getTranslation("myinvoices.bank.reference"), "vlt-00000610"));
Div bankCard = createCard(getTranslation("myinvoices.card.bank"), bankData);
container.add(openInvoicesCard, bankCard);
return container;
@@ -104,19 +104,19 @@ public class MyInvoicesView extends Main {
styleCard(card);
// Kopfzeile
H3 title = new H3("Rechnungen");
H3 title = new H3(getTranslation("myinvoices.section.title"));
title.getStyle().set("margin", "0");
// Steuerleiste: Seitengröße + Suche
Select<Integer> pageSize = new Select<>();
pageSize.setItems(10, 25, 50);
pageSize.setLabel("Einträge anzeigen");
pageSize.setLabel(getTranslation("myinvoices.filter.pagesize"));
pageSize.setValue(10);
pageSize.setWidth("160px");
TextField search = new TextField();
search.setLabel("Suchen");
search.setPlaceholder("Rechnungsnr., Datum, Betrag...");
search.setLabel(getTranslation("myinvoices.filter.search"));
search.setPlaceholder(getTranslation("myinvoices.filter.search.placeholder"));
search.setClearButtonVisible(true);
search.setValueChangeMode(ValueChangeMode.EAGER);
search.setWidth("300px");
@@ -137,12 +137,12 @@ public class MyInvoicesView extends Main {
grid.addThemeVariants(GridVariant.LUMO_ROW_STRIPES, GridVariant.LUMO_COMPACT,
GridVariant.LUMO_WRAP_CELL_CONTENT, GridVariant.LUMO_COLUMN_BORDERS);
grid.setWidthFull();
grid.addColumn(new ComponentRenderer<>(row -> statusBadge(row.status()))).setHeader("Status").setAutoWidth(true)
grid.addColumn(new ComponentRenderer<>(row -> statusBadge(row.status()))).setHeader(getTranslation("myinvoices.column.status")).setAutoWidth(true)
.setFlexGrow(0);
grid.addColumn(MyInvoicesView::formatInvoiceNumber).setHeader("Rechnungsnummer").setAutoWidth(true);
grid.addColumn(row -> DateTimeFormatUtil.formatDate(row.date())).setHeader("Datum").setAutoWidth(true)
grid.addColumn(MyInvoicesView::formatInvoiceNumber).setHeader(getTranslation("myinvoices.column.number")).setAutoWidth(true);
grid.addColumn(row -> DateTimeFormatUtil.formatDate(row.date())).setHeader(getTranslation("myinvoices.column.date")).setAutoWidth(true)
.setFlexGrow(0);
grid.addColumn(row -> CURRENCY_FMT.format(row.amount())).setHeader("Betrag").setAutoWidth(true)
grid.addColumn(row -> CURRENCY_FMT.format(row.amount())).setHeader(getTranslation("myinvoices.column.amount")).setAutoWidth(true)
.setTextAlign(ColumnTextAlign.END).setFlexGrow(0);
grid.setAllRowsVisible(true);
grid.setItems(allRows); // zunächst leer
@@ -155,8 +155,8 @@ public class MyInvoicesView extends Main {
// Leerer Zustand
emptyState.removeAll();
H4 emptyTitle = new H4("Keine Rechnungen vorhanden");
Paragraph emptyDesc = new Paragraph("Sobald Rechnungen vorliegen, erscheinen sie hier.");
H4 emptyTitle = new H4(getTranslation("myinvoices.empty.title"));
Paragraph emptyDesc = new Paragraph(getTranslation("myinvoices.empty.desc"));
emptyState.add(emptyTitle, emptyDesc);
emptyState.getStyle().set("text-align", "center").set("color", "var(--lumo-secondary-text-color)")
.set("padding", "var(--lumo-space-m)").set("border", "1px dashed var(--lumo-contrast-20pct)")
@@ -167,8 +167,8 @@ public class MyInvoicesView extends Main {
search.addValueChangeListener(e -> applyFilter(e.getValue()));
// Paginierungs-Buttons (vorerst ohne Funktion, als Platzhalter)
Button prev = new Button("Zurück", VaadinIcon.ANGLE_LEFT.create());
Button next = new Button("Nächste", VaadinIcon.ANGLE_RIGHT.create());
Button prev = new Button(getTranslation("myinvoices.button.prev"), VaadinIcon.ANGLE_LEFT.create());
Button next = new Button(getTranslation("myinvoices.button.next"), VaadinIcon.ANGLE_RIGHT.create());
prev.setEnabled(false);
next.setEnabled(false);
HorizontalLayout pager = new HorizontalLayout(prev, next);
@@ -298,4 +298,9 @@ public class MyInvoicesView extends Main {
return systemInvoiceService.generateInvoicePdfFromHtml(data);
}
@Override
public String getPageTitle() {
return getTranslation("page.title.myinvoices");
}
}

View File

@@ -11,7 +11,7 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.textfield.PasswordField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.VaadinSession;
import com.vaadin.flow.server.auth.AnonymousAllowed;
@@ -23,9 +23,8 @@ import java.time.Duration;
import java.time.LocalDateTime;
@Route("register")
@PageTitle("Bei VotianLT registrieren")
@AnonymousAllowed
public class RegisterView extends VerticalLayout {
public class RegisterView extends VerticalLayout implements HasDynamicTitle {
private final UserService userService;
private final EmailService emailService;
@@ -82,12 +81,12 @@ public class RegisterView extends VerticalLayout {
container.getStyle().set("box-shadow", "var(--lumo-box-shadow-s)");
// Titel
H1 title = new H1("Registrierung");
H1 title = new H1(getTranslation("register.title"));
title.getStyle().set("text-align", "center");
title.getStyle().set("color", "var(--lumo-primary-color)");
title.getStyle().set("margin-top", "0");
H2 subtitle = new H2("Erstellen Sie Ihr VotianLT-Konto");
H2 subtitle = new H2(getTranslation("register.subtitle"));
subtitle.getStyle().set("text-align", "center");
subtitle.getStyle().set("color", "var(--lumo-secondary-text-color)");
subtitle.getStyle().set("font-size", "var(--lumo-font-size-l)");
@@ -95,52 +94,52 @@ public class RegisterView extends VerticalLayout {
subtitle.getStyle().set("margin-bottom", "var(--lumo-space-l)");
// Formularfelder
emailField = new TextField("E-Mail-Adresse");
emailField = new TextField(getTranslation("register.email"));
emailField.setWidthFull();
emailField.setRequired(true);
emailField.setPlaceholder("");
passwordField = new PasswordField("Passwort");
passwordField = new PasswordField(getTranslation("register.password"));
passwordField.setWidthFull();
passwordField.setRequired(true);
passwordField.setPlaceholder("Mindestens 6 Zeichen");
passwordField.setPlaceholder(getTranslation("register.password.placeholder"));
confirmPasswordField = new PasswordField("Passwort bestätigen");
confirmPasswordField = new PasswordField(getTranslation("register.password.confirm"));
confirmPasswordField.setWidthFull();
confirmPasswordField.setRequired(true);
confirmPasswordField.setPlaceholder("Passwort wiederholen");
confirmPasswordField.setPlaceholder(getTranslation("register.password.confirm.placeholder"));
// Pflichtfelder aus EditProfileView
firstNameField = new TextField("Vorname");
firstNameField = new TextField(getTranslation("register.firstname"));
firstNameField.setWidthFull();
firstNameField.setRequired(true);
lastNameField = new TextField("Nachname");
lastNameField = new TextField(getTranslation("register.lastname"));
lastNameField.setWidthFull();
lastNameField.setRequired(true);
phoneField = new TextField("Telefonnummer");
phoneField = new TextField(getTranslation("register.phone"));
phoneField.setWidthFull();
phoneField.setRequired(true);
companyField = new TextField("Firma");
companyField = new TextField(getTranslation("register.company"));
companyField.setWidthFull();
companyField.setRequired(true);
streetField = new TextField("Straße");
streetField = new TextField(getTranslation("register.street"));
streetField.setWidthFull();
streetField.setRequired(true);
houseNumberField = new TextField("Hausnr");
houseNumberField = new TextField(getTranslation("register.housenr"));
houseNumberField.setWidthFull();
houseNumberField.setRequired(true);
zipField = new TextField("Postleitzahl");
zipField = new TextField(getTranslation("register.zip"));
zipField.setWidthFull();
zipField.setRequired(true);
cityField = new TextField("Stadt");
cityField = new TextField(getTranslation("register.city"));
cityField.setWidthFull();
cityField.setRequired(true);
codeField = new TextField("Bestätigungscode (6 Ziffern)");
codeField = new TextField(getTranslation("register.code.label"));
codeField.setWidthFull();
codeField.setMaxLength(6);
codeField.setPattern("\\d{6}");
codeField.setPlaceholder("z. B. 123456");
codeField.setPlaceholder(getTranslation("register.code.placeholder"));
codeField.setVisible(false);
codeField.addValueChangeListener(e -> {
String v = e.getValue();
@@ -150,22 +149,22 @@ public class RegisterView extends VerticalLayout {
});
// Buttons
submitButton = new Button("Registrieren", event -> onStartRegistration());
submitButton = new Button(getTranslation("register.button.submit"), event -> onStartRegistration());
submitButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE);
submitButton.setWidthFull();
verifyButton = new Button("Code prüfen und registrieren", event -> onVerifyCode());
verifyButton = new Button(getTranslation("register.button.verify"), event -> onVerifyCode());
verifyButton.addThemeVariants(ButtonVariant.LUMO_SUCCESS);
verifyButton.setWidthFull();
verifyButton.setVisible(false);
resendButton = new Button("Code erneut senden", event -> onResendCode());
resendButton = new Button(getTranslation("register.button.resend"), event -> onResendCode());
resendButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
resendButton.setWidthFull();
resendButton.setVisible(false);
// Zurück-Link
Button backButton = new Button("Zurück zur Startseite", event -> getUI().ifPresent(ui -> ui.navigate("")));
Button backButton = new Button(getTranslation("register.button.back"), event -> getUI().ifPresent(ui -> ui.navigate("")));
backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
backButton.setWidthFull();
@@ -418,4 +417,9 @@ public class RegisterView extends VerticalLayout {
int num = random.nextInt(1_000_000); // 0..999999
return String.format("%06d", num);
}
@Override
public String getPageTitle() {
return getTranslation("page.title.register");
}
}

View File

@@ -7,7 +7,7 @@ import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.Customer;
import de.assecutor.votianlt.pages.service.CustomerService;
@@ -15,10 +15,9 @@ import de.assecutor.votianlt.security.SecurityService;
import jakarta.annotation.security.RolesAllowed;
import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("Kunden")
@Route(value = "customers", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({ "USER", "ADMIN" })
public class ShowCustomersView extends VerticalLayout {
public class ShowCustomersView extends VerticalLayout implements HasDynamicTitle {
private final CustomerService customerService;
private final SecurityService securityService;
@@ -35,8 +34,8 @@ public class ShowCustomersView extends VerticalLayout {
// Header with title and add button
HorizontalLayout header = new HorizontalLayout();
header.setWidthFull();
header.add(new H2("Kunden"));
Button addCustomerButton = new Button("Kunde hinzufügen", new Icon(VaadinIcon.PLUS));
header.add(new H2(getTranslation("customers.title")));
Button addCustomerButton = new Button(getTranslation("customers.button.add"), new Icon(VaadinIcon.PLUS));
header.add(addCustomerButton);
header.setJustifyContentMode(JustifyContentMode.BETWEEN);
header.setAlignItems(Alignment.CENTER);
@@ -44,23 +43,23 @@ public class ShowCustomersView extends VerticalLayout {
// Add hint text
var hintText = new com.vaadin.flow.component.html.Paragraph(
"Klicken Sie auf einen Eintrag, um ihn zu bearbeiten.");
getTranslation("customers.hint.click"));
hintText.getStyle().set("color", "var(--lumo-secondary-text-color)");
hintText.getStyle().set("font-size", "var(--lumo-font-size-s)");
add(hintText);
// Configure grid columns
grid.addColumn(Customer::getCompanyName).setHeader("Firma").setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(Customer::getCompanyName).setHeader(getTranslation("customers.column.company")).setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(customer -> (customer.getFirstname() != null ? customer.getFirstname() : "") + " "
+ (customer.getLastName() != null ? customer.getLastName() : "")).setHeader("Name").setAutoWidth(true)
+ (customer.getLastName() != null ? customer.getLastName() : "")).setHeader(getTranslation("customers.column.name")).setAutoWidth(true)
.setFlexGrow(1).setSortable(true);
grid.addColumn(Customer::getMail).setHeader("E-Mail").setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(Customer::getTelephone).setHeader("Telefon").setAutoWidth(true).setSortable(true);
grid.addColumn(Customer::getMail).setHeader(getTranslation("customers.column.email")).setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(Customer::getTelephone).setHeader(getTranslation("customers.column.phone")).setAutoWidth(true).setSortable(true);
grid.addColumn(customer -> (customer.getStreet() != null ? customer.getStreet() : "") + " "
+ (customer.getHouseNumber() != null ? customer.getHouseNumber() : "")).setHeader("Straße")
+ (customer.getHouseNumber() != null ? customer.getHouseNumber() : "")).setHeader(getTranslation("customers.column.street"))
.setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(customer -> (customer.getZip() != null ? customer.getZip() : "") + " "
+ (customer.getCity() != null ? customer.getCity() : "")).setHeader("Ort").setAutoWidth(true)
+ (customer.getCity() != null ? customer.getCity() : "")).setHeader(getTranslation("customers.column.city")).setAutoWidth(true)
.setFlexGrow(1).setSortable(true);
grid.setMultiSort(true);
@@ -93,4 +92,9 @@ public class ShowCustomersView extends VerticalLayout {
.filter(c -> c.getCreatedBy() != null && c.getCreatedBy().equals(currentUserId)).toList();
grid.setItems(ownCustomers);
}
@Override
public String getPageTitle() {
return getTranslation("page.title.show.customers");
}
}

View File

@@ -16,7 +16,7 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.server.StreamResource;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.Job;
import de.assecutor.votianlt.model.JobStatus;
@@ -33,16 +33,15 @@ import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime;
import java.util.Map;
@PageTitle("Aufträge")
@Route(value = "jobs", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({ "USER" })
@Slf4j
public class ShowJobsView extends VerticalLayout {
public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
private final DatePicker startDate = new DatePicker("Startdatum");
private final DatePicker endDate = new DatePicker("Enddatum");
private final TextField searchField = new TextField("Auftragsnummer suchen");
private final ComboBox<String> statusFilter = new ComboBox<>("Status");
private final DatePicker startDate = new DatePicker();
private final DatePicker endDate = new DatePicker();
private final TextField searchField = new TextField();
private final ComboBox<String> statusFilter = new ComboBox<>();
private final JobRepository jobRepository;
private final JobHistoryService jobHistoryService;
private final SecurityService securityService;
@@ -63,28 +62,33 @@ public class ShowJobsView extends VerticalLayout {
setPadding(true);
setSpacing(true);
H2 title = new H2("Aufträge");
startDate.setLabel(getTranslation("jobs.filter.startdate"));
endDate.setLabel(getTranslation("jobs.filter.enddate"));
searchField.setLabel(getTranslation("jobs.filter.search"));
statusFilter.setLabel(getTranslation("jobs.filter.status"));
H2 title = new H2(getTranslation("nav.jobs"));
add(title);
// Configure status filter
statusFilter.setItems("Alle", "Offen", "Erledigt");
statusFilter.setValue("Offen");
statusFilter.setItems(getTranslation("jobs.status.all"), getTranslation("jobs.status.open"), getTranslation("jobs.status.done"));
statusFilter.setValue(getTranslation("jobs.status.open"));
statusFilter.setWidth("150px");
// Configure search field
searchField.setPlaceholder("Auftragsnummer eingeben...");
searchField.setPlaceholder(getTranslation("jobs.filter.search.placeholder"));
searchField.setClearButtonVisible(true);
searchField.setWidth("200px");
// Filterleiste mit Export-Button am rechten Rand
Button applyFilter = new Button("Anwenden");
Button applyFilter = new Button(getTranslation("jobs.filter.apply"));
HorizontalLayout leftFilters = new HorizontalLayout(startDate, endDate, searchField, statusFilter, applyFilter);
leftFilters.setAlignItems(Alignment.END);
HorizontalLayout filterBar = new HorizontalLayout();
filterBar.setWidthFull();
filterBar.add(leftFilters);
Button exportButton = new Button("CSV Export");
Button exportButton = new Button(getTranslation("jobs.button.csvexport"));
filterBar.add(exportButton);
filterBar.setJustifyContentMode(JustifyContentMode.BETWEEN);
filterBar.setAlignItems(Alignment.END);
@@ -104,12 +108,12 @@ public class ShowJobsView extends VerticalLayout {
endDate.addValueChangeListener(e -> loadData());
// Configure grid columns: Auftraggeber, Auftragsnummer, Auftragsdatum, Zielort
grid.addColumn(job -> extractCompanyName(job.getCustomerSelection())).setHeader("Auftraggeber")
grid.addColumn(job -> extractCompanyName(job.getCustomerSelection())).setHeader(getTranslation("jobs.column.customer"))
.setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(Job::getJobNumber).setHeader("Auftragsnummer").setAutoWidth(true).setSortable(true);
grid.addColumn(job -> DateTimeFormatUtil.formatDateTime(job.getCreatedAt())).setHeader("Auftragsdatum")
grid.addColumn(Job::getJobNumber).setHeader(getTranslation("jobs.column.jobnumber")).setAutoWidth(true).setSortable(true);
grid.addColumn(job -> DateTimeFormatUtil.formatDateTime(job.getCreatedAt())).setHeader(getTranslation("jobs.column.jobdate"))
.setAutoWidth(true).setSortable(true);
grid.addColumn(Job::getDeliveryCity).setHeader("Zielort").setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(Job::getDeliveryCity).setHeader(getTranslation("jobs.column.destination")).setAutoWidth(true).setFlexGrow(1).setSortable(true);
// Action column: manual completion for jobs without digital processing
grid.addComponentColumn(job -> {
@@ -117,7 +121,7 @@ public class ShowJobsView extends VerticalLayout {
&& job.getStatus() != JobStatus.CANCELLED) {
Button completeBtn = new Button(new Icon(VaadinIcon.CHECK_CIRCLE));
completeBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SUCCESS);
completeBtn.setTooltipText("Auftrag manuell abschließen");
completeBtn.setTooltipText(getTranslation("jobs.tooltip.complete"));
completeBtn.addClickListener(e -> {
e.getSource().getElement().getNode(); // prevent row click
showCompleteJobDialog(job);
@@ -132,7 +136,7 @@ public class ShowJobsView extends VerticalLayout {
if (job.getStatus() == JobStatus.COMPLETED) {
Button invoiceBtn = new Button(new Icon(VaadinIcon.DOLLAR));
invoiceBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SUCCESS);
invoiceBtn.setTooltipText("Rechnung erstellen");
invoiceBtn.setTooltipText(getTranslation("jobs.tooltip.createinvoice"));
invoiceBtn.addClickListener(e -> {
e.getSource().getElement().getNode(); // prevent row click
getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString()));
@@ -150,7 +154,7 @@ public class ShowJobsView extends VerticalLayout {
}
Button deleteBtn = new Button(new Icon(VaadinIcon.TRASH));
deleteBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ERROR);
deleteBtn.setTooltipText("Auftrag löschen");
deleteBtn.setTooltipText(getTranslation("jobs.tooltip.delete"));
deleteBtn.addClickListener(e -> {
e.getSource().getElement().getNode(); // prevent row click
showDeleteJobDialog(job);
@@ -180,11 +184,11 @@ public class ShowJobsView extends VerticalLayout {
private void showCompleteJobDialog(Job job) {
ConfirmDialog dialog = new ConfirmDialog();
dialog.setHeader("Auftrag abschließen");
dialog.setText("Möchten Sie den Auftrag " + job.getJobNumber() + " manuell abschließen?");
dialog.setHeader(getTranslation("jobs.dialog.complete.title"));
dialog.setText(getTranslation("jobs.dialog.complete.text", job.getJobNumber()));
dialog.setCancelable(true);
dialog.setCancelText("Abbrechen");
dialog.setConfirmText("Abschließen");
dialog.setCancelText(getTranslation("button.cancel"));
dialog.setConfirmText(getTranslation("jobs.dialog.complete.confirm"));
dialog.setConfirmButtonTheme("primary");
dialog.addConfirmListener(e -> {
try {
@@ -193,11 +197,11 @@ public class ShowJobsView extends VerticalLayout {
job.setUpdatedAt(LocalDateTime.now());
jobRepository.save(job);
jobHistoryService.logStatusChange(job, oldStatus, JobStatus.COMPLETED, "Manuell");
Notification.show("Auftrag " + job.getJobNumber() + " wurde abgeschlossen.", 3000,
Notification.show(getTranslation("jobs.notification.completed", job.getJobNumber()), 3000,
Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_SUCCESS);
loadData();
} catch (Exception ex) {
Notification.show("Fehler beim Abschließen: " + ex.getMessage(), 5000, Notification.Position.BOTTOM_END)
Notification.show(getTranslation("jobs.notification.complete.error", ex.getMessage()), 5000, Notification.Position.BOTTOM_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
}
});
@@ -206,12 +210,11 @@ public class ShowJobsView extends VerticalLayout {
private void showDeleteJobDialog(Job job) {
ConfirmDialog dialog = new ConfirmDialog();
dialog.setHeader("Auftrag löschen");
dialog.setText("Möchten Sie den Auftrag " + job.getJobNumber()
+ " wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.");
dialog.setHeader(getTranslation("jobs.dialog.delete.title"));
dialog.setText(getTranslation("jobs.dialog.delete.text", job.getJobNumber()));
dialog.setCancelable(true);
dialog.setCancelText("Abbrechen");
dialog.setConfirmText("Löschen");
dialog.setCancelText(getTranslation("button.cancel"));
dialog.setConfirmText(getTranslation("button.delete"));
dialog.setConfirmButtonTheme("error primary");
dialog.addConfirmListener(e -> {
try {
@@ -219,11 +222,11 @@ public class ShowJobsView extends VerticalLayout {
notifyClientJobDeleted(job);
jobRepository.delete(job);
Notification.show("Auftrag " + job.getJobNumber() + " wurde gelöscht.", 3000,
Notification.show(getTranslation("jobs.notification.deleted", job.getJobNumber()), 3000,
Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_SUCCESS);
loadData();
} catch (Exception ex) {
Notification.show("Fehler beim Löschen: " + ex.getMessage(), 5000, Notification.Position.BOTTOM_END)
Notification.show(getTranslation("jobs.notification.delete.error", ex.getMessage()), 5000, Notification.Position.BOTTOM_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
}
});
@@ -264,9 +267,9 @@ public class ShowJobsView extends VerticalLayout {
String selectedStatus = statusFilter.getValue();
java.util.List<JobStatus> statusList;
if ("Erledigt".equals(selectedStatus)) {
if (getTranslation("jobs.status.done").equals(selectedStatus)) {
statusList = java.util.List.of(JobStatus.DELIVERED, JobStatus.COMPLETED, JobStatus.CANCELLED);
} else if ("Offen".equals(selectedStatus)) {
} else if (getTranslation("jobs.status.open").equals(selectedStatus)) {
statusList = java.util.List.of(JobStatus.CREATED, JobStatus.IN_PROGRESS, JobStatus.PICKUP_SCHEDULED,
JobStatus.PICKED_UP, JobStatus.IN_TRANSIT);
} else { // "Alle"
@@ -311,7 +314,10 @@ public class ShowJobsView extends VerticalLayout {
private String generateCsv(java.util.List<Job> jobs) {
StringBuilder csv = new StringBuilder();
// CSV Header
csv.append("Auftraggeber,Auftragsnummer,Auftragsdatum,Zielort\n");
csv.append(getTranslation("csv.header.customer")).append(",")
.append(getTranslation("csv.header.jobnumber")).append(",")
.append(getTranslation("csv.header.jobdate")).append(",")
.append(getTranslation("csv.header.destination")).append("\n");
// CSV Data
for (Job job : jobs) {
@@ -344,4 +350,9 @@ public class ShowJobsView extends VerticalLayout {
}
return customerSelection.trim();
}
@Override
public String getPageTitle() {
return getTranslation("page.title.jobs");
}
}

View File

@@ -11,7 +11,7 @@ import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
@@ -20,9 +20,8 @@ import de.assecutor.votianlt.security.SecurityService;
import org.springframework.beans.factory.annotation.Value;
@Route("")
@PageTitle("VotianLT - Willkommen")
@AnonymousAllowed
public class StartView extends VerticalLayout implements BeforeEnterObserver {
public class StartView extends VerticalLayout implements BeforeEnterObserver, HasDynamicTitle {
private final SecurityService securityService;
private final String appVersion;
@@ -89,10 +88,10 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
navButtons.setSpacing(true);
navButtons.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
Button loginBtn = new Button("Anmelden", event -> login());
Button loginBtn = new Button(getTranslation("start.button.login"), event -> login());
loginBtn.addThemeVariants(ButtonVariant.LUMO_CONTRAST);
Button registerBtn = new Button("Registrieren", event -> register());
Button registerBtn = new Button(getTranslation("start.button.register"), event -> register());
registerBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
navButtons.add(loginBtn, registerBtn);
@@ -181,7 +180,7 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
heroIcon.setSize("120px");
heroIcon.getStyle().set("color", "var(--lumo-primary-color)");
H1 heroTitle = new H1("VotianLT - Ihr digitaler Transportpartner");
H1 heroTitle = new H1(getTranslation("start.title"));
heroTitle.getStyle().set("text-align", "center");
heroTitle.getStyle().set("color", "var(--lumo-primary-text-color)");
heroTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)");
@@ -193,7 +192,7 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
heroDescription.getStyle().set("max-width", "600px");
heroDescription.getStyle().set("font-size", "var(--lumo-font-size-l)");
Button ctaButton = new Button("Jetzt kostenlos testen", event -> register());
Button ctaButton = new Button(getTranslation("cta.freetest"), event -> register());
ctaButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE);
heroSection.add(heroIcon, heroTitle, heroDescription, ctaButton);
@@ -344,4 +343,9 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver {
private void login() {
UI.getCurrent().navigate("login");
}
@Override
public String getPageTitle() {
return getTranslation("page.title.welcome");
}
}

View File

@@ -17,7 +17,7 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.Scroller;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.ai.service.AiStatisticsService;
import de.assecutor.votianlt.util.DateTimeFormatUtil;
@@ -27,12 +27,11 @@ import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.util.UUID;
@PageTitle("KI-Statistiken")
@Route(value = "statistics", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({ "USER", "ADMIN" })
@JavaScript("https://cdn.jsdelivr.net/npm/chart.js")
@Slf4j
public class StatisticsView extends VerticalLayout {
public class StatisticsView extends VerticalLayout implements HasDynamicTitle {
private final AiStatisticsService aiStatisticsService;
private final VerticalLayout chatContainer;
@@ -43,7 +42,7 @@ public class StatisticsView extends VerticalLayout {
// Prompt Field initialisieren
this.promptField = new TextField();
this.promptField.setPlaceholder("Stelle eine Frage zu deinen Statistiken...");
this.promptField.setPlaceholder(getTranslation("statistics.prompt.placeholder"));
this.promptField.setWidthFull();
this.promptField.setClearButtonVisible(true);
this.promptField.addKeyPressListener(Key.ENTER, e -> sendPrompt());
@@ -87,10 +86,10 @@ public class StatisticsView extends VerticalLayout {
Icon aiIcon = VaadinIcon.MAGIC.create();
aiIcon.getStyle().set("color", "var(--lumo-primary-color)");
H2 title = new H2("KI-Statistik-Assistent");
H2 title = new H2(getTranslation("statistics.title"));
title.getStyle().set("margin", "0").set("font-size", "var(--lumo-font-size-xl)");
Span subtitle = new Span("Frage mich zu Aufträgen, Umsätzen und Statistiken");
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)");
@@ -113,11 +112,12 @@ public class StatisticsView extends VerticalLayout {
sendButton.getStyle().set("min-width", "50px");
// Quick Action Buttons
Button jobCountBtn = createQuickActionButton("Aufträge zählen",
"Wie viele Aufträge gibt es insgesamt und nach Status?");
Button revenueBtn = createQuickActionButton("Umsatz", "Zeige mir den Umsatz pro Kunde.");
Button trendBtn = createQuickActionButton("Monatstrend",
"Zeige mir den Monatstrend der Aufträge für dieses Jahr.");
Button jobCountBtn = createQuickActionButton(getTranslation("statistics.quick.jobcount"),
getTranslation("statistics.quick.jobcount.prompt"));
Button revenueBtn = createQuickActionButton(getTranslation("statistics.quick.revenue"),
getTranslation("statistics.quick.revenue.prompt"));
Button trendBtn = createQuickActionButton(getTranslation("statistics.quick.trend"),
getTranslation("statistics.quick.trend.prompt"));
HorizontalLayout quickActions = new HorizontalLayout(jobCountBtn, revenueBtn, trendBtn);
quickActions.setSpacing(true);
@@ -179,7 +179,7 @@ public class StatisticsView extends VerticalLayout {
log.error("Error processing AI request", e);
ui.access(() -> {
chatContainer.remove(loadingMessage);
addErrorMessage("Entschuldigung, es gab einen Fehler bei der Verarbeitung: " + e.getMessage());
addErrorMessage(getTranslation("statistics.error", e.getMessage()));
scrollToBottom();
});
}
@@ -234,7 +234,7 @@ public class StatisticsView extends VerticalLayout {
aiIcon.setSize("16px");
aiIcon.getStyle().set("color", "var(--lumo-primary-color)");
Span aiLabel = new Span("KI-Assistent");
Span aiLabel = new Span(getTranslation("statistics.ai.label"));
aiLabel.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-s)");
header.add(aiIcon, aiLabel);
@@ -245,7 +245,7 @@ public class StatisticsView extends VerticalLayout {
textDiv.getStyle().set("margin-top", "var(--lumo-space-s)");
String responseText = response.textResponse();
if (responseText == null || responseText.isBlank()) {
responseText = "*Die Statistikdaten wurden erfolgreich abgerufen und werden im Diagramm angezeigt.*";
responseText = getTranslation("statistics.data.fetched");
}
textDiv.getElement().setProperty("innerHTML", formatMarkdown(responseText));
bubble.add(textDiv);
@@ -307,7 +307,7 @@ public class StatisticsView extends VerticalLayout {
.set("padding", "var(--lumo-space-s) var(--lumo-space-m)")
.set("border-radius", "var(--lumo-border-radius-l)");
Span dots = new Span("Analysiere...");
Span dots = new Span(getTranslation("statistics.loading"));
dots.getStyle().set("color", "var(--lumo-secondary-text-color)").set("font-style", "italic");
bubble.add(dots);
@@ -650,4 +650,9 @@ public class StatisticsView extends VerticalLayout {
super.onAttach(attachEvent);
scrollToBottom();
}
@Override
public String getPageTitle() {
return getTranslation("page.title.statistics");
}
}

View File

@@ -11,8 +11,8 @@ import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.BeforeEvent;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.HasUrlParameter;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.AppUser;
import de.assecutor.votianlt.model.Message;
@@ -36,10 +36,9 @@ import java.util.Optional;
import java.util.stream.Collectors;
@Route(value = "user-messages", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@PageTitle("Nachrichten")
@RolesAllowed("USER")
@Slf4j
public class UserMessagesView extends Main implements HasUrlParameter<String> {
public class UserMessagesView extends Main implements HasUrlParameter<String>, HasDynamicTitle {
private final AppUserService appUserService;
private final MessageService messageService;
@@ -79,7 +78,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
}
String clientName = client != null ? client.getVorname() + " " + client.getNachname()
: Optional.ofNullable(participantKey).orElse("Unbekannter Teilnehmer");
: Optional.ofNullable(participantKey).orElse(getTranslation("usermessages.unknown.participant"));
HorizontalLayout headerLayout = createHeaderLayout(clientName);
contentLayout.add(headerLayout);
@@ -96,10 +95,10 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
}
private HorizontalLayout createHeaderLayout(String clientName) {
Button backButton = new Button("Zurück", VaadinIcon.ARROW_LEFT.create());
Button backButton = new Button(getTranslation("button.back"), VaadinIcon.ARROW_LEFT.create());
backButton.addClickListener(e -> UI.getCurrent().navigate("messages"));
H2 title = new H2("Nachrichten mit " + clientName);
H2 title = new H2(getTranslation("usermessages.title.with", clientName));
HorizontalLayout layout = new HorizontalLayout(backButton, title);
layout.setWidthFull();
@@ -118,7 +117,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
section.setWidthFull();
section.getStyle().set("margin-right", "20px");
H3 title = new H3("Allgemeine Nachrichten");
H3 title = new H3(getTranslation("usermessages.general.title"));
section.add(title);
List<Message> sortedMessages = new ArrayList<>();
@@ -136,7 +135,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
LocalDateTime lastMessageTime = latest != null ? latest.getCreatedAt() : null;
String preview = resolvePreview(latest);
section.add(createMessageCard("Allgemeine Unterhaltung", preview, lastMessageTime, messageCount, unreadCount,
section.add(createMessageCard(getTranslation("usermessages.general.conversation"), preview, lastMessageTime, messageCount, unreadCount,
"general"));
return section;
@@ -151,11 +150,11 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
section.setWidthFull();
section.getStyle().set("margin-right", "20px");
H3 title = new H3("Nachrichten zu Aufträgen");
H3 title = new H3(getTranslation("usermessages.job.title"));
section.add(title);
if (jobMessages == null || jobMessages.isEmpty()) {
section.add(new Span("Keine auftragsbezogenen Nachrichten vorhanden."));
section.add(new Span(getTranslation("usermessages.no.job.messages")));
return section;
}
@@ -168,7 +167,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
int unreadCount = (int) messages.stream()
.filter(message -> message.getOrigin() == MessageOrigin.CLIENT && !message.isRead()).count();
String conversationTitle = "Auftrag " + jobKey;
String conversationTitle = getTranslation("usermessages.job.conversation", jobKey);
section.add(createMessageCard(conversationTitle, resolvePreview(latest), latest.getCreatedAt(),
messages.size(), unreadCount, "job-" + sanitizeConversationId(jobKey)));
});
@@ -182,7 +181,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
}
if (message.getContentType() == MessageContentType.IMAGE) {
return "[Bildnachricht]";
return getTranslation("messages.preview.image");
}
return Optional.ofNullable(message.getContent()).map(String::trim).orElse("");
@@ -235,7 +234,8 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
// Preview text
Span preview = new Span(
Optional.ofNullable(lastMessagePreview).filter(s -> !s.isBlank()).orElse("(kein Inhalt)"));
Optional.ofNullable(lastMessagePreview).filter(s -> !s.isBlank())
.orElse(getTranslation("usermessages.preview.empty")));
preview.getStyle().set("color", "#666666");
preview.getStyle().set("font-size", "14px");
@@ -247,7 +247,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
timeSpan.getStyle().set("color", "#999999");
timeSpan.getStyle().set("font-size", "12px");
Span countSpan = new Span(messageCount + " Nachrichten");
Span countSpan = new Span(getTranslation("usermessages.message.count", messageCount));
countSpan.getStyle().set("color", "#999999");
countSpan.getStyle().set("font-size", "12px");
@@ -270,7 +270,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
private String resolveJobKey(Message message) {
if (message == null) {
return "Unbekannt";
return getTranslation("usermessages.unknown");
}
String jobNumber = Optional.ofNullable(message.getJobNumber()).filter(s -> !s.isBlank()).orElse(null);
if (jobNumber != null) {
@@ -280,7 +280,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
if (jobId != null && !jobId.isBlank()) {
return jobId;
}
return "Unbekannt";
return getTranslation("usermessages.unknown");
}
private String sanitizeConversationId(String value) {
@@ -289,4 +289,9 @@ public class UserMessagesView extends Main implements HasUrlParameter<String> {
}
return value.replaceAll("[^a-zA-Z0-9_-]", "_");
}
@Override
public String getPageTitle() {
return getTranslation("page.title.user.messages");
}
}

View File

@@ -5,7 +5,7 @@ import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Menu;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
@@ -13,25 +13,24 @@ import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import jakarta.annotation.security.RolesAllowed;
@Route(value = "verwaltung", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@PageTitle("Verwaltung")
@Menu(order = 5, icon = "vaadin:cogs", title = "Verwaltung")
@RolesAllowed("USER")
public class VerwaltungView extends Main {
public class VerwaltungView extends Main implements HasDynamicTitle {
public VerwaltungView() {
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
add(new ViewToolbar("Verwaltung"));
add(new ViewToolbar(getTranslation("verwaltung.title")));
// Content
VerticalLayout content = new VerticalLayout();
H1 title = new H1("Verwaltung");
H1 title = new H1(getTranslation("verwaltung.title"));
title.getStyle().set("color", "var(--lumo-primary-color)");
Paragraph description = new Paragraph("Willkommen im Verwaltungsbereich. Wählen Sie eine Option aus dem Menü.");
Paragraph description = new Paragraph(getTranslation("verwaltung.description"));
description.getStyle().set("color", "var(--lumo-secondary-text-color)");
content.add(title, description);
@@ -41,4 +40,9 @@ public class VerwaltungView extends Main {
add(content);
}
}
@Override
public String getPageTitle() {
return getTranslation("page.title.verwaltung");
}
}

View File

@@ -0,0 +1,88 @@
package de.assecutor.votianlt.service;
import com.vaadin.flow.component.UI;
import de.assecutor.votianlt.model.Language;
import de.assecutor.votianlt.model.User;
import de.assecutor.votianlt.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Locale;
import java.util.ResourceBundle;
@Service
public class LanguageService {
private final UserRepository userRepository;
@Autowired
public LanguageService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void updateUserLanguage(User user, Language language) {
user.setLanguage(language);
userRepository.save(user);
}
public Language getUserLanguage(User user) {
return user.getLanguage();
}
public String getTranslation(String key, Language language) {
try {
Locale locale;
switch (language) {
case DE:
locale = Locale.GERMAN;
break;
case EN:
locale = Locale.ENGLISH;
break;
case FR:
locale = Locale.FRENCH;
break;
case ES:
locale = new Locale("es", "ES");
break;
default:
locale = Locale.GERMAN;
}
ResourceBundle bundle = ResourceBundle.getBundle("messages", locale);
return bundle.getString(key);
} catch (Exception e) {
// Fallback to key itself if translation not found
return key;
}
}
public String getTranslation(String key, User user) {
return getTranslation(key, user.getLanguage());
}
public void applyLocaleForUser(User user) {
if (user != null && user.getLanguage() != null) {
// Get the UI instance and set the locale
UI ui = UI.getCurrent();
if (ui != null) {
Locale locale = getLocaleFromLanguage(user.getLanguage());
ui.setLocale(locale);
}
}
}
private Locale getLocaleFromLanguage(Language language) {
return switch (language) {
case DE -> Locale.GERMAN;
case EN -> Locale.ENGLISH;
case FR -> Locale.FRENCH;
case ES -> new Locale("es", "ES");
default -> Locale.GERMAN;
};
}
public Language getCurrentUserLanguage(User user) {
return user != null ? user.getLanguage() : Language.DE;
}
}

View File

@@ -0,0 +1,876 @@
# Navigation and Main Layout
nav.jobs=Aufträge
nav.customers=Kunden
nav.appusers=App-Nutzer
nav.statistics=Statistiken
nav.invoices=Rechnungen
nav.messages=Nachrichten
nav.profile=Mein Profil
nav.myinvoices=Meine Rechnungen
nav.imprint=Impressum
nav.management=Verwaltung
nav.users=Benutzer
nav.showprofile=Profil anzeigen
nav.settings=Einstellungen
nav.logout=Abmelden
# Profile View
profile.title=Profil bearbeiten
profile.language=Sprache
profile.company=Firma
profile.companyadd=Firmenzusatz
profile.firstname=Vorname
profile.lastname=Nachname
profile.phone=Telefonnummer
profile.fax=Telefon (Fax)
profile.mobile=Telefon (Mobil)
profile.email=E-Mail-Adresse (Login)*
profile.street=Straße
profile.housenr=Hausnr
profile.addressadd=Adresszusatz
profile.zip=Postleitzahl
profile.city=Stadt
profile.diffinvoice=Abweichende Rechnungsadresse
profile.basicdata=Stammdaten
profile.map=Karte
profile.invoicecreation=Rechnungserstellung
profile.settings=Einstellungen
profile.account=Konto
profile.security=Sicherheit
profile.services=Leistungskatalog
profile.saved=Profil gespeichert
profile.save.error=Fehler beim Speichern: {0}
profile.validation.required.fill=Bitte füllen Sie alle Pflichtfelder korrekt aus
# Profile Settings
settings.digitalprocessing=Digitale Abwicklung per App
settings.digitalprocessinginfo=Aktiviert die digitale Auftragsabwicklung über die mobile App
settings.locationtracking=App-Nutzer orten
settings.locationtrackinginfo=Ermöglicht die Ortung von App-Nutzern während der Auftragsausführung
settings.twofactor=2-Faktor-Authentifizierung
settings.twofactorinfo=Bei Aktivierung wird bei jeder Anmeldung ein Code per E-Mail gesendet
# Profile Billing
profile.billing.enabled=Rechnungslegung über votianLT
# Profile Validation
profile.validation.company=Firma ist ein Pflichtfeld
profile.validation.firstname=Vorname ist ein Pflichtfeld
profile.validation.lastname=Nachname ist ein Pflichtfeld
profile.validation.phone=Telefonnummer ist ein Pflichtfeld
profile.validation.street=Straße ist ein Pflichtfeld
profile.validation.housenr=Hausnummer ist ein Pflichtfeld
profile.validation.zip=Postleitzahl ist ein Pflichtfeld
profile.validation.city=Stadt ist ein Pflichtfeld
profile.validation.email.required=E-Mail-Adresse ist ein Pflichtfeld
profile.validation.email.invalid=Bitte geben Sie eine gültige E-Mail-Adresse ein
profile.validation.company.required=Firma ist erforderlich
profile.validation.street.required=Straße ist erforderlich
profile.validation.housenr.required=Hausnummer ist erforderlich
profile.validation.zip.required=Postleitzahl ist erforderlich
profile.validation.city.required=Stadt ist erforderlich
profile.validation.firstname.required=Vorname ist erforderlich
profile.validation.lastname.required=Nachname ist erforderlich
profile.validation.phone.required=Telefonnummer ist erforderlich
# Profile Invoice
profile.invoice.masterdata=Meine Stammdaten
profile.invoice.name=Name
profile.invoice.city=Ort
profile.invoice.email=E-Mail
profile.invoice.phone=Telefon
profile.invoice.placeholder.company=Ihre Firma
profile.invoice.placeholder.name=Ihr Name
profile.invoice.placeholder.street=Ihre Straße
profile.invoice.placeholder.city=PLZ Ort
profile.invoice.placeholder.email=ihre@email.de
profile.invoice.placeholder.phone=Ihre Telefonnummer
profile.invoice.services.list=Leistungen auflisten
profile.invoice.net=Nettosumme
profile.invoice.vat=Umsatzsteuer
profile.invoice.gross=Bruttosumme
profile.invoice.customerdata=Kundendaten
profile.invoice.customer.company=Kunde Firma
profile.invoice.customer.name=Kunde Name
profile.invoice.customer.street=Kunde Straße
profile.invoice.customer.city=Kunde Ort
profile.invoice.customer.email=Kunde E-Mail
profile.invoice.customer.phone=Kunde Telefon
profile.invoice.free.elements=Freie Elemente
profile.invoice.element.text=Textfeld
profile.invoice.element.header=Überschrift
profile.invoice.element.date=Datum
profile.invoice.element.customer=Kundeninfo
profile.invoice.element.company=Firmeninfo
profile.invoice.element.amount=Betrag
profile.invoice.element.line=Linie
profile.invoice.element.image=Bild
profile.invoice.properties=Eigenschaften
profile.invoice.properties.info=Klicken Sie auf ein Element im Canvas, um dessen Eigenschaften zu bearbeiten.
profile.invoice.type=Typ
profile.invoice.variable=Variable
profile.invoice.xposition=X Position
profile.invoice.yposition=Y Position
profile.invoice.fontsize=Schriftgröße
profile.invoice.color=Farbe
profile.invoice.element.delete=Element löschen
profile.invoice.image=Bild hochladen
profile.invoice.image.drop=Bild hierher ziehen oder klicken
profile.invoice.image.uploaded=Bild erfolgreich hochgeladen
profile.invoice.image.upload.error=Fehler beim Hochladen: {0}
profile.invoice.file.rejected=Datei abgelehnt: {0}
profile.invoice.text.from.masterdata=Text kommt aus Ihren Stammdaten
profile.invoice.canvas.cleared=Canvas wurde geleert
profile.invoice.canvas.read.error=Fehler: Canvas-Daten konnten nicht gelesen werden
profile.invoice.template.saved=Template erfolgreich gespeichert
profile.invoice.pdf.error=Fehler bei PDF-Generierung: {0}
profile.invoice.pdf.preview=Vorschau
profile.invoice.pdf.preview.error=Fehler beim Generieren der Vorschau: {0}
# Profile Services
profile.services.label=Leistungen
profile.services.description=Verwalten Sie hier Ihre Leistungen, die Sie Ihren Kunden anbieten.
profile.services.add=Neue Leistung hinzufügen
profile.services.load.error=Fehler beim Laden der Leistungen: {0}
profile.services.saved=Leistung erfolgreich gespeichert
profile.services.save.error=Fehler beim Speichern der Leistung: {0}
profile.services.deleted=Leistung erfolgreich gelöscht
profile.services.delete.error=Fehler beim Löschen der Leistung: {0}
profile.services.dialog.create=Neue Leistung erstellen
profile.services.dialog.edit=Leistung bearbeiten
profile.services.basis=Berechnungsgrundlage
profile.services.basis.distance=Gefahrene Kilometer
profile.services.basis.time=Zeit
profile.services.basis.flatrate=Pauschal
profile.services.vatrate=Mehrwertsteuersatz (%)
profile.services.vatrate.percent=Mehrwertsteuersatz (%)
profile.services.price.flatrate=Pauschalpreis (€)
profile.services.price.distance=Preis pro Kilometer (€)
profile.services.price.time=Preis pro 15 Minuten (€)
profile.services.mandatory=Verpflichtend
profile.services.calculated=Wird berechnet
profile.services.validation.name=Name ist erforderlich
profile.services.validation.basis=Berechnungsgrundlage ist erforderlich
profile.services.validation.flatrate=Pauschalpreis ist erforderlich
profile.services.validation.distance=Preis pro Kilometer ist erforderlich
profile.services.validation.time=Preis pro 15 Minuten ist erforderlich
profile.services.validation.vatrate=Mehrwertsteuersatz ist erforderlich
profile.services.savechanges=Leistung speichern
# Buttons
button.save=Profiländerungen speichern
button.savechanges=Speichern
button.clear=Leeren
button.preview=Vorschau
button.savetemplate=Template speichern
button.changepassword=Passwort ändern
button.deleteaccount=Benutzerkonto löschen
button.add=Neu
button.edit=Bearbeiten
button.delete=Löschen
button.cancel=Abbrechen
button.close=Schließen
button.download=Herunterladen
button.back=Zurück
# Common
common.name=Name
common.yes=Ja
common.no=Nein
common.total=Gesamt
common.price=Preis
common.service=Leistung
common.customer=Kunde
common.actions=Aktionen
common.loading=Laden...
common.error=Fehler
common.success=Erfolg
common.required=Pflichtfeld
# Validation
validation.required=Feld ist erforderlich
validation.email=Ungültige E-Mail-Adresse
validation.error=Fehler bei der Validierung
# Notifications
notification.saved=Profil gespeichert
notification.error=Fehler beim Speichern
notification.languagechanged=Sprache geändert
# Login
login.title=Anmelden
login.username=Benutzername
login.password=Passwort
login.login=Anmelden
login.forgotpassword=Passwort vergessen?
login.rememberme=Angemeldet bleiben
login.register=Registrieren
login.2fa.helper=6-stelliger Code
login.2fa.sent=Code wurde per E-Mail gesendet
login.2fa.no.credentials=Keine Anmeldedaten vorhanden
login.2fa.invalid.code=Ungültiger Code
login.2fa.wrong.code=Falscher Code
# Error Messages
error.loading=Fehler beim Laden
error.saving=Fehler beim Speichern
error.validation=Validierungsfehler
# Page Titles
page.title.dashboard=VotianLT - Dashboard
page.title.appuser.create=Neuen App-Nutzer anlegen
page.title.messages=Nachrichten
page.title.register=Bei VotianLT registrieren
page.title.customers=Kunden
page.title.customer.edit=Kunde bearbeiten
page.title.verwaltung=Verwaltung
page.title.company.create=Neue Firma anlegen
page.title.imprint=Impressum
page.title.profile.edit=Profil bearbeiten
page.title.admin.dashboard=Admin Dashboard
page.title.invoice.create=Rechnung erstellen
page.title.customer.create=Neuen Kunden anlegen
page.title.login=Bei VotianLT anmelden
page.title.jobs=Aufträge
page.title.appuser.edit=App-Nutzer bearbeiten
page.title.statistics=KI-Statistiken
page.title.password.forget=Passwort zurücksetzen
page.title.invoices=Rechnungen
page.title.appusers=App-Nutzer
page.title.job.history=Job Historie
page.title.message.history=Nachrichtenverlauf
page.title.myinvoices=Meine Rechnungen
page.title.job.create=Neuen Auftrag anlegen
page.title.job.summary=Zusammenfassung
page.title.pricetable=Preis-Tabelle
page.title.invoice.generator=Rechnungsgenerator
page.title.welcome=VotianLT - Willkommen
page.title.password.reset=Passwort zurücksetzen E-Mail angeben
page.title.add.appuser=Neuen App-Nutzer anlegen
page.title.user.messages=Nachrichten
page.title.edit.customer=Kunde bearbeiten
page.title.show.customers=Kunden
page.title.add.company=Neue Firma anlegen
page.title.create.invoice=Rechnung erstellen
page.title.add.customer=Neuen Kunden anlegen
page.title.edit.appuser=App-Nutzer bearbeiten
page.title.forget.password=Passwort zurücksetzen
page.title.job.history=Job Historie
page.title.admin.pricetable=Preis-Tabelle
page.title.invoice.generator=Rechnungsgenerator
page.title.job.summary=Zusammenfassung
page.title.add.job=Neuen Auftrag anlegen
# Dashboard
dashboard.welcome=Willkommen, {0}!
dashboard.footer.copyright=© 2024 VotianLT. Alle Rechte vorbehalten.
dashboard.description=Hier können Sie Ihre Aufträge verwalten, Kunden organisieren und alle wichtigen Funktionen von VotianLT nutzen.
dashboard.system.title=Systemübersicht
dashboard.system.intro=Verwalten Sie Ihre Geschäftsprozesse effizient mit den folgenden Funktionen
dashboard.feature.setup.title=Einrichtung
dashboard.feature.setup.desc=Konfigurieren Sie Ihre Systemeinstellungen und Stammdaten
dashboard.feature.customers.title=Kunden
dashboard.feature.customers.desc=Verwalten Sie Ihre Kundenbeziehungen und Kontakte
dashboard.feature.jobs.title=Aufträge
dashboard.feature.jobs.desc=Erstellen und verwalten Sie Aufträge effizient
dashboard.app.title=Mobile App
dashboard.app.description=Nutzen Sie die VotianLT App für unterwegs und bleiben Sie immer verbunden
# Add App User
addappuser.title=Neuen App-Nutzer anlegen
addappuser.designation=Bezeichnung
addappuser.phone=Telefon (Mobil)
addappuser.password=Passwort
addappuser.password.confirm=Passwort bestätigen
addappuser.button.submit=App-Nutzer anlegen
addappuser.validation.designation=Bezeichnung ist erforderlich
addappuser.validation.phone=Telefonnummer ist erforderlich
addappuser.validation.password.required=Passwort ist erforderlich
addappuser.validation.password.min=Passwort muss mindestens 6 Zeichen haben
addappuser.validation.password.confirm=Passwortbestätigung ist erforderlich
addappuser.validation.password.mismatch=Passwörter stimmen nicht überein
addappuser.validation.email.required=E-Mail ist erforderlich
addappuser.validation.email.invalid=Ungültige E-Mail-Adresse
addappuser.notification.validation=Bitte füllen Sie alle Pflichtfelder aus
addappuser.notification.success=App-Nutzer erfolgreich angelegt
addappuser.notification.check=Bitte überprüfen Sie Ihre Eingaben
addappuser.notification.email.duplicate=Diese E-Mail-Adresse wird bereits verwendet
addappuser.notification.error=Fehler: {0}
addappuser.placeholder.designation=(HH H 000)
# Edit App User
editappuser.title=App-Nutzer bearbeiten
editappuser.password.change=Neues Passwort
editappuser.password.change.confirm=Neues Passwort bestätigen
editappuser.password.placeholder=Leer lassen, um Passwort nicht zu ändern
editappuser.notification.invalid.id=Ungültige App-Nutzer-ID
editappuser.notification.password.mismatch=Passwörter stimmen nicht überein
editappuser.notification.saved=App-Nutzer erfolgreich gespeichert
editappuser.notification.check=Bitte überprüfen Sie Ihre Eingaben
editappuser.notification.password.confirm=Bitte bestätigen Sie das neue Passwort
editappuser.notification.password.enter=Bitte geben Sie ein neues Passwort ein
editappuser.notification.deleted=App-Nutzer erfolgreich gelöscht
editappuser.dialog.delete.text=Möchten Sie diesen App-Nutzer wirklich löschen?
editappuser.dialog.delete.confirm=Löschen
# Customers
customers.title=Kunden
customers.button.add=Neuen Kunden hinzufügen
customers.hint.click=Klicken Sie auf einen Kunden, um Details zu sehen
customers.column.company=Firma
customers.column.name=Name
customers.column.email=E-Mail
customers.column.phone=Telefon
customers.column.street=Straße
customers.column.city=Ort
# Edit Customer
editcustomer.title=Kunde bearbeiten
editcustomer.notification.notfound=Kunde nicht gefunden
editcustomer.notification.invalid.id=Ungültige Kunden-ID
editcustomer.notification.saved=Kunde erfolgreich gespeichert
editcustomer.notification.check=Bitte überprüfen Sie Ihre Eingaben
editcustomer.notification.deleted=Kunde erfolgreich gelöscht
editcustomer.dialog.delete.text=Möchten Sie diesen Kunden wirklich löschen?
editcustomer.dialog.delete.confirm=Löschen
# Add Customer
addcustomer.title=Neuen Kunden anlegen
addcustomer.button.submit=Kunden anlegen
addcustomer.notification.validation=Bitte füllen Sie alle Pflichtfelder aus
addcustomer.notification.success=Kunde erfolgreich angelegt
addcustomer.notification.check=Bitte überprüfen Sie Ihre Eingaben
addcustomer.notification.error=Fehler: {0}
addcustomer.validation.required=Dieses Feld ist erforderlich
# Add Company
addcompany.title=Neue Firma anlegen
addcompany.button.submit=Firma anlegen
# Verwaltung
verwaltung.title=Verwaltung
verwaltung.description=Verwalten Sie hier Ihre Firmen, Kunden und Systemeinstellungen
# User Messages
usermessages.title.with=Nachrichten mit {0}
usermessages.general.title=Allgemeine Konversationen
usermessages.general.conversation=Allgemeine Konversation
usermessages.job.title=Auftragsbezogene Nachrichten
usermessages.job.conversation=Auftrag {0}
usermessages.no.job.messages=Keine auftragsbezogenen Nachrichten
usermessages.preview.empty=Keine Vorschau verfügbar
usermessages.message.count={0} Nachrichten
usermessages.unknown=Unbekannt
usermessages.unknown.participant=Unbekannter Teilnehmer
# Admin Dashboard
admindashboard.title=Admin Dashboard
admindashboard.loading=Statistiken werden geladen...
admindashboard.error=Fehler beim Laden: {0}
admindashboard.section.overview=Übersicht
admindashboard.section.jobs=Aufträge
admindashboard.section.tasks=Aufgaben
admindashboard.section.users=Benutzeraktivitäten
admindashboard.section.health=Systemstatus
admindashboard.stat.totaljobs=Gesamtaufträge
admindashboard.stat.users=Benutzer
admindashboard.stat.appusers=App-Nutzer
admindashboard.stat.lastupdated=Zuletzt aktualisiert
admindashboard.stat.openjobs=Offene Aufträge
admindashboard.stat.inprogress=In Bearbeitung
admindashboard.stat.completed=Abgeschlossen
admindashboard.stat.cargo=Frachtstücke
admindashboard.stat.status.info=Status
admindashboard.stat.status.unavailable=Nicht verfügbar
admindashboard.stat.totaltasks=Gesamtaufgaben
admindashboard.stat.completedtasks=Erledigt
admindashboard.stat.pendingtasks=Ausstehend
admindashboard.stat.successrate=Erfolgsrate
admindashboard.stat.photos=Fotos
admindashboard.stat.barcodes=Barcodes
admindashboard.stat.signatures=Unterschriften
admindashboard.stat.comments=Kommentare
admindashboard.stat.database=Datenbank
admindashboard.stat.database.connected=Verbunden
admindashboard.stat.database.error=Fehler
admindashboard.stat.websocket=WebSocket
admindashboard.stat.websocket.active=Aktiv
admindashboard.stat.app=Anwendung
admindashboard.stat.app.running=Läuft
admindashboard.stat.memory=Speicher
# Messages
messages.title=Nachrichten
messages.column.status=Status
messages.column.client=Kunde
messages.column.email=E-Mail
messages.column.total=Gesamt
messages.column.unread=Ungelesen
messages.column.lastmessage=Letzte Nachricht
messages.column.preview=Vorschau
messages.notification.error=Fehler beim Laden der Nachrichten
messages.preview.image=Bild
messages.preview.empty=Keine Vorschau
messages.sender.unknown=Unbekannter Absender
# Add Job
addjob.title=Neuen Auftrag anlegen
addjob.customer.label=Kunde
addjob.customer.placeholder=Kunde auswählen
addjob.customer.unnamed=Unbenannter Kunde
addjob.button.clearfields=Felder leeren
addjob.button.submit=Auftrag anlegen
addjob.address.salutation=Anrede
addjob.address.salutation.placeholder=Anrede wählen
addjob.salutation.mr=Herr
addjob.salutation.ms=Frau
addjob.salutation.other=Divers
addjob.address.company.placeholder=Firma eingeben
addjob.address.street.placeholder=Straße eingeben
addjob.address.housenumber=Hausnummer
addjob.address.addition.placeholder=Adresszusatz
addjob.address.city=Ort
addjob.address.city.placeholder.pickup=Ort (Abholung)
addjob.address.city.placeholder.delivery=Ort (Lieferung)
addjob.address.delivery.street.placeholder=Straße (Lieferung)
addjob.address.delivery.addition.placeholder=Adresszusatz (Lieferung)
addjob.address.save=Adresse speichern
addjob.section.pickup=Abholung
addjob.section.delivery=Lieferung
addjob.tab.addresses=Auftraggeber & Adressen
addjob.tab.appointments=Termine & Verarbeitung
addjob.tab.cargo=Fracht
addjob.tab.tasks=Aufgaben
addjob.tab.price=Preis & Abschluss
addjob.appointment.date=Datum
addjob.appointment.time=Uhrzeit
addjob.appointment.pickup=Abholtermin
addjob.appointment.delivery=Liefertermin
addjob.settings.digitalprocess=Digitale Abwicklung per App
addjob.appuser.label=App-Nutzer
addjob.appuser.placeholder=App-Nutzer auswählen
addjob.cargo.description=Beschreibung
addjob.cargo.description.placeholder=Beschreibung eingeben
addjob.cargo.quantity=Anzahl
addjob.cargo.weight=Gewicht
addjob.cargo.length=Länge
addjob.cargo.width=Breite
addjob.cargo.height=Höhe
addjob.cargo.europalette=Europalette
addjob.cargo.disposablepalette=Einwegpalette
addjob.cargo.dusseldorfpalette=Düsseldorfer Palette
addjob.cargo.gridboxpalette=Gitterboxpalette
addjob.cargo.gridcart=Gitterwagen
addjob.cargo.parcel=Paket
addjob.cargo.add=Fracht hinzufügen
addjob.tasks.title=Aufgaben
addjob.tasks.template.placeholder=Template auswählen
addjob.tasks.template.save.tooltip=Als Template speichern
addjob.tasks.template.save.title=Template speichern
addjob.tasks.template.name=Template-Name
addjob.tasks.template.name.placeholder=Name eingeben
addjob.tasks.template.name.required=Name ist erforderlich
addjob.tasks.template.saved=Template "{0}" gespeichert
addjob.tasks.template.save.error=Fehler beim Speichern: {0}
addjob.tasks.template.dialog.error=Fehler beim Öffnen des Dialogs: {0}
addjob.tasks.template.no.tasks=Keine Aufgaben zum Speichern
addjob.tasks.template.load.title=Template laden
addjob.tasks.template.load.text=Möchten Sie das Template "{0}" laden? Diese Aktion ersetzt alle aktuellen Aufgaben.
addjob.tasks.template.load.confirm=Laden
addjob.tasks.template.loaded=Template "{0}" geladen
addjob.tasks.template.load.error=Fehler beim Laden: {0}
addjob.tasks.template.load.templates.error=Fehler beim Laden der Templates: {0}
addjob.tasks.add=Aufgabe hinzufügen
addjob.tasks.tasktype=Aufgabentyp
addjob.tasks.tasktype.placeholder=Typ wählen
addjob.tasks.description=Beschreibung
addjob.tasks.description.placeholder=Beschreibung eingeben
addjob.tasks.buttontext=Button-Text
addjob.tasks.buttontext.placeholder=Text eingeben
addjob.tasks.remark=Bemerkung
addjob.tasks.remark.placeholder=Bemerkung eingeben
addjob.tasks.photo.min=Min. Fotos
addjob.tasks.photo.max=Max. Fotos
addjob.tasks.barcode.min=Min. Barcodes
addjob.tasks.barcode.max=Max. Barcodes
addjob.tasks.signature.noconfig=Keine Konfiguration erforderlich
addjob.tasks.todolist.title=To-Do Liste
addjob.tasks.todolist.item.placeholder=To-Do eingeben
addjob.tasks.todolist.add=To-Do hinzufügen
addjob.tasks.comment.label=Kommentar
addjob.tasks.comment.placeholder=Kommentar eingeben
addjob.tasks.comment.required=Kommentar erforderlich
addjob.services.title=Leistungen
addjob.services.add=Leistung hinzufügen
addjob.services.calculation=Berechnung
addjob.services.basis.distance=Gefahrene Kilometer
addjob.services.basis.time=Zeit
addjob.services.basis.flatrate=Pauschal
addjob.services.vat=Mehrwertsteuer
addjob.services.route.missing=Route fehlt
addjob.services.dialog.title=Leistung auswählen
addjob.services.dialog.placeholder=Leistung wählen
addjob.services.dialog.add=Hinzufügen
addjob.summary.title=Zusammenfassung
addjob.summary.net=Netto
addjob.summary.vat=Mehrwertsteuer
addjob.summary.gross=Brutto
addjob.route.title=Route
addjob.route.distance=Entfernung
addjob.route.distance.km=Entfernung (km)
addjob.route.distance.placeholder=z.B. 150.5
addjob.route.duration=Dauer
addjob.route.duration.min=Dauer (Min.)
addjob.route.duration.placeholder=z.B. 120
addjob.route.manual.title=Manuelle Streckeneingabe
addjob.route.manual.hint=Geben Sie die Entfernung und Dauer manuell ein, wenn keine Route berechnet wurde
addjob.notification.success=Auftrag {0} erfolgreich angelegt
addjob.notification.cleared=Alle Felder wurden geleert
addjob.notification.draft.restored=Entwurf wiederhergestellt
addjob.validation.required.fields=Bitte füllen Sie alle Pflichtfelder aus
addjob.validation.appuser.required=Bitte wählen Sie einen App-Nutzer aus
addjob.validation.cargo.required=Bitte geben Sie mindestens eine Fracht an
addjob.validation.pickupdate.future=Abholdatum muss heute oder in der Zukunft liegen
addjob.validation.deliverydate.future=Lieferdatum muss heute oder in der Zukunft liegen
addjob.validation.dialog.title=Adressvalidierung
addjob.validation.dialog.loading=Adressen werden validiert...
addjob.validation.dialog.back=Zurück
addjob.validation.dialog.continue=Weiter
addjob.validation.dialog.continue.anyway=Trotzdem weiter
addjob.validation.pickup.address=Abholadresse
addjob.validation.delivery.address=Lieferadresse
addjob.validation.route=Route
# Job Summary
jobsummary.title=Zusammenfassung
jobsummary.error.noid=Keine Job-ID angegeben
jobsummary.error.invalidid=Ungültige Job-ID Format: {0}
jobsummary.error.notfound=Job mit ID {0} nicht gefunden
jobsummary.button.sendmessage=Nachricht senden
jobsummary.button.jobhistory=Job Historie
jobsummary.button.complete=Auftrag manuell abschließen
jobsummary.dialog.complete.title=Auftrag abschließen
jobsummary.dialog.complete.text=Möchten Sie den Auftrag {0} manuell abschließen?
jobsummary.dialog.complete.cancel=Abbrechen
jobsummary.dialog.complete.confirm=Abschließen
jobsummary.notification.completed=Auftrag {0} wurde abgeschlossen
jobsummary.notification.complete.error=Fehler beim Abschließen: {0}
jobsummary.notification.noappuser=Diesem Auftrag ist kein App-Nutzer zugeordnet
jobsummary.section.pickup=Abholung
jobsummary.section.delivery=Lieferung
jobsummary.section.tasks=Zu quittierende Aufgaben
jobsummary.section.cargo=Zu transportierende Fracht
jobsummary.section.info=Weitere Informationen
jobsummary.tasks.none=Keine Aufgaben
jobsummary.cargo.none=Keine Frachtangaben
jobsummary.info.netto=Netto
jobsummary.info.ust=USt
jobsummary.info.gesamt=Gesamt
jobsummary.info.bemerkung=Bemerkung
jobsummary.info.digital=Digitale Abwicklung per App: aktiviert
jobsummary.info.appuser=App-Nutzer
jobsummary.task.status.abgeschlossen=Abgeschlossen
jobsummary.task.status.offen=Offen
jobsummary.task.typ=Typ
jobsummary.task.completedAt=Abgeschlossen am
jobsummary.task.completedBy=Abgeschlossen von
jobsummary.task.todo.items=To-Do Items
jobsummary.task.photo.info=Fotos
jobsummary.task.photo.minmax=Mindestens {0} Fotos erforderlich
jobsummary.task.photo.maxonly=Maximal {0} Fotos erlaubt
jobsummary.task.photo.taken=Aufgenommene Fotos ({0})
jobsummary.task.button.text=Button-Text
jobsummary.button.schliessen=Schließen
# Jobs
jobs.title=Aufträge
jobs.filter.search=Suchen
jobs.filter.search.placeholder=Suche nach Auftragsnummer...
jobs.filter.startdate=Startdatum
jobs.filter.enddate=Enddatum
jobs.filter.status=Status
jobs.filter.apply=Filter anwenden
jobs.status.all=Alle
jobs.status.open=Offen
jobs.status.done=Erledigt
jobs.notification.completed=Auftrag {0} wurde abgeschlossen
jobs.column.status=Status
jobs.column.customer=Kunde
jobs.column.jobnumber=Auftragsnummer
jobs.column.jobdate=Auftragsdatum
jobs.column.destination=Zielort
jobs.historie.manuell=Manuell
jobs.button.csvexport=CSV Export
jobs.tooltip.complete=Auftrag abschließen
jobs.tooltip.createinvoice=Rechnung erstellen
jobs.tooltip.delete=Auftrag löschen
jobs.dialog.complete.title=Auftrag abschließen
jobs.dialog.complete.text=Möchten Sie den Auftrag {0} manuell abschließen?
jobs.dialog.complete.confirm=Abschließen
jobs.dialog.delete.title=Auftrag löschen
jobs.dialog.delete.text=Möchten Sie den Auftrag {0} wirklich löschen?
jobs.notification.completed=Auftrag {0} wurde abgeschlossen
jobs.notification.complete.error=Fehler beim Abschließen: {0}
jobs.notification.deleted=Auftrag {0} wurde gelöscht
jobs.notification.delete.error=Fehler beim Löschen: {0}
# Create Invoice
createinvoice.error.invalidid=Ungültige Job-ID
createinvoice.error.notfound=Job nicht gefunden
createinvoice.button.create=Rechnung erstellen
createinvoice.section.job=Auftragsdetails
createinvoice.section.route=Streckeninfo
createinvoice.section.services=Leistungen
createinvoice.section.summary=Zusammenfassung
createinvoice.field.jobnumber=Auftragsnummer
createinvoice.field.customer=Kunde
createinvoice.field.status=Status
createinvoice.field.price=Preis
createinvoice.route.distance=Entfernung
createinvoice.route.duration=Fahrtzeit
createinvoice.column.service=Leistung
createinvoice.column.basis=Berechnungsbasis
createinvoice.summary.net=Nettosumme
createinvoice.summary.total=Gesamtsumme
createinvoice.notification.noservices=Bitte wählen Sie mindestens eine Leistung aus
createinvoice.notification.nouser=Benutzer nicht gefunden
createinvoice.notification.notemplate=Kein Rechnungstemplate gefunden
createinvoice.notification.error=Fehler beim Erstellen der Rechnung: {0}
# Invoices
invoices.title=Rechnungen
invoices.column.number=Nummer
invoices.column.customer=Kunde
invoices.column.date=Datum
invoices.column.amount=Betrag
invoices.column.description=Beschreibung
# My Invoices
myinvoices.title=Meine Rechnungen
myinvoices.hint.noopen=Sie haben keine offenen Rechnungen. Alle Rechnungen sind beglichen.
myinvoices.bank.institute=Bank
myinvoices.bank.beneficiary=Empfänger
myinvoices.bank.iban=IBAN
myinvoices.recipient.name=Kunde
myinvoices.recipient.department=
myinvoices.item.description=Position: {0}
# App User
appuser.title=App-Nutzer
appuser.button.add=App-Nutzer hinzufügen
appuser.column.designation=Bezeichnung
appuser.column.firstname=Vorname
appuser.column.lastname=Nachname
appuser.column.phone=Telefon
appuser.column.appcode=App-Code
appuser.column.email=E-Mail
# Statistics
statistics.title=KI-Statistiken
statistics.subtitle=Stellen Sie Fragen zu Ihren Aufträgen und Kunden
statistics.prompt.placeholder=Frage eingeben...
statistics.quick.jobcount=Anzahl Aufträge
statistics.quick.jobcount.prompt=Wie viele Aufträge habe ich aktuell?
statistics.quick.revenue=Umsatz
statistics.quick.revenue.prompt=Wie hoch ist mein Umsatz diesen Monat?
statistics.quick.trend=Trends
statistics.quick.trend.prompt=Zeige mir Trends in den letzten 3 Monaten
statistics.ai.label=KI-Antwort
statistics.data.fetched=Daten wurden abgerufen
statistics.loading=Berechne...
# Job Status
jobstatus.IN_PROGRESS=In Bearbeitung
jobstatus.COMPLETED=Abgeschlossen
# Task Types
tasktype.CONFIRMATION=Bestätigung
tasktype.SIGNATURE=Unterschrift
tasktype.TODOLIST=To-Do Liste
tasktype.PHOTO=Foto
tasktype.BARCODE=Barcode
tasktype.COMMENT=Kommentar
# Password Reset
passwordreset.title=Passwort zurücksetzen
passwordreset.newpassword=Neues Passwort
passwordreset.confirmpassword=Passwort bestätigen
passwordreset.button.submit=Passwort speichern
passwordreset.button.cancel=Abbrechen
passwordreset.button.send=E-Mail senden
passwordreset.notification.enterpassword=Bitte geben Sie ein neues Passwort ein
passwordreset.notification.mismatch=Die Passwörter stimmen nicht überein
passwordreset.notification.success=Passwort wurde erfolgreich geändert
passwordreset.notification.invalidtoken=Token ungültig oder abgelaufen
passwordreset.notification.entermail=Bitte E-Mail eingeben
passwordreset.notification.sent=Falls die E-Mail existiert, wurde ein Link versendet
passwordreset.notification.wait=Bitte warten Sie {0} Sekunden, bevor Sie den Code erneut senden
# Email
email.2fa.subject=Ihr VotianLT Bestätigungscode
email.2fa.body=Ihr Bestätigungscode lautet: {0}\n\nDieser Code ist 10 Minuten gültig.\nWenn Sie diese Registrierung nicht angefragt haben, ignorieren Sie diese E-Mail.
# Register
register.title=Registrierung
register.subtitle=Erstellen Sie Ihr VotianLT-Konto
register.email=E-Mail-Adresse
register.password=Passwort
register.password.placeholder=Mindestens 6 Zeichen
register.password.confirm=Passwort bestätigen
register.password.confirm.placeholder=Passwort wiederholen
register.firstname=Vorname
register.lastname=Nachname
register.phone=Telefonnummer
register.company=Firma
register.street=Straße
register.housenr=Hausnr
register.zip=Postleitzahl
register.city=Stadt
register.code.label=Bestätigungscode (6 Ziffern)
register.code.placeholder=z. B. 123456
register.button.submit=Registrieren
register.button.verify=Code prüfen und registrieren
register.button.resend=Code erneut senden
register.button.back=Zurück zur Startseite
register.notification.email.required=Bitte geben Sie eine E-Mail-Adresse ein
register.notification.email.invalid=Bitte geben Sie eine gültige E-Mail-Adresse ein
register.notification.email.duplicate=Ein Benutzer mit dieser E-Mail-Adresse existiert bereits
register.notification.password.required=Bitte geben Sie ein Passwort ein
register.notification.password.min=Das Passwort muss mindestens 6 Zeichen lang sein
register.notification.password.mismatch=Die Passwörter stimmen nicht überein
register.notification.firstname.required=Bitte geben Sie Ihren Vornamen ein
register.notification.lastname.required=Bitte geben Sie Ihren Nachnamen ein
register.notification.phone.required=Bitte geben Sie Ihre Telefonnummer ein
register.notification.company.required=Bitte geben Sie den Firmennamen ein
register.notification.street.required=Bitte geben Sie die Straße ein
register.notification.housenr.required=Bitte geben Sie die Hausnummer ein
register.notification.zip.required=Bitte geben Sie die Postleitzahl ein
register.notification.city.required=Bitte geben Sie die Stadt ein
register.notification.code.sent=Ein Bestätigungscode wurde an {0} gesendet
register.notification.code.emailerror=Fehler beim Senden der E-Mail: {0}
register.notification.code.expired=Der Code ist abgelaufen. Bitte senden Sie einen neuen Code.
register.notification.code.invalid=Der eingegebene Code ist ungültig
register.notification.code.startfirst=Bitte starten Sie zuerst die Registrierung
register.notification.code.required=Bitte geben Sie den 6-stelligen Code ein
register.notification.success=Registrierung erfolgreich. Bitte melden Sie sich an.
register.notification.failed=Registrierung fehlgeschlagen: {0}
# Start Page
start.title=VotianLT - Ihr digitaler Transportpartner
start.button.login=Anmelden
start.button.register=Registrieren
start.button.createorder=Auftragserstellung
start.button.notifications=Benachrichtigungen
start.button.nonotifications=Keine neuen Benachrichtigungen
start.system.title=Das System
start.feature.setup.title=Einrichtungsassistent
start.feature.setup.desc=Mithilfe des Einrichtungsassistenten haben Sie die Möglichkeit, Ihr Nutzerprofil zu vervollständigen.
start.feature.customers.title=Kunden- und Auftragsverwaltung
start.feature.customers.desc=Mit der Kunden- und Auftragsverwaltung haben Sie alle Kontaktdaten und Auftragsdetails stets im Blick.
start.feature.jobs.title=Auftragserstellung
start.feature.jobs.desc=Stellen Sie mit wenigen Mausklicks Aufträge ins System ein und legen Sie fest, welcher Mitarbeiter welchen Transportauftrag abarbeiten soll.
start.app.title=Die App
start.app.description=Jeder Auftrag kann optional über die votianLT-App abgearbeitet werden ganz ohne "Zettelwirtschaft". So gelangen alle relevanten Auftragsinformationen direkt auf das Smartphone des Fahrers.
start.imprint.title=Impressum
start.imprint.company=Assecutor Data Service GmbH
start.imprint.address=Ottensener Str. 8, 22525 Hamburg
start.imprint.phone=Telefon: +49 40 18 123 771 0
start.imprint.email=E-Mail: ahoi@assecutor.de
start.slogan=Betreiben Sie Ihr Geschäft smart … mit votianLT!
start.version=Version
# Login View
login.2fa.title=2FA Code
login.2fa.button=Code prüfen
login.votianlt=VotianLT
login.version=Version
# Message Details
messagedetails.button.send=Senden
messagedetails.placeholder=Nachricht eingeben...
messagedetails.noimage=(kein Bildinhalt)
messagedetails.imageerror=(Bild konnte nicht geladen werden)
# Invoice Generator
invoicegenerator.properties.title=Eigenschaften
invoicegenerator.properties.type=Typ
invoicegenerator.fontsize.label=Schriftgröße
invoicegenerator.color.label=Schriftfarbe
invoicegenerator.color.dialog.title=Schriftfarbe wählen
invoicegenerator.color.dialog.hex=Hex-Farbwert
invoicegenerator.button.cancel=Abbrechen
invoicegenerator.button.apply=Übernehmen
invoicegenerator.button.delete=Element löschen
invoicegenerator.notification.color.applied=Farbe übernommen
invoicegenerator.upload.drop=Bild hierher ziehen oder klicken
invoicegenerator.upload.success=Bild erfolgreich hochgeladen
invoicegenerator.upload.error=Fehler beim Hochladen: {0}
invoicegenerator.file.rejected=Datei abgelehnt: {0}
invoicegenerator.properties.select.info=Klicken Sie auf ein Element im Canvas, um dessen Eigenschaften zu bearbeiten.
# CSV Export
csv.header.customer=Auftraggeber
csv.header.jobnumber=Auftragsnummer
csv.header.jobdate=Auftragsdatum
csv.header.destination=Zielort
csv.filename=jobs.csv
# DatePicker I18n
datepicker.month.januar=Januar
datepicker.month.februar=Februar
datepicker.month.märz=März
datepicker.month.april=April
datepicker.month.mai=Mai
datepicker.month.juni=Juni
datepicker.month.juli=Juli
datepicker.month.august=August
datepicker.month.september=September
datepicker.month.oktober=Oktober
datepicker.month.november=November
datepicker.month.dezember=Dezember
datepicker.weekday.sonntag=Sonntag
datepicker.weekday.montag=Montag
datepicker.weekday.dienstag=Dienstag
datepicker.weekday.mittwoch=Mittwoch
datepicker.weekday.donnerstag=Donnerstag
datepicker.weekday.freitag=Freitag
datepicker.weekday.samstag=Samstag
datepicker.weekdayshort.so=So
datepicker.weekdayshort.mo=Mo
datepicker.weekdayshort.di=Di
datepicker.weekdayshort.mi=Mi
datepicker.weekdayshort.do=Do
datepicker.weekdayshort.fr=Fr
datepicker.weekdayshort.sa=Sa
# Job History
jobhistory.status.pickupscheduled=Abholung geplant
jobhistory.status.pickedup=Abgeholt
jobhistory.status.intransit=Unterwegs
jobhistory.status.delivered=Zugestellt
jobhistory.image.alt=Vergrößertes Foto
# Version
version.label=Version
# Management Combo
management.placeholder=Verwaltung
management.customers=Kunden
management.jobs=Aufträge
management.companies=Firmen
# User Menu
usermenu.profile=Profil anzeigen
usermenu.settings=Einstellungen
usermenu.logout=Abmelden
# CTA Button
cta.freetest=Jetzt kostenlos testen
# Miscellaneous
misc.toggle.hide=Ausblenden
misc.toggle.show=Einblenden
misc.nodata=Keine Daten vorhanden
misc.loading=Daten werden geladen...
misc.error=Fehler aufgetreten
misc.retry=Erneut versuchen

View File

@@ -0,0 +1,876 @@
# Navigation and Main Layout
nav.jobs=Jobs
nav.customers=Customers
nav.appusers=App Users
nav.statistics=Statistics
nav.invoices=Invoices
nav.messages=Messages
nav.profile=My Profile
nav.myinvoices=My Invoices
nav.imprint=Imprint
nav.management=Management
nav.users=Users
nav.showprofile=Show Profile
nav.settings=Settings
nav.logout=Logout
# Profile View
profile.title=Edit Profile
profile.language=Language
profile.company=Company
profile.companyadd=Company Addition
profile.firstname=First Name
profile.lastname=Last Name
profile.phone=Phone Number
profile.fax=Fax
profile.mobile=Mobile
profile.email=Email Address (Login)*
profile.street=Street
profile.housenr=House No.
profile.addressadd=Address Addition
profile.zip=Zip Code
profile.city=City
profile.diffinvoice=Different Invoice Address
profile.basicdata=Basic Data
profile.map=Map
profile.invoicecreation=Invoice Creation
profile.settings=Settings
profile.account=Account
profile.security=Security
profile.services=Service Catalog
profile.saved=Profile saved
profile.save.error=Error saving: {0}
profile.validation.required.fill=Please fill in all required fields correctly
# Profile Settings
settings.digitalprocessing=Digital Processing via App
settings.digitalprocessinginfo=Enables digital order processing through the mobile app
settings.locationtracking=Track App Users
settings.locationtrackinginfo=Allows tracking of app users during order execution
settings.twofactor=Two-Factor Authentication
settings.twofactorinfo=When enabled, a code will be sent via email for each login
# Profile Billing
profile.billing.enabled=Billing via votianLT
# Profile Validation
profile.validation.company=Company is a required field
profile.validation.firstname=First name is a required field
profile.validation.lastname=Last name is a required field
profile.validation.phone=Phone number is a required field
profile.validation.street=Street is a required field
profile.validation.housenr=House number is a required field
profile.validation.zip=Zip code is a required field
profile.validation.city=City is a required field
profile.validation.email.required=Email address is a required field
profile.validation.email.invalid=Please enter a valid email address
profile.validation.company.required=Company is required
profile.validation.street.required=Street is required
profile.validation.housenr.required=House number is required
profile.validation.zip.required=Zip code is required
profile.validation.city.required=City is required
profile.validation.firstname.required=First name is required
profile.validation.lastname.required=Last name is required
profile.validation.phone.required=Phone number is required
# Profile Invoice
profile.invoice.masterdata=My Data
profile.invoice.name=Name
profile.invoice.city=City
profile.invoice.email=Email
profile.invoice.phone=Phone
profile.invoice.placeholder.company=Your Company
profile.invoice.placeholder.name=Your Name
profile.invoice.placeholder.street=Your Street
profile.invoice.placeholder.city=ZIP City
profile.invoice.placeholder.email=your@email.com
profile.invoice.placeholder.phone=Your Phone Number
profile.invoice.services.list=List Services
profile.invoice.net=Net Total
profile.invoice.vat=VAT
profile.invoice.gross=Gross Total
profile.invoice.customerdata=Customer Data
profile.invoice.customer.company=Customer Company
profile.invoice.customer.name=Customer Name
profile.invoice.customer.street=Customer Street
profile.invoice.customer.city=Customer City
profile.invoice.customer.email=Customer Email
profile.invoice.customer.phone=Customer Phone
profile.invoice.free.elements=Free Elements
profile.invoice.element.text=Text Field
profile.invoice.element.header=Header
profile.invoice.element.date=Date
profile.invoice.element.customer=Customer Info
profile.invoice.element.company=Company Info
profile.invoice.element.amount=Amount
profile.invoice.element.line=Line
profile.invoice.element.image=Image
profile.invoice.properties=Properties
profile.invoice.properties.info=Click on an element in the canvas to edit its properties
profile.invoice.type=Type
profile.invoice.variable=Variable
profile.invoice.xposition=X Position
profile.invoice.yposition=Y Position
profile.invoice.fontsize=Font Size
profile.invoice.color=Color
profile.invoice.element.delete=Delete Element
profile.invoice.image=Upload Image
profile.invoice.image.drop=Drag image here or click
profile.invoice.image.uploaded=Image uploaded successfully
profile.invoice.image.upload.error=Error uploading: {0}
profile.invoice.file.rejected=File rejected: {0}
profile.invoice.text.from.masterdata=Text comes from your master data
profile.invoice.canvas.cleared=Canvas cleared
profile.invoice.canvas.read.error=Error: Could not read canvas data
profile.invoice.template.saved=Template saved successfully
profile.invoice.pdf.error=Error generating PDF: {0}
profile.invoice.pdf.preview=Preview
profile.invoice.pdf.preview.error=Error generating preview: {0}
# Profile Services
profile.services.label=Services
profile.services.description=Manage your services that you offer to your customers
profile.services.add=Add New Service
profile.services.load.error=Error loading services: {0}
profile.services.saved=Service saved successfully
profile.services.save.error=Error saving service: {0}
profile.services.deleted=Service deleted successfully
profile.services.delete.error=Error deleting service: {0}
profile.services.dialog.create=Create New Service
profile.services.dialog.edit=Edit Service
profile.services.basis=Calculation Basis
profile.services.basis.distance=Distance (km)
profile.services.basis.time=Time
profile.services.basis.flatrate=Flat Rate
profile.services.vatrate=VAT Rate (%)
profile.services.vatrate.percent=VAT Rate (%)
profile.services.price.flatrate=Flat Rate Price (€)
profile.services.price.distance=Price per Kilometer (€)
profile.services.price.time=Price per 15 Minutes (€)
profile.services.mandatory=Mandatory
profile.services.calculated=Calculated
profile.services.validation.name=Name is required
profile.services.validation.basis=Calculation basis is required
profile.services.validation.flatrate=Flat rate price is required
profile.services.validation.distance=Price per kilometer is required
profile.services.validation.time=Price per 15 minutes is required
profile.services.validation.vatrate=VAT rate is required
profile.services.savechanges=Save Service
# Buttons
button.save=Save Profile Changes
button.savechanges=Save
button.clear=Clear
button.preview=Preview
button.savetemplate=Save Template
button.changepassword=Change Password
button.deleteaccount=Delete Account
button.add=New
button.edit=Edit
button.delete=Delete
button.cancel=Cancel
button.close=Close
button.download=Download
button.back=Back
# Common
common.name=Name
common.yes=Yes
common.no=No
common.total=Total
common.price=Price
common.service=Service
common.customer=Customer
common.actions=Actions
common.loading=Loading...
common.error=Error
common.success=Success
common.required=Required
# Validation
validation.required=Field is required
validation.email=Invalid email address
validation.error=Validation error
# Notifications
notification.saved=Profile saved
notification.error=Error saving
notification.languagechanged=Language changed
# Login
login.title=Login
login.username=Username
login.password=Password
login.login=Login
login.forgotpassword=Forgot password?
login.rememberme=Remember me
login.register=Register
login.2fa.helper=6-digit code
login.2fa.sent=Code sent via email
login.2fa.no.credentials=No credentials available
login.2fa.invalid.code=Invalid code
login.2fa.wrong.code=Wrong code
# Error Messages
error.loading=Error loading
error.saving=Error saving
error.validation=Validation error
# Page Titles
page.title.dashboard=VotianLT - Dashboard
page.title.appuser.create=Create New App User
page.title.messages=Messages
page.title.register=Register with VotianLT
page.title.customers=Customers
page.title.customer.edit=Edit Customer
page.title.verwaltung=Management
page.title.company.create=Create New Company
page.title.imprint=Imprint
page.title.profile.edit=Edit Profile
page.title.admin.dashboard=Admin Dashboard
page.title.invoice.create=Create Invoice
page.title.customer.create=Create New Customer
page.title.login=Login to VotianLT
page.title.jobs=Jobs
page.title.appuser.edit=Edit App User
page.title.statistics=AI Statistics
page.title.password.forget=Reset Password
page.title.invoices=Invoices
page.title.appusers=App Users
page.title.job.history=Job History
page.title.message.history=Message History
page.title.myinvoices=My Invoices
page.title.job.create=Create New Job
page.title.job.summary=Summary
page.title.pricetable=Price Table
page.title.invoice.generator=Invoice Generator
page.title.welcome=VotianLT - Welcome
page.title.password.reset=Reset Password - Enter Email
page.title.add.appuser=Create New App User
page.title.user.messages=Messages
page.title.edit.customer=Edit Customer
page.title.show.customers=Customers
page.title.add.company=Create New Company
page.title.create.invoice=Create Invoice
page.title.add.customer=Create New Customer
page.title.edit.appuser=Edit App User
page.title.forget.password=Reset Password
page.title.job.history=Job History
page.title.admin.pricetable=Price Table
page.title.invoice.generator=Invoice Generator
page.title.job.summary=Summary
page.title.add.job=Create New Job
# Dashboard
dashboard.welcome=Welcome, {0}!
dashboard.footer.copyright=© 2024 VotianLT. All rights reserved.
dashboard.description=Here you can manage your jobs, organize customers and use all important features of VotianLT.
dashboard.system.title=System Overview
dashboard.system.intro=Manage your business processes efficiently with the following features
dashboard.feature.setup.title=Setup
dashboard.feature.setup.desc=Configure your system settings and master data
dashboard.feature.customers.title=Customers
dashboard.feature.customers.desc=Manage your customer relationships and contacts
dashboard.feature.jobs.title=Jobs
dashboard.feature.jobs.desc=Create and manage jobs efficiently
dashboard.app.title=Mobile App
dashboard.app.description=Use the VotianLT app on the go and stay connected
# Add App User
addappuser.title=Create New App User
addappuser.designation=Designation
addappuser.phone=Phone (Mobile)
addappuser.password=Password
addappuser.password.confirm=Confirm Password
addappuser.button.submit=Create App User
addappuser.validation.designation=Designation is required
addappuser.validation.phone=Phone number is required
addappuser.validation.password.required=Password is required
addappuser.validation.password.min=Password must be at least 6 characters
addappuser.validation.password.confirm=Password confirmation is required
addappuser.validation.password.mismatch=Passwords do not match
addappuser.validation.email.required=Email is required
addappuser.validation.email.invalid=Invalid email address
addappuser.notification.validation=Please fill in all required fields
addappuser.notification.success=App user created successfully
addappuser.notification.check=Please check your input
addappuser.notification.email.duplicate=This email address is already in use
addappuser.notification.error=Error: {0}
addappuser.placeholder.designation=(HH H 000)
# Edit App User
editappuser.title=Edit App User
editappuser.password.change=New Password
editappuser.password.change.confirm=Confirm New Password
editappuser.password.placeholder=Leave empty to keep current password
editappuser.notification.invalid.id=Invalid app user ID
editappuser.notification.password.mismatch=Passwords do not match
editappuser.notification.saved=App user saved successfully
editappuser.notification.check=Please check your input
editappuser.notification.password.confirm=Please confirm the new password
editappuser.notification.password.enter=Please enter a new password
editappuser.notification.deleted=App user deleted successfully
editappuser.dialog.delete.text=Do you really want to delete this app user?
editappuser.dialog.delete.confirm=Delete
# Customers
customers.title=Customers
customers.button.add=Add New Customer
customers.hint.click=Click on a customer to view details
customers.column.company=Company
customers.column.name=Name
customers.column.email=Email
customers.column.phone=Phone
customers.column.street=Street
customers.column.city=City
# Edit Customer
editcustomer.title=Edit Customer
editcustomer.notification.notfound=Customer not found
editcustomer.notification.invalid.id=Invalid customer ID
editcustomer.notification.saved=Customer saved successfully
editcustomer.notification.check=Please check your input
editcustomer.notification.deleted=Customer deleted successfully
editcustomer.dialog.delete.text=Do you really want to delete this customer?
editcustomer.dialog.delete.confirm=Delete
# Add Customer
addcustomer.title=Create New Customer
addcustomer.button.submit=Create Customer
addcustomer.notification.validation=Please fill in all required fields
addcustomer.notification.success=Customer created successfully
addcustomer.notification.check=Please check your input
addcustomer.notification.error=Error: {0}
addcustomer.validation.required=This field is required
# Add Company
addcompany.title=Create New Company
addcompany.button.submit=Create Company
# Verwaltung
verwaltung.title=Management
verwaltung.description=Manage your companies, customers and system settings here
# User Messages
usermessages.title.with=Messages with {0}
usermessages.general.title=General Conversations
usermessages.general.conversation=General Conversation
usermessages.job.title=Job-related Messages
usermessages.job.conversation=Job {0}
usermessages.no.job.messages=No job-related messages
usermessages.preview.empty=No preview available
usermessages.message.count={0} Messages
usermessages.unknown=Unknown
usermessages.unknown.participant=Unknown Participant
# Admin Dashboard
admindashboard.title=Admin Dashboard
admindashboard.loading=Loading statistics...
admindashboard.error=Error loading: {0}
admindashboard.section.overview=Overview
admindashboard.section.jobs=Jobs
admindashboard.section.tasks=Tasks
admindashboard.section.users=User Activities
admindashboard.section.health=System Status
admindashboard.stat.totaljobs=Total Jobs
admindashboard.stat.users=Users
admindashboard.stat.appusers=App Users
admindashboard.stat.lastupdated=Last Updated
admindashboard.stat.openjobs=Open Jobs
admindashboard.stat.inprogress=In Progress
admindashboard.stat.completed=Completed
admindashboard.stat.cargo=Cargo Items
admindashboard.stat.status.info=Status
admindashboard.stat.status.unavailable=Not Available
admindashboard.stat.totaltasks=Total Tasks
admindashboard.stat.completedtasks=Completed
admindashboard.stat.pendingtasks=Pending
admindashboard.stat.successrate=Success Rate
admindashboard.stat.photos=Photos
admindashboard.stat.barcodes=Barcodes
admindashboard.stat.signatures=Signatures
admindashboard.stat.comments=Comments
admindashboard.stat.database=Database
admindashboard.stat.database.connected=Connected
admindashboard.stat.database.error=Error
admindashboard.stat.websocket=WebSocket
admindashboard.stat.websocket.active=Active
admindashboard.stat.app=Application
admindashboard.stat.app.running=Running
admindashboard.stat.memory=Memory
# Messages
messages.title=Messages
messages.column.status=Status
messages.column.client=Client
messages.column.email=Email
messages.column.total=Total
messages.column.unread=Unread
messages.column.lastmessage=Last Message
messages.column.preview=Preview
messages.notification.error=Error loading messages
messages.preview.image=Image
messages.preview.empty=No preview
messages.sender.unknown=Unknown sender
# Add Job
addjob.title=Create New Job
addjob.customer.label=Customer
addjob.customer.placeholder=Select customer
addjob.customer.unnamed=Unnamed Customer
addjob.button.clearfields=Clear Fields
addjob.button.submit=Create Job
addjob.address.salutation=Salutation
addjob.address.salutation.placeholder=Select salutation
addjob.salutation.mr=Mr
addjob.salutation.ms=Ms
addjob.salutation.other=Other
addjob.address.company.placeholder=Enter company
addjob.address.street.placeholder=Enter street
addjob.address.housenumber=House Number
addjob.address.addition.placeholder=Address addition
addjob.address.city=City
addjob.address.city.placeholder.pickup=City (Pickup)
addjob.address.city.placeholder.delivery=City (Delivery)
addjob.address.delivery.street.placeholder=Street (Delivery)
addjob.address.delivery.addition.placeholder=Address addition (Delivery)
addjob.address.save=Save Address
addjob.section.pickup=Pickup
addjob.section.delivery=Delivery
addjob.tab.addresses=Customer & Addresses
addjob.tab.appointments=Appointments & Processing
addjob.tab.cargo=Cargo
addjob.tab.tasks=Tasks
addjob.tab.price=Price & Submit
addjob.appointment.date=Date
addjob.appointment.time=Time
addjob.appointment.pickup=Pickup Appointment
addjob.appointment.delivery=Delivery Appointment
addjob.settings.digitalprocess=Digital Processing via App
addjob.appuser.label=App User
addjob.appuser.placeholder=Select app user
addjob.cargo.description=Description
addjob.cargo.description.placeholder=Enter description
addjob.cargo.quantity=Quantity
addjob.cargo.weight=Weight
addjob.cargo.length=Length
addjob.cargo.width=Width
addjob.cargo.height=Height
addjob.cargo.europalette=Euro Pallet
addjob.cargo.disposablepalette=Disposable Pallet
addjob.cargo.dusseldorfpalette=Düsseldorf Pallet
addjob.cargo.gridboxpalette=Grid Box Pallet
addjob.cargo.gridcart=Grid Cart
addjob.cargo.parcel=Parcel
addjob.cargo.add=Add Cargo
addjob.tasks.title=Tasks
addjob.tasks.template.placeholder=Select template
addjob.tasks.template.save.tooltip=Save as template
addjob.tasks.template.save.title=Save Template
addjob.tasks.template.name=Template Name
addjob.tasks.template.name.placeholder=Enter name
addjob.tasks.template.name.required=Name is required
addjob.tasks.template.saved=Template "{0}" saved
addjob.tasks.template.save.error=Error saving: {0}
addjob.tasks.template.dialog.error=Error opening dialog: {0}
addjob.tasks.template.no.tasks=No tasks to save
addjob.tasks.template.load.title=Load Template
addjob.tasks.template.load.text=Do you want to load template "{0}"? This will replace all current tasks.
addjob.tasks.template.load.confirm=Load
addjob.tasks.template.loaded=Template "{0}" loaded
addjob.tasks.template.load.error=Error loading: {0}
addjob.tasks.template.load.templates.error=Error loading templates: {0}
addjob.tasks.add=Add Task
addjob.tasks.tasktype=Task Type
addjob.tasks.tasktype.placeholder=Select type
addjob.tasks.description=Description
addjob.tasks.description.placeholder=Enter description
addjob.tasks.buttontext=Button Text
addjob.tasks.buttontext.placeholder=Enter text
addjob.tasks.remark=Remark
addjob.tasks.remark.placeholder=Enter remark
addjob.tasks.photo.min=Min. Photos
addjob.tasks.photo.max=Max. Photos
addjob.tasks.barcode.min=Min. Barcodes
addjob.tasks.barcode.max=Max. Barcodes
addjob.tasks.signature.noconfig=No configuration required
addjob.tasks.todolist.title=To-Do List
addjob.tasks.todolist.item.placeholder=Enter to-do
addjob.tasks.todolist.add=Add To-Do
addjob.tasks.comment.label=Comment
addjob.tasks.comment.placeholder=Enter comment
addjob.tasks.comment.required=Comment required
addjob.services.title=Services
addjob.services.add=Add Service
addjob.services.calculation=Calculation
addjob.services.basis.distance=Distance (km)
addjob.services.basis.time=Time
addjob.services.basis.flatrate=Flat Rate
addjob.services.vat=VAT
addjob.services.route.missing=Route missing
addjob.services.dialog.title=Select Service
addjob.services.dialog.placeholder=Select service
addjob.services.dialog.add=Add
addjob.summary.title=Summary
addjob.summary.net=Net
addjob.summary.vat=VAT
addjob.summary.gross=Gross
addjob.route.title=Route
addjob.route.distance=Distance
addjob.route.distance.km=Distance (km)
addjob.route.distance.placeholder=e.g. 150.5
addjob.route.duration=Duration
addjob.route.duration.min=Duration (Min.)
addjob.route.duration.placeholder=e.g. 120
addjob.route.manual.title=Manual Route Entry
addjob.route.manual.hint=Enter distance and duration manually if no route was calculated
addjob.notification.success=Job {0} created successfully
addjob.notification.cleared=All fields cleared
addjob.notification.draft.restored=Draft restored
addjob.validation.required.fields=Please fill in all required fields
addjob.validation.appuser.required=Please select an app user
addjob.validation.cargo.required=Please enter at least one cargo item
addjob.validation.pickupdate.future=Pickup date must be today or in the future
addjob.validation.deliverydate.future=Delivery date must be today or in the future
addjob.validation.dialog.title=Address Validation
addjob.validation.dialog.loading=Validating addresses...
addjob.validation.dialog.back=Back
addjob.validation.dialog.continue=Continue
addjob.validation.dialog.continue.anyway=Continue anyway
addjob.validation.pickup.address=Pickup Address
addjob.validation.delivery.address=Delivery Address
addjob.validation.route=Route
# Job Summary
jobsummary.title=Summary
jobsummary.error.noid=No job ID provided
jobsummary.error.invalidid=Invalid job ID format: {0}
jobsummary.error.notfound=Job with ID {0} not found
jobsummary.button.sendmessage=Send Message
jobsummary.button.jobhistory=Job History
jobsummary.button.complete=Complete Job Manually
jobsummary.dialog.complete.title=Complete Job
jobsummary.dialog.complete.text=Do you want to manually complete job {0}?
jobsummary.dialog.complete.cancel=Cancel
jobsummary.dialog.complete.confirm=Complete
jobsummary.notification.completed=Job {0} completed
jobsummary.notification.complete.error=Error completing job: {0}
jobsummary.notification.noappuser=No app user assigned to this job
jobsummary.section.pickup=Pickup
jobsummary.section.delivery=Delivery
jobsummary.section.tasks=Tasks to Confirm
jobsummary.section.cargo=Cargo to Transport
jobsummary.section.info=Additional Information
jobsummary.tasks.none=No tasks
jobsummary.cargo.none=No cargo information
jobsummary.info.netto=Net
jobsummary.info.ust=VAT
jobsummary.info.gesamt=Total
jobsummary.info.bemerkung=Remark
jobsummary.info.digital=Digital Processing via App: enabled
jobsummary.info.appuser=App User
jobsummary.task.status.abgeschlossen=Completed
jobsummary.task.status.offen=Open
jobsummary.task.typ=Type
jobsummary.task.completedAt=Completed at
jobsummary.task.completedBy=Completed by
jobsummary.task.todo.items=To-Do Items
jobsummary.task.photo.info=Photos
jobsummary.task.photo.minmax=At least {0} photos required
jobsummary.task.photo.maxonly=Maximum {0} photos allowed
jobsummary.task.photo.taken=Photos taken ({0})
jobsummary.task.button.text=Button Text
jobsummary.button.schliessen=Close
# Jobs
jobs.title=Jobs
jobs.filter.search=Search
jobs.filter.search.placeholder=Search by job number...
jobs.filter.startdate=Start Date
jobs.filter.enddate=End Date
jobs.filter.status=Status
jobs.filter.apply=Apply Filter
jobs.status.all=All
jobs.status.open=Open
jobs.status.done=Done
jobs.notification.completed=Job {0} completed
jobs.column.status=Status
jobs.column.customer=Customer
jobs.column.jobnumber=Job Number
jobs.column.jobdate=Job Date
jobs.column.destination=Destination
jobs.historie.manuell=Manual
jobs.button.csvexport=CSV Export
jobs.tooltip.complete=Complete Job
jobs.tooltip.createinvoice=Create Invoice
jobs.tooltip.delete=Delete Job
jobs.dialog.complete.title=Complete Job
jobs.dialog.complete.text=Do you want to manually complete job {0}?
jobs.dialog.complete.confirm=Complete
jobs.dialog.delete.title=Delete Job
jobs.dialog.delete.text=Do you really want to delete job {0}?
jobs.notification.completed=Job {0} completed
jobs.notification.complete.error=Error completing job: {0}
jobs.notification.deleted=Job {0} deleted
jobs.notification.delete.error=Error deleting job: {0}
# Create Invoice
createinvoice.error.invalidid=Invalid Job ID
createinvoice.error.notfound=Job not found
createinvoice.button.create=Create Invoice
createinvoice.section.job=Job Details
createinvoice.section.route=Route Info
createinvoice.section.services=Services
createinvoice.section.summary=Summary
createinvoice.field.jobnumber=Job Number
createinvoice.field.customer=Customer
createinvoice.field.status=Status
createinvoice.field.price=Price
createinvoice.route.distance=Distance
createinvoice.route.duration=Duration
createinvoice.column.service=Service
createinvoice.column.basis=Calculation Basis
createinvoice.summary.net=Net Total
createinvoice.summary.total=Total Amount
createinvoice.notification.noservices=Please select at least one service
createinvoice.notification.nouser=User not found
createinvoice.notification.notemplate=No invoice template found
createinvoice.notification.error=Error creating invoice: {0}
# Invoices
invoices.title=Invoices
invoices.column.number=Number
invoices.column.customer=Customer
invoices.column.date=Date
invoices.column.amount=Amount
invoices.column.description=Description
# My Invoices
myinvoices.title=My Invoices
myinvoices.hint.noopen=You have no open invoices. All invoices are settled.
myinvoices.bank.institute=Bank
myinvoices.bank.beneficiary=Beneficiary
myinvoices.bank.iban=IBAN
myinvoices.recipient.name=Customer
myinvoices.recipient.department=
myinvoices.item.description=Item: {0}
# App User
appuser.title=App Users
appuser.button.add=Add App User
appuser.column.designation=Designation
appuser.column.firstname=First Name
appuser.column.lastname=Last Name
appuser.column.phone=Phone
appuser.column.appcode=App Code
appuser.column.email=Email
# Statistics
statistics.title=AI Statistics
statistics.subtitle=Ask questions about your jobs and customers
statistics.prompt.placeholder=Enter question...
statistics.quick.jobcount=Number of Jobs
statistics.quick.jobcount.prompt=How many jobs do I currently have?
statistics.quick.revenue=Revenue
statistics.quick.revenue.prompt=What is my revenue this month?
statistics.quick.trend=Trends
statistics.quick.trend.prompt=Show me trends in the last 3 months
statistics.ai.label=AI Response
statistics.data.fetched=Data fetched
statistics.loading=Calculating...
# Job Status
jobstatus.IN_PROGRESS=In Progress
jobstatus.COMPLETED=Completed
# Task Types
tasktype.CONFIRMATION=Confirmation
tasktype.SIGNATURE=Signature
tasktype.TODOLIST=To-Do List
tasktype.PHOTO=Photo
tasktype.BARCODE=Barcode
tasktype.COMMENT=Comment
# Password Reset
passwordreset.title=Reset Password
passwordreset.newpassword=New Password
passwordreset.confirmpassword=Confirm Password
passwordreset.button.submit=Save Password
passwordreset.button.cancel=Cancel
passwordreset.button.send=Send Email
passwordreset.notification.enterpassword=Please enter a new password
passwordreset.notification.mismatch=Passwords do not match
passwordreset.notification.success=Password changed successfully
passwordreset.notification.invalidtoken=Token invalid or expired
passwordreset.notification.entermail=Please enter email
passwordreset.notification.sent=If the email exists, a link has been sent
passwordreset.notification.wait=Please wait {0} seconds before sending the code again
# Email
email.2fa.subject=Your VotianLT Verification Code
email.2fa.body=Your verification code is: {0}\n\nThis code is valid for 10 minutes.\nIf you did not request this registration, please ignore this email.
# Register
register.title=Registration
register.subtitle=Create your VotianLT account
register.email=Email Address
register.password=Password
register.password.placeholder=At least 6 characters
register.password.confirm=Confirm Password
register.password.confirm.placeholder=Repeat password
register.firstname=First Name
register.lastname=Last Name
register.phone=Phone Number
register.company=Company
register.street=Street
register.housenr=House No.
register.zip=Zip Code
register.city=City
register.code.label=Verification Code (6 digits)
register.code.placeholder=e.g. 123456
register.button.submit=Register
register.button.verify=Verify Code and Register
register.button.resend=Resend Code
register.button.back=Back to Start Page
register.notification.email.required=Please enter an email address
register.notification.email.invalid=Please enter a valid email address
register.notification.email.duplicate=A user with this email address already exists
register.notification.password.required=Please enter a password
register.notification.password.min=Password must be at least 6 characters long
register.notification.password.mismatch=Passwords do not match
register.notification.firstname.required=Please enter your first name
register.notification.lastname.required=Please enter your last name
register.notification.phone.required=Please enter your phone number
register.notification.company.required=Please enter the company name
register.notification.street.required=Please enter the street
register.notification.housenr.required=Please enter the house number
register.notification.zip.required=Please enter the zip code
register.notification.city.required=Please enter the city
register.notification.code.sent=A verification code has been sent to {0}
register.notification.code.emailerror=Error sending email: {0}
register.notification.code.expired=The code has expired. Please request a new code.
register.notification.code.invalid=The entered code is invalid
register.notification.code.startfirst=Please start the registration first
register.notification.code.required=Please enter the 6-digit code
register.notification.success=Registration successful. Please log in.
register.notification.failed=Registration failed: {0}
# Start Page
start.title=VotianLT - Your Digital Transport Partner
start.button.login=Login
start.button.register=Register
start.button.createorder=Create Order
start.button.notifications=Notifications
start.button.nonotifications=No new notifications
start.system.title=The System
start.feature.setup.title=Setup Assistant
start.feature.setup.desc=Use the setup assistant to complete your user profile.
start.feature.customers.title=Customer and Job Management
start.feature.customers.desc=With customer and job management, you always have all contact details and job details in view.
start.feature.jobs.title=Job Creation
start.feature.jobs.desc=Create jobs in the system with just a few clicks and determine which employee should process which transport job.
start.app.title=The App
start.app.description=Every job can optionally be processed via the votianLT app - completely without "paperwork". All relevant job information goes directly to the driver's smartphone.
start.imprint.title=Imprint
start.imprint.company=Assecutor Data Service GmbH
start.imprint.address=Ottensener Str. 8, 22525 Hamburg
start.imprint.phone=Phone: +49 40 18 123 771 0
start.imprint.email=Email: ahoi@assecutor.de
start.slogan=Run your business smart … with votianLT!
start.version=Version
# Login View
login.2fa.title=2FA Code
login.2fa.button=Verify Code
login.votianlt=VotianLT
login.version=Version
# Message Details
messagedetails.button.send=Send
messagedetails.placeholder=Enter message...
messagedetails.noimage=(no image content)
messagedetails.imageerror=(image could not be loaded)
# Invoice Generator
invoicegenerator.properties.title=Properties
invoicegenerator.properties.type=Type
invoicegenerator.fontsize.label=Font Size
invoicegenerator.color.label=Text Color
invoicegenerator.color.dialog.title=Choose Text Color
invoicegenerator.color.dialog.hex=Hex Color Value
invoicegenerator.button.cancel=Cancel
invoicegenerator.button.apply=Apply
invoicegenerator.button.delete=Delete Element
invoicegenerator.notification.color.applied=Color applied
invoicegenerator.upload.drop=Drag image here or click
invoicegenerator.upload.success=Image uploaded successfully
invoicegenerator.upload.error=Error uploading: {0}
invoicegenerator.file.rejected=File rejected: {0}
invoicegenerator.properties.select.info=Click on an element in the canvas to edit its properties
# CSV Export
csv.header.customer=Customer
csv.header.jobnumber=Job Number
csv.header.jobdate=Job Date
csv.header.destination=Destination
csv.filename=jobs.csv
# DatePicker I18n
datepicker.month.januar=January
datepicker.month.februar=February
datepicker.month.märz=March
datepicker.month.april=April
datepicker.month.mai=May
datepicker.month.juni=June
datepicker.month.juli=July
datepicker.month.august=August
datepicker.month.september=September
datepicker.month.oktober=October
datepicker.month.november=November
datepicker.month.dezember=December
datepicker.weekday.sonntag=Sunday
datepicker.weekday.montag=Monday
datepicker.weekday.dienstag=Tuesday
datepicker.weekday.mittwoch=Wednesday
datepicker.weekday.donnerstag=Thursday
datepicker.weekday.freitag=Friday
datepicker.weekday.samstag=Saturday
datepicker.weekdayshort.so=Su
datepicker.weekdayshort.mo=Mo
datepicker.weekdayshort.di=Tu
datepicker.weekdayshort.mi=We
datepicker.weekdayshort.do=Th
datepicker.weekdayshort.fr=Fr
datepicker.weekdayshort.sa=Sa
# Job History
jobhistory.status.pickupscheduled=Pickup Scheduled
jobhistory.status.pickedup=Picked Up
jobhistory.status.intransit=In Transit
jobhistory.status.delivered=Delivered
jobhistory.image.alt=Enlarged Photo
# Version
version.label=Version
# Management Combo
management.placeholder=Management
management.customers=Customers
management.jobs=Jobs
management.companies=Companies
# User Menu
usermenu.profile=Show Profile
usermenu.settings=Settings
usermenu.logout=Logout
# CTA Button
cta.freetest=Try for free now
# Miscellaneous
misc.toggle.hide=Hide
misc.toggle.show=Show
misc.nodata=No data available
misc.loading=Loading data...
misc.error=Error occurred
misc.retry=Retry