From 00811cdc36c7343fa2632d4869fbbe776acf72a7 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Thu, 19 Feb 2026 19:43:22 +0100 Subject: [PATCH] Erweiterungen --- .../config/LocaleVaadinInitListener.java | 64 ++ .../votianlt/config/TranslationProvider.java | 57 ++ .../de/assecutor/votianlt/model/Language.java | 27 + .../de/assecutor/votianlt/model/User.java | 7 +- .../votianlt/model/task/BarcodeTask.java | 2 +- .../votianlt/model/task/CommentTask.java | 2 +- .../votianlt/model/task/ConfirmationTask.java | 2 +- .../votianlt/model/task/PhotoTask.java | 2 +- .../votianlt/model/task/SignatureTask.java | 2 +- .../votianlt/model/task/TaskType.java | 39 +- .../votianlt/model/task/TodoListTask.java | 2 +- .../pages/base/ui/view/MainLayout.java | 77 +- .../votianlt/pages/view/AddAppUserView.java | 89 +- .../votianlt/pages/view/AddCompanyView.java | 38 +- .../votianlt/pages/view/AddCustomerView.java | 77 +- .../votianlt/pages/view/AddJobView.java | 364 ++++---- .../pages/view/AdminDashboardView.java | 70 +- .../pages/view/AdminPricetableView.java | 29 +- .../votianlt/pages/view/AppUserView.java | 26 +- .../pages/view/AuthenticatedStartView.java | 53 +- .../pages/view/CreateInvoiceView.java | 76 +- .../votianlt/pages/view/CustomersView.java | 18 +- .../votianlt/pages/view/EditAppUserView.java | 71 +- .../votianlt/pages/view/EditCustomerView.java | 74 +- .../votianlt/pages/view/EditProfileView.java | 414 +++++---- .../pages/view/ForgetPasswordView.java | 10 +- .../pages/view/ForgotPasswordRequestView.java | 10 +- .../votianlt/pages/view/ImprintView.java | 12 +- .../pages/view/InvoiceGeneratorView.java | 88 +- .../votianlt/pages/view/InvoicesView.java | 24 +- .../votianlt/pages/view/JobHistoryView.java | 48 +- .../votianlt/pages/view/JobSummaryView.java | 56 +- .../votianlt/pages/view/LoginView.java | 28 +- .../pages/view/MessageDetailsView.java | 12 +- .../votianlt/pages/view/MessagesView.java | 40 +- .../votianlt/pages/view/MyInvoicesView.java | 49 +- .../votianlt/pages/view/RegisterView.java | 52 +- .../pages/view/ShowCustomersView.java | 28 +- .../votianlt/pages/view/ShowJobsView.java | 83 +- .../votianlt/pages/view/StartView.java | 18 +- .../votianlt/pages/view/StatisticsView.java | 35 +- .../votianlt/pages/view/UserMessagesView.java | 37 +- .../votianlt/pages/view/VerwaltungView.java | 18 +- .../votianlt/service/LanguageService.java | 88 ++ src/main/resources/messages.properties | 876 ++++++++++++++++++ src/main/resources/messages_en.properties | 876 ++++++++++++++++++ 46 files changed, 3221 insertions(+), 949 deletions(-) create mode 100644 src/main/java/de/assecutor/votianlt/config/LocaleVaadinInitListener.java create mode 100644 src/main/java/de/assecutor/votianlt/config/TranslationProvider.java create mode 100644 src/main/java/de/assecutor/votianlt/model/Language.java create mode 100644 src/main/java/de/assecutor/votianlt/service/LanguageService.java create mode 100644 src/main/resources/messages.properties create mode 100644 src/main/resources/messages_en.properties diff --git a/src/main/java/de/assecutor/votianlt/config/LocaleVaadinInitListener.java b/src/main/java/de/assecutor/votianlt/config/LocaleVaadinInitListener.java new file mode 100644 index 0000000..852a7cc --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/config/LocaleVaadinInitListener.java @@ -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"); + }; + } +} diff --git a/src/main/java/de/assecutor/votianlt/config/TranslationProvider.java b/src/main/java/de/assecutor/votianlt/config/TranslationProvider.java new file mode 100644 index 0000000..f42862e --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/config/TranslationProvider.java @@ -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 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); + } +} \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/model/Language.java b/src/main/java/de/assecutor/votianlt/model/Language.java new file mode 100644 index 0000000..d5c8ccb --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/model/Language.java @@ -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 + } +} \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/model/User.java b/src/main/java/de/assecutor/votianlt/model/User.java index 8835f39..11d3e05 100644 --- a/src/main/java/de/assecutor/votianlt/model/User.java +++ b/src/main/java/de/assecutor/votianlt/model/User.java @@ -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; -} \ No newline at end of file + + // Spracheinstellung (standardmäßig Deutsch) + @Field("language") + private Language language = Language.DE; +} diff --git a/src/main/java/de/assecutor/votianlt/model/task/BarcodeTask.java b/src/main/java/de/assecutor/votianlt/model/task/BarcodeTask.java index cd88c8b..656d696 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/BarcodeTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/BarcodeTask.java @@ -28,7 +28,7 @@ public class BarcodeTask extends BaseTask { @Override public String getDisplayName() { - return "Barcode"; + return TaskType.BARCODE.getDisplayName(); } @Override diff --git a/src/main/java/de/assecutor/votianlt/model/task/CommentTask.java b/src/main/java/de/assecutor/votianlt/model/task/CommentTask.java index 2dbcb76..92f2e52 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/CommentTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/CommentTask.java @@ -28,7 +28,7 @@ public class CommentTask extends BaseTask { @Override public String getDisplayName() { - return "Kommentar"; + return TaskType.COMMENT.getDisplayName(); } @Override diff --git a/src/main/java/de/assecutor/votianlt/model/task/ConfirmationTask.java b/src/main/java/de/assecutor/votianlt/model/task/ConfirmationTask.java index 5fae187..df67259 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/ConfirmationTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/ConfirmationTask.java @@ -24,7 +24,7 @@ public class ConfirmationTask extends BaseTask { @Override public String getDisplayName() { - return "Bestätigung"; + return TaskType.CONFIRMATION.getDisplayName(); } @Override diff --git a/src/main/java/de/assecutor/votianlt/model/task/PhotoTask.java b/src/main/java/de/assecutor/votianlt/model/task/PhotoTask.java index 37f3914..eb14c18 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/PhotoTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/PhotoTask.java @@ -28,7 +28,7 @@ public class PhotoTask extends BaseTask { @Override public String getDisplayName() { - return "Foto"; + return TaskType.PHOTO.getDisplayName(); } @Override diff --git a/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java b/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java index 2914803..22b17b2 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/SignatureTask.java @@ -16,7 +16,7 @@ public class SignatureTask extends BaseTask { @Override public String getDisplayName() { - return "Unterschrift"; + return TaskType.SIGNATURE.getDisplayName(); } @Override diff --git a/src/main/java/de/assecutor/votianlt/model/task/TaskType.java b/src/main/java/de/assecutor/votianlt/model/task/TaskType.java index 3f7b494..e57ffd3 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/TaskType.java +++ b/src/main/java/de/assecutor/votianlt/model/task/TaskType.java @@ -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"; + }; } } diff --git a/src/main/java/de/assecutor/votianlt/model/task/TodoListTask.java b/src/main/java/de/assecutor/votianlt/model/task/TodoListTask.java index 1731253..540f149 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/TodoListTask.java +++ b/src/main/java/de/assecutor/votianlt/model/task/TodoListTask.java @@ -26,7 +26,7 @@ public class TodoListTask extends BaseTask { @Override public String getDisplayName() { - return "To-Do Liste"; + return TaskType.TODOLIST.getDisplayName(); } @Override diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java index 26ee175..a87fc39 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java @@ -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); } -} \ No newline at end of file +} diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java index 3ff1d3e..2b84d24 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java @@ -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 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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddCompanyView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddCompanyView.java index 4189a22..1f0b8a7 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddCompanyView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddCompanyView.java @@ -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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java index 0de918f..66b530f 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java @@ -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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java index 58ea67e..1b56726 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -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 availableServices = serviceRepository .findByUserId(securityService.getCurrentDatabaseUser().getId().toString()); - ComboBox serviceCombo = new ComboBox<>("Leistung"); + ComboBox 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 desc = new ComboBox<>("Beschreibung"); - desc.setItems("Europalette", "Einwegpalette", "Düsseldorfer-Palette", "Gitterboxpalette", "Gitterwagen", - "Paket"); + ComboBox 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 taskTypeCombo = new ComboBox<>("Aufgabentyp"); + ComboBox 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 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 taskTypeCombo = new ComboBox<>("Aufgabentyp"); + ComboBox 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 diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AdminDashboardView.java b/src/main/java/de/assecutor/votianlt/pages/view/AdminDashboardView.java index 3f6f27e..be3283a 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AdminDashboardView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AdminDashboardView.java @@ -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"); + } } \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java b/src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java index c1433aa..81e56a6 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java @@ -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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java b/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java index be6e52b..b5ed569 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java @@ -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 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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java b/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java index 8dade16..65f63ab 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java @@ -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; } -} \ No newline at end of file + + @Override + public String getPageTitle() { + return getTranslation("page.title.dashboard"); + } +} diff --git a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java index 9a18c4c..ac0a870 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java @@ -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 { +public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter, 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 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"); + } } \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/pages/view/CustomersView.java b/src/main/java/de/assecutor/votianlt/pages/view/CustomersView.java index 0d89712..ddcac14 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/CustomersView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/CustomersView.java @@ -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 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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java index 2048be2..337b757 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java @@ -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 { +public class EditAppUserView extends VerticalLayout implements HasUrlParameter, HasDynamicTitle { private final AppUserService appUserService; private AppUser appUser; private final Binder 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 navigateBack()); @@ -101,11 +109,9 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter 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 { + 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 ui.navigate("app-user")); } + + @Override + public String getPageTitle() { + return getTranslation("page.title.appuser.edit"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java index e610bdd..67b33e6 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java @@ -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 { +public class EditCustomerView extends VerticalLayout implements HasUrlParameter, HasDynamicTitle { private final CustomerService customerService; private Customer customer; private final Binder 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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java index a39cc74..3956d49 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java @@ -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 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 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 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 calculationBasisCombo = new ComboBox<>("Berechnungsgrundlage"); + ComboBox 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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/ForgetPasswordView.java b/src/main/java/de/assecutor/votianlt/pages/view/ForgetPasswordView.java index 10f7816..535ca62 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/ForgetPasswordView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/ForgetPasswordView.java @@ -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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/ForgotPasswordRequestView.java b/src/main/java/de/assecutor/votianlt/pages/view/ForgotPasswordRequestView.java index 32f875b..1023ab1 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/ForgotPasswordRequestView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/ForgotPasswordRequestView.java @@ -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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/ImprintView.java b/src/main/java/de/assecutor/votianlt/pages/view/ImprintView.java index e20a24c..5b6a3c0 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/ImprintView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/ImprintView.java @@ -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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java b/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java index 6e6604c..aff4a72 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java @@ -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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java b/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java index ecbbaf9..b0949a5 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java @@ -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 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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java b/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java index 45bd133..7dcb7d3 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java @@ -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 { +public class JobHistoryView extends Main implements HasUrlParameter, HasDynamicTitle { private final JobRepository jobRepository; private final JobHistoryService jobHistoryService; @@ -57,7 +56,7 @@ public class JobHistoryView extends Main implements HasUrlParameter { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { // 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 { 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 { "
" + responsiveSvg + "
"); } + @Override + public String getPageTitle() { + return getTranslation("page.title.job.history"); + } + private void showEnlargedSignature(String svgContent) { Dialog signatureDialog = new Dialog(); signatureDialog.setWidth("60vw"); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java index f15f9a8..2b8947a 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java @@ -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 { +public class JobSummaryView extends Main implements HasUrlParameter, HasDynamicTitle { private final JobRepository jobRepository; private final CargoItemRepository cargoItemRepository; @@ -145,12 +144,12 @@ public class JobSummaryView extends Main implements HasUrlParameter { } // 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 { }); // 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 { 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 { 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 { // 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 { 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 { 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 { } 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 { 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 { 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 { 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"); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java b/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java index 9749366..1ad820a 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java @@ -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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java b/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java index c73a5dd..ff100bb 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java @@ -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 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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/MessagesView.java b/src/main/java/de/assecutor/votianlt/pages/view/MessagesView.java index c605ba8..28f1c7d 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/MessagesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/MessagesView.java @@ -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 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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java b/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java index 1ced6e7..d2b64d1 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java @@ -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 grid = new Grid<>(MyInvoiceRow.class, false); private final List 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 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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java b/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java index 441a81a..903bbb9 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java @@ -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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java b/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java index 047c851..13a2629 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java @@ -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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java b/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java index 53a847a..d11a21f 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java @@ -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 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 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 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 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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/StartView.java b/src/main/java/de/assecutor/votianlt/pages/view/StartView.java index 4fe8a19..64f62e0 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/StartView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/StartView.java @@ -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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/StatisticsView.java b/src/main/java/de/assecutor/votianlt/pages/view/StatisticsView.java index 47f7f94..5df7c74 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/StatisticsView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/StatisticsView.java @@ -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"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java b/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java index 6871cb1..d31a791 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java @@ -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 { +public class UserMessagesView extends Main implements HasUrlParameter, HasDynamicTitle { private final AppUserService appUserService; private final MessageService messageService; @@ -79,7 +78,7 @@ public class UserMessagesView extends Main implements HasUrlParameter { } 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 { } 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 { 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 sortedMessages = new ArrayList<>(); @@ -136,7 +135,7 @@ public class UserMessagesView extends Main implements HasUrlParameter { 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 { 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 { 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 { } 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 { // 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 { 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 { 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 { 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 { } return value.replaceAll("[^a-zA-Z0-9_-]", "_"); } + + @Override + public String getPageTitle() { + return getTranslation("page.title.user.messages"); + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/VerwaltungView.java b/src/main/java/de/assecutor/votianlt/pages/view/VerwaltungView.java index 39cddfc..8b764df 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/VerwaltungView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/VerwaltungView.java @@ -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); } -} \ No newline at end of file + + @Override + public String getPageTitle() { + return getTranslation("page.title.verwaltung"); + } +} diff --git a/src/main/java/de/assecutor/votianlt/service/LanguageService.java b/src/main/java/de/assecutor/votianlt/service/LanguageService.java new file mode 100644 index 0000000..6c09c02 --- /dev/null +++ b/src/main/java/de/assecutor/votianlt/service/LanguageService.java @@ -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; + } +} diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties new file mode 100644 index 0000000..8f2a343 --- /dev/null +++ b/src/main/resources/messages.properties @@ -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 \ No newline at end of file diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties new file mode 100644 index 0000000..7775be7 --- /dev/null +++ b/src/main/resources/messages_en.properties @@ -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 \ No newline at end of file