Erweiterungen

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,10 @@ import de.assecutor.votianlt.model.UserInvoiceData;
import de.assecutor.votianlt.pages.service.AppUserService; import de.assecutor.votianlt.pages.service.AppUserService;
import de.assecutor.votianlt.pages.service.UserInvoiceDataService; import de.assecutor.votianlt.pages.service.UserInvoiceDataService;
import de.assecutor.votianlt.pages.view.EditProfileView; 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.security.SecurityService;
import de.assecutor.votianlt.service.LanguageService;
import de.assecutor.votianlt.service.MessageBadgeUpdateService; import de.assecutor.votianlt.service.MessageBadgeUpdateService;
import de.assecutor.votianlt.service.MessageService; import de.assecutor.votianlt.service.MessageService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -36,6 +39,7 @@ import lombok.extern.slf4j.Slf4j;
import static com.vaadin.flow.theme.lumo.LumoUtility.*; import static com.vaadin.flow.theme.lumo.LumoUtility.*;
import java.util.List; import java.util.List;
import java.util.Locale;
@AnonymousAllowed @AnonymousAllowed
@Slf4j @Slf4j
@@ -47,6 +51,7 @@ public final class MainLayout extends AppLayout {
private final MessageService messageService; private final MessageService messageService;
private final MessageBadgeUpdateService messageBadgeUpdateService; private final MessageBadgeUpdateService messageBadgeUpdateService;
private final AppUserService appUserService; private final AppUserService appUserService;
private final LanguageService languageService;
private Div headerRef; private Div headerRef;
private Scroller navRef; private Scroller navRef;
private Component userMenuRef; private Component userMenuRef;
@@ -56,12 +61,13 @@ public final class MainLayout extends AppLayout {
public MainLayout(SecurityService securityService, UserInvoiceDataService userInvoiceDataService, public MainLayout(SecurityService securityService, UserInvoiceDataService userInvoiceDataService,
MessageService messageService, MessageBadgeUpdateService messageBadgeUpdateService, MessageService messageService, MessageBadgeUpdateService messageBadgeUpdateService,
AppUserService appUserService) { AppUserService appUserService, LanguageService languageService) {
this.securityService = securityService; this.securityService = securityService;
this.userInvoiceDataService = userInvoiceDataService; this.userInvoiceDataService = userInvoiceDataService;
this.messageService = messageService; this.messageService = messageService;
this.messageBadgeUpdateService = messageBadgeUpdateService; this.messageBadgeUpdateService = messageBadgeUpdateService;
this.appUserService = appUserService; this.appUserService = appUserService;
this.languageService = languageService;
setPrimarySection(Section.DRAWER); setPrimarySection(Section.DRAWER);
// Always build the drawer; keep references and toggle visibility on attach and // 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 // Create Details component for "Verwaltung" with collapsible list
Details verwaltungDetails = new Details(); Details verwaltungDetails = new Details();
verwaltungDetails.setSummaryText("Verwaltung"); verwaltungDetails.setSummaryText(getTranslation("nav.management"));
verwaltungDetails.addClassNames(Margin.Horizontal.MEDIUM, FontSize.MEDIUM, FontWeight.MEDIUM, "#000000"); verwaltungDetails.addClassNames(Margin.Horizontal.MEDIUM, FontSize.MEDIUM, FontWeight.MEDIUM, "#000000");
// Create collapsible content with navigation items // Create collapsible content with navigation items
@@ -123,16 +129,16 @@ public final class MainLayout extends AppLayout {
verwaltungContent.setSpacing(true); verwaltungContent.setSpacing(true);
// Create navigation items for the collapsible list // Create navigation items for the collapsible list
SideNavItem jobs = new SideNavItem("Aufträge", "jobs", new Icon(VaadinIcon.LIST)); SideNavItem jobs = new SideNavItem(getTranslation("nav.jobs"), "jobs", new Icon(VaadinIcon.LIST));
SideNavItem customers = new SideNavItem("Kunden", "customers", new Icon(VaadinIcon.USERS)); SideNavItem customers = new SideNavItem(getTranslation("nav.customers"), "customers", new Icon(VaadinIcon.USERS));
SideNavItem appUsers = new SideNavItem("App-Nutzer", "app-user", new Icon(VaadinIcon.USERS)); SideNavItem appUsers = new SideNavItem(getTranslation("nav.appusers"), "app-user", new Icon(VaadinIcon.USERS));
SideNavItem statistics = new SideNavItem("Statistiken", "statistics", new Icon(VaadinIcon.BAR_CHART)); 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 // Only show invoices menu if billing is enabled for the current user
if (isBillingEnabledForCurrentUser()) { 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); verwaltungContent.add(invoices);
} }
@@ -141,7 +147,7 @@ public final class MainLayout extends AppLayout {
// Create Details component for "Verwaltung" with collapsible list // Create Details component for "Verwaltung" with collapsible list
Details userDetails = new Details(); Details userDetails = new Details();
userDetails.setSummaryText("Benutzer"); userDetails.setSummaryText(getTranslation("nav.users"));
userDetails.addClassNames(Margin.Horizontal.MEDIUM, FontSize.MEDIUM, FontWeight.MEDIUM, TextColor.BODY); userDetails.addClassNames(Margin.Horizontal.MEDIUM, FontSize.MEDIUM, FontWeight.MEDIUM, TextColor.BODY);
// Create collapsible content with navigation items // Create collapsible content with navigation items
@@ -150,9 +156,9 @@ public final class MainLayout extends AppLayout {
userContent.setSpacing(true); userContent.setSpacing(true);
// Create navigation items for the collapsible list // Create navigation items for the collapsible list
SideNavItem profile = new SideNavItem("Mein Profil", "edit-profile", new Icon(VaadinIcon.USER)); SideNavItem profile = new SideNavItem(getTranslation("nav.profile"), "edit-profile", new Icon(VaadinIcon.USER));
SideNavItem myInvoices = new SideNavItem("Meine Rechnungen", "my-invoices", new Icon(VaadinIcon.FILE_TEXT)); SideNavItem myInvoices = new SideNavItem(getTranslation("nav.myinvoices"), "my-invoices", new Icon(VaadinIcon.FILE_TEXT));
SideNavItem imprint = new SideNavItem("Impressum", "impressum", new Icon(VaadinIcon.INFO_CIRCLE)); SideNavItem imprint = new SideNavItem(getTranslation("nav.imprint"), "impressum", new Icon(VaadinIcon.INFO_CIRCLE));
userContent.add(profile, myInvoices, imprint); userContent.add(profile, myInvoices, imprint);
userDetails.add(userContent); userDetails.add(userContent);
@@ -261,9 +267,9 @@ public final class MainLayout extends AppLayout {
userMenuItem.add(userNameSpan); userMenuItem.add(userNameSpan);
// Profil anzeigen mit Navigation // Profil anzeigen mit Navigation
userMenuItem.getSubMenu().addItem("Profil anzeigen", e -> UI.getCurrent().navigate(EditProfileView.class)); userMenuItem.getSubMenu().addItem(getTranslation("nav.showprofile"), e -> UI.getCurrent().navigate(EditProfileView.class));
userMenuItem.getSubMenu().addItem("Einstellungen"); userMenuItem.getSubMenu().addItem(getTranslation("nav.settings"));
userMenuItem.getSubMenu().addItem("Abmelden", e -> securityService.logout()); userMenuItem.getSubMenu().addItem(getTranslation("nav.logout"), e -> securityService.logout());
// Update-Funktion für Benutzername und Avatar // Update-Funktion für Benutzername und Avatar
Runnable updateUserInfo = () -> { Runnable updateUserInfo = () -> {
@@ -298,6 +304,9 @@ public final class MainLayout extends AppLayout {
super.onAttach(attachEvent); super.onAttach(attachEvent);
UI ui = attachEvent.getUI(); UI ui = attachEvent.getUI();
// Apply user's preferred language immediately after login
applyUserLanguagePreference();
// Update badge immediately when layout is attached // Update badge immediately when layout is attached
updateMessagesBadge(); 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 @Override
protected void onDetach(DetachEvent detachEvent) { protected void onDetach(DetachEvent detachEvent) {
if (badgeUpdateRegistration != null) { if (badgeUpdateRegistration != null) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,8 +10,8 @@ import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Menu; import com.vaadin.flow.router.Menu;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility; import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.votianlt.model.JobStatus; import de.assecutor.votianlt.model.JobStatus;
@@ -25,11 +25,10 @@ import java.time.LocalDateTime;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@Route(value = "admin-dashboard", layout = de.assecutor.votianlt.pages.base.ui.view.AdminLayout.class) @Route(value = "admin-dashboard", layout = de.assecutor.votianlt.pages.base.ui.view.AdminLayout.class)
@PageTitle("Admin Dashboard")
@RolesAllowed("ADMIN") @RolesAllowed("ADMIN")
@Menu(order = 1, icon = "lumo:edit") @Menu(order = 1, icon = "lumo:edit")
@Slf4j @Slf4j
public class AdminDashboardView extends Main { public class AdminDashboardView extends Main implements HasDynamicTitle {
private final JobRepository jobRepository; private final JobRepository jobRepository;
private final TaskRepository taskRepository; private final TaskRepository taskRepository;
@@ -64,7 +63,7 @@ public class AdminDashboardView extends Main {
LumoUtility.Padding.MEDIUM); LumoUtility.Padding.MEDIUM);
// Header // Header
H1 title = new H1("Administrator Dashboard"); H1 title = new H1(getTranslation("admindashboard.title"));
title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, LumoUtility.Margin.Top.NONE); title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, LumoUtility.Margin.Top.NONE);
HorizontalLayout header = new HorizontalLayout(title); HorizontalLayout header = new HorizontalLayout(title);
@@ -92,7 +91,7 @@ public class AdminDashboardView extends Main {
// Show loading indicator // Show loading indicator
statisticsContainer.removeAll(); statisticsContainer.removeAll();
statisticsContainer.add(new Span("Lade Statistiken...")); statisticsContainer.add(new Span(getTranslation("admindashboard.loading")));
// Load statistics asynchronously // Load statistics asynchronously
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(() -> {
@@ -102,7 +101,7 @@ public class AdminDashboardView extends Main {
} catch (Exception e) { } catch (Exception e) {
log.error("Error loading dashboard statistics", e); log.error("Error loading dashboard statistics", e);
statisticsContainer.removeAll(); 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.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem"); 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); title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
HorizontalLayout cards = new HorizontalLayout(); HorizontalLayout cards = new HorizontalLayout();
@@ -149,19 +148,19 @@ public class AdminDashboardView extends Main {
// Total jobs card // Total jobs card
long totalJobs = jobRepository.count(); 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 // Total users card
long totalUsers = userRepository.count(); 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 // Total app users card
long totalAppUsers = appUserRepository.count(); 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 // Current time
String currentTime = DateTimeFormatUtil.formatDateTime(LocalDateTime.now()); 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); section.add(title, cards);
return section; return section;
@@ -173,7 +172,7 @@ public class AdminDashboardView extends Main {
section.addClassName(LumoUtility.Background.CONTRAST_5); section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem"); 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); title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
HorizontalLayout cards = new HorizontalLayout(); HorizontalLayout cards = new HorizontalLayout();
@@ -186,17 +185,17 @@ public class AdminDashboardView extends Main {
long inProgressJobs = jobRepository.countByStatus(JobStatus.IN_PROGRESS); long inProgressJobs = jobRepository.countByStatus(JobStatus.IN_PROGRESS);
long completedJobs = jobRepository.countByStatus(JobStatus.COMPLETED); long completedJobs = jobRepository.countByStatus(JobStatus.COMPLETED);
cards.add(createStatCard("Offene Jobs", String.valueOf(openJobs), VaadinIcon.HOURGLASS_START, "orange")); cards.add(createStatCard(getTranslation("admindashboard.stat.openjobs"), String.valueOf(openJobs), VaadinIcon.HOURGLASS_START, "orange"));
cards.add(createStatCard("In Bearbeitung", String.valueOf(inProgressJobs), VaadinIcon.PLAY, "blue")); cards.add(createStatCard(getTranslation("admindashboard.stat.inprogress"), String.valueOf(inProgressJobs), VaadinIcon.PLAY, "blue"));
cards.add(createStatCard("Abgeschlossen", String.valueOf(completedJobs), VaadinIcon.CHECK_CIRCLE, "green")); cards.add(createStatCard(getTranslation("admindashboard.stat.completed"), String.valueOf(completedJobs), VaadinIcon.CHECK_CIRCLE, "green"));
// Total cargo items // Total cargo items
long totalCargoItems = cargoItemRepository.count(); 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) { } catch (Exception e) {
log.warn("Could not load job statistics by status", 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); section.add(title, cards);
@@ -209,7 +208,7 @@ public class AdminDashboardView extends Main {
section.addClassName(LumoUtility.Background.CONTRAST_5); section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem"); 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); title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
HorizontalLayout cards = new HorizontalLayout(); HorizontalLayout cards = new HorizontalLayout();
@@ -218,19 +217,19 @@ public class AdminDashboardView extends Main {
// Total tasks // Total tasks
long totalTasks = taskRepository.count(); 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 // Completed tasks
long completedTasks = taskRepository.countByCompleted(true); 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 // Pending tasks
long pendingTasks = totalTasks - completedTasks; 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 // Completion rate
double completionRate = totalTasks > 0 ? (completedTasks * 100.0 / totalTasks) : 0; 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")); "purple"));
section.add(title, cards); section.add(title, cards);
@@ -243,7 +242,7 @@ public class AdminDashboardView extends Main {
section.addClassName(LumoUtility.Background.CONTRAST_5); section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem"); 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); title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
HorizontalLayout cards = new HorizontalLayout(); HorizontalLayout cards = new HorizontalLayout();
@@ -252,16 +251,16 @@ public class AdminDashboardView extends Main {
// Content statistics // Content statistics
long totalPhotos = photoRepository.count(); 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(); 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(); 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(); 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); section.add(title, cards);
return section; return section;
@@ -273,7 +272,7 @@ public class AdminDashboardView extends Main {
section.addClassName(LumoUtility.Background.CONTRAST_5); section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem"); 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); title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
HorizontalLayout cards = new HorizontalLayout(); HorizontalLayout cards = new HorizontalLayout();
@@ -283,16 +282,16 @@ public class AdminDashboardView extends Main {
// Database connection status // Database connection status
try { try {
userRepository.count(); // Test database connection 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) { } 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 // 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) // 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) // Memory usage (placeholder)
Runtime runtime = Runtime.getRuntime(); Runtime runtime = Runtime.getRuntime();
@@ -300,7 +299,7 @@ public class AdminDashboardView extends Main {
long totalMemory = runtime.totalMemory() / 1024 / 1024; // MB long totalMemory = runtime.totalMemory() / 1024 / 1024; // MB
long usedMemory = totalMemory - (runtime.freeMemory() / 1024 / 1024); // MB long usedMemory = totalMemory - (runtime.freeMemory() / 1024 / 1024); // MB
String memoryInfo = usedMemory + "/" + maxMemory + " 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); section.add(title, cards);
return section; return section;
@@ -337,4 +336,9 @@ public class AdminDashboardView extends Main {
card.add(content); card.add(content);
return card; return card;
} }
@Override
public String getPageTitle() {
return getTranslation("page.title.admin.dashboard");
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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