diff --git a/src/main/frontend/utils/language-cookie.ts b/src/main/frontend/utils/language-cookie.ts index be1da26..873bbc7 100644 --- a/src/main/frontend/utils/language-cookie.ts +++ b/src/main/frontend/utils/language-cookie.ts @@ -9,8 +9,8 @@ const COOKIE_MAX_AGE_DAYS = 365; // Cookie gültig für 1 Jahr * @param languageCode - The language code (e.g., 'de', 'en', 'fr', 'es') */ export function setLanguageCookie(languageCode: string): void { - const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60; // Convert days to seconds - document.cookie = `${LANGUAGE_COOKIE_NAME}=${languageCode};path=/;max-age=${maxAge};SameSite=Lax`; + const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60; // Convert days to seconds + document.cookie = `${LANGUAGE_COOKIE_NAME}=${languageCode};path=/;max-age=${maxAge};SameSite=Lax`; } /** @@ -18,39 +18,39 @@ export function setLanguageCookie(languageCode: string): void { * @returns The language code or null if not found */ export function getLanguageCookie(): string | null { - const cookies = document.cookie.split(';'); - for (const cookie of cookies) { - const [name, value] = cookie.trim().split('='); - if (name === LANGUAGE_COOKIE_NAME) { - return decodeURIComponent(value); - } + const cookies = document.cookie.split(';'); + for (const cookie of cookies) { + const [name, value] = cookie.trim().split('='); + if (name === LANGUAGE_COOKIE_NAME) { + return decodeURIComponent(value); } - return null; + } + return null; } /** * Clears the language cookie */ export function clearLanguageCookie(): void { - document.cookie = `${LANGUAGE_COOKIE_NAME}=;path=/;max-age=0;SameSite=Lax`; + document.cookie = `${LANGUAGE_COOKIE_NAME}=;path=/;max-age=0;SameSite=Lax`; } /** * Maps Language enum values to locale strings */ export const languageToLocale: Record = { - 'DE': 'de', - 'EN': 'en', - 'FR': 'fr', - 'ES': 'es' + DE: 'de', + EN: 'en', + FR: 'fr', + ES: 'es', }; /** * Maps locale strings to Language enum values */ export const localeToLanguage: Record = { - 'de': 'DE', - 'en': 'EN', - 'fr': 'FR', - 'es': 'ES' -}; \ No newline at end of file + de: 'DE', + en: 'EN', + fr: 'FR', + es: 'ES', +}; diff --git a/src/main/java/de/assecutor/votianlt/config/LocaleVaadinInitListener.java b/src/main/java/de/assecutor/votianlt/config/LocaleVaadinInitListener.java index f6016b3..9f2abf3 100644 --- a/src/main/java/de/assecutor/votianlt/config/LocaleVaadinInitListener.java +++ b/src/main/java/de/assecutor/votianlt/config/LocaleVaadinInitListener.java @@ -16,12 +16,13 @@ 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. + * constructed. Registered via {@code UIInitListener} → + * {@code BeforeEnterListener}, which fires prior to the router creating the + * layout component tree. * * For authenticated users: Uses the language preference from the user profile. - * For anonymous users: Uses the language from the 'votianlt.language' cookie - * or falls back to the browser's preferred locale. + * For anonymous users: Uses the language from the 'votianlt.language' cookie or + * falls back to the browser's preferred locale. */ @Component @Slf4j @@ -40,7 +41,7 @@ public class LocaleVaadinInitListener implements VaadinServiceInitListener { private void applyLocale(UI ui) { try { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - + if (auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken)) { // Authenticated user: use profile language applyLocaleFromAuthenticatedUser(ui, auth); @@ -70,14 +71,14 @@ public class LocaleVaadinInitListener implements VaadinServiceInitListener { private void applyLocaleFromCookieOrBrowser(UI ui) { Locale targetLocale = null; - + // Try to get locale from cookie first String cookieLanguage = getLanguageFromCookie(ui); if (cookieLanguage != null) { targetLocale = getLocaleFromLanguageCode(cookieLanguage); log.debug("Using locale {} from cookie for anonymous user", targetLocale); } - + // If no cookie, use browser's preferred locale if supported if (targetLocale == null) { targetLocale = getSupportedLocaleFromBrowser(ui); @@ -85,7 +86,7 @@ public class LocaleVaadinInitListener implements VaadinServiceInitListener { log.debug("Using browser locale {} for anonymous user", targetLocale); } } - + // Apply the locale if different from current if (targetLocale != null && !targetLocale.equals(ui.getLocale())) { ui.setLocale(targetLocale); @@ -99,12 +100,12 @@ public class LocaleVaadinInitListener implements VaadinServiceInitListener { if (request == null) { return null; } - + Cookie[] cookies = request.getCookies(); if (cookies == null) { return null; } - + for (Cookie cookie : cookies) { if (LANGUAGE_COOKIE_NAME.equals(cookie.getName())) { return cookie.getValue(); @@ -122,30 +123,30 @@ public class LocaleVaadinInitListener implements VaadinServiceInitListener { if (locale == null) { return null; } - + // Check if the browser locale is supported String language = locale.getLanguage().toLowerCase(); return switch (language) { - case "de" -> Locale.GERMAN; - case "en" -> Locale.ENGLISH; - case "fr" -> Locale.FRENCH; - case "es" -> Locale.of("es", "ES"); - default -> null; // Return null to use Vaadin's default + case "de" -> Locale.GERMAN; + case "en" -> Locale.ENGLISH; + case "fr" -> Locale.FRENCH; + case "es" -> Locale.of("es", "ES"); + default -> null; // Return null to use Vaadin's default }; } private Locale getLocaleFromLanguage(Language language) { return switch (language) { - case DE -> Locale.GERMAN; - case EN -> Locale.ENGLISH; - case FR -> Locale.FRENCH; - case ES -> Locale.of("es", "ES"); - case TR -> Locale.of("tr", "TR"); - case PL -> Locale.of("pl", "PL"); - case RU -> Locale.of("ru", "RU"); - case EE -> Locale.of("et", "EE"); - case LV -> Locale.of("lv", "LV"); - case LT -> Locale.of("lt", "LT"); + case DE -> Locale.GERMAN; + case EN -> Locale.ENGLISH; + case FR -> Locale.FRENCH; + case ES -> Locale.of("es", "ES"); + case TR -> Locale.of("tr", "TR"); + case PL -> Locale.of("pl", "PL"); + case RU -> Locale.of("ru", "RU"); + case EE -> Locale.of("et", "EE"); + case LV -> Locale.of("lv", "LV"); + case LT -> Locale.of("lt", "LT"); }; } @@ -153,19 +154,19 @@ public class LocaleVaadinInitListener implements VaadinServiceInitListener { if (languageCode == null) { return null; } - + return switch (languageCode.toUpperCase()) { - case "DE" -> Locale.GERMAN; - case "EN" -> Locale.ENGLISH; - case "FR" -> Locale.FRENCH; - case "ES" -> Locale.of("es", "ES"); - case "TR" -> Locale.of("tr", "TR"); - case "PL" -> Locale.of("pl", "PL"); - case "RU" -> Locale.of("ru", "RU"); - case "EE" -> Locale.of("et", "EE"); - case "LV" -> Locale.of("lv", "LV"); - case "LT" -> Locale.of("lt", "LT"); - default -> null; + case "DE" -> Locale.GERMAN; + case "EN" -> Locale.ENGLISH; + case "FR" -> Locale.FRENCH; + case "ES" -> Locale.of("es", "ES"); + case "TR" -> Locale.of("tr", "TR"); + case "PL" -> Locale.of("pl", "PL"); + case "RU" -> Locale.of("ru", "RU"); + case "EE" -> Locale.of("et", "EE"); + case "LV" -> Locale.of("lv", "LV"); + case "LT" -> Locale.of("lt", "LT"); + default -> null; }; } } diff --git a/src/main/java/de/assecutor/votianlt/config/TranslationProvider.java b/src/main/java/de/assecutor/votianlt/config/TranslationProvider.java index 7f1e68c..ce4ea7e 100644 --- a/src/main/java/de/assecutor/votianlt/config/TranslationProvider.java +++ b/src/main/java/de/assecutor/votianlt/config/TranslationProvider.java @@ -4,6 +4,7 @@ import com.vaadin.flow.i18n.I18NProvider; import de.assecutor.votianlt.model.Language; import org.springframework.stereotype.Component; +import java.text.MessageFormat; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -15,48 +16,38 @@ import java.util.ResourceBundle.Control; public class TranslationProvider implements I18NProvider { public static final String BUNDLE_PREFIX = "messages"; - + // Custom Control to map language codes to file names private static final Control BUNDLE_CONTROL = new Control() { @Override public List getCandidateLocales(String baseName, Locale locale) { // Map Estonian "et" to "ee" file, Latvian "lv" to "lv", Lithuanian "lt" to "lt" String language = locale.getLanguage(); - String country = locale.getCountry(); - + // Create a locale that matches our file naming convention Locale mappedLocale = switch (language) { - case "et" -> new Locale("ee"); // Estonian -> messages_ee.properties - case "lv" -> new Locale("lv"); // Latvian -> messages_lv.properties - case "lt" -> new Locale("lt"); // Lithuanian -> messages_lt.properties - case "ru" -> new Locale("ru"); // Russian -> messages_ru.properties - case "pl" -> new Locale("pl"); // Polish -> messages_pl.properties - case "tr" -> new Locale("tr"); // Turkish -> messages_tr.properties - case "es" -> new Locale("es"); // Spanish -> messages_es.properties - case "fr" -> new Locale("fr"); // French -> messages_fr.properties - case "en" -> new Locale("en"); // English -> messages_en.properties - case "de" -> new Locale("de"); // German -> messages.properties (default) - default -> locale; + case "et" -> Locale.of("ee"); // Estonian -> messages_ee.properties + case "lv" -> Locale.of("lv"); // Latvian -> messages_lv.properties + case "lt" -> Locale.of("lt"); // Lithuanian -> messages_lt.properties + case "ru" -> Locale.of("ru"); // Russian -> messages_ru.properties + case "pl" -> Locale.of("pl"); // Polish -> messages_pl.properties + case "tr" -> Locale.of("tr"); // Turkish -> messages_tr.properties + case "es" -> Locale.of("es"); // Spanish -> messages_es.properties + case "fr" -> Locale.of("fr"); // French -> messages_fr.properties + case "en" -> Locale.of("en"); // English -> messages_en.properties + case "de" -> Locale.of("de"); // German -> messages.properties (default) + default -> locale; }; - + return super.getCandidateLocales(baseName, mappedLocale); } }; @Override public List getProvidedLocales() { - return Collections.unmodifiableList(Arrays.asList( - Locale.GERMAN, - Locale.ENGLISH, - Locale.FRENCH, - Locale.of("es", "ES"), - Locale.of("tr", "TR"), - Locale.of("pl", "PL"), - Locale.of("ru", "RU"), - Locale.of("et", "EE"), - Locale.of("lv", "LV"), - Locale.of("lt", "LT") - )); + return Collections.unmodifiableList(Arrays.asList(Locale.GERMAN, Locale.ENGLISH, Locale.FRENCH, + Locale.of("es", "ES"), Locale.of("tr", "TR"), Locale.of("pl", "PL"), Locale.of("ru", "RU"), + Locale.of("et", "EE"), Locale.of("lv", "LV"), Locale.of("lt", "LT"))); } @Override @@ -70,7 +61,7 @@ public class TranslationProvider implements I18NProvider { String value = bundle.getString(key); if (params.length > 0) { - value = String.format(value, params); + value = MessageFormat.format(value, params); } return value; @@ -81,16 +72,16 @@ public class TranslationProvider implements I18NProvider { public String getTranslation(String key, Language language) { Locale locale = switch (language) { - case DE -> Locale.GERMAN; - case EN -> Locale.ENGLISH; - case FR -> Locale.FRENCH; - case ES -> Locale.of("es", "ES"); - case TR -> Locale.of("tr", "TR"); - case PL -> Locale.of("pl", "PL"); - case RU -> Locale.of("ru", "RU"); - case EE -> Locale.of("et", "EE"); - case LV -> Locale.of("lv", "LV"); - case LT -> Locale.of("lt", "LT"); + case DE -> Locale.GERMAN; + case EN -> Locale.ENGLISH; + case FR -> Locale.FRENCH; + case ES -> Locale.of("es", "ES"); + case TR -> Locale.of("tr", "TR"); + case PL -> Locale.of("pl", "PL"); + case RU -> Locale.of("ru", "RU"); + case EE -> Locale.of("et", "EE"); + case LV -> Locale.of("lv", "LV"); + case LT -> Locale.of("lt", "LT"); }; return getTranslation(key, locale); } diff --git a/src/main/java/de/assecutor/votianlt/model/task/TaskType.java b/src/main/java/de/assecutor/votianlt/model/task/TaskType.java index e57ffd3..bd1be8e 100644 --- a/src/main/java/de/assecutor/votianlt/model/task/TaskType.java +++ b/src/main/java/de/assecutor/votianlt/model/task/TaskType.java @@ -25,15 +25,15 @@ public enum TaskType { } 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"; + case CONFIRMATION -> "Bestätigung"; + case SIGNATURE -> "Unterschrift"; + case TODOLIST -> "To-Do Liste"; + case PHOTO -> "Foto"; + case BARCODE -> "Barcode"; + case COMMENT -> "Kommentar"; }; } } diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java index 242251b..b390258 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java @@ -109,42 +109,51 @@ public final class MainLayout extends AppLayout { private Component createSideNav() { // Create tree data with hierarchical menu structure TreeData treeData = new TreeData<>(); - + // Root nodes - MenuTreeItem auftragserstellungItem = new MenuTreeItem(getTranslation("nav.job.create"), "add_job", VaadinIcon.PLUS_CIRCLE); - MenuTreeItem nachrichtenItem = new MenuTreeItem(getTranslation("nav.messages"), "messages", VaadinIcon.ENVELOPE); + MenuTreeItem auftragserstellungItem = new MenuTreeItem(getTranslation("nav.job.create"), "add_job", + VaadinIcon.PLUS_CIRCLE); + MenuTreeItem nachrichtenItem = new MenuTreeItem(getTranslation("nav.messages"), "messages", + VaadinIcon.ENVELOPE); MenuTreeItem verwaltungItem = new MenuTreeItem(getTranslation("nav.management"), null, VaadinIcon.COG); MenuTreeItem benutzerItem = new MenuTreeItem(getTranslation("nav.users"), null, VaadinIcon.USER); - + // Store reference to messages item for badge updates messagesTreeItem = nachrichtenItem; - + // Add root items treeData.addItem(null, auftragserstellungItem); treeData.addItem(null, nachrichtenItem); treeData.addItem(null, verwaltungItem); treeData.addItem(null, benutzerItem); - + // Add children to "Verwaltung" - treeData.addItem(verwaltungItem, new MenuTreeItem(getTranslation("nav.customers"), "customers", VaadinIcon.USERS)); - treeData.addItem(verwaltungItem, new MenuTreeItem(getTranslation("nav.appusers"), "app-user", VaadinIcon.USERS)); - treeData.addItem(verwaltungItem, new MenuTreeItem(getTranslation("nav.statistics"), "statistics", VaadinIcon.BAR_CHART)); - + treeData.addItem(verwaltungItem, + new MenuTreeItem(getTranslation("nav.customers"), "customers", VaadinIcon.USERS)); + treeData.addItem(verwaltungItem, + new MenuTreeItem(getTranslation("nav.appusers"), "app-user", VaadinIcon.USERS)); + treeData.addItem(verwaltungItem, + new MenuTreeItem(getTranslation("nav.statistics"), "statistics", VaadinIcon.BAR_CHART)); + // Add invoices only if billing is enabled if (isBillingEnabledForCurrentUser()) { - treeData.addItem(verwaltungItem, new MenuTreeItem(getTranslation("nav.invoices"), "invoices", VaadinIcon.FILE_TEXT)); + treeData.addItem(verwaltungItem, + new MenuTreeItem(getTranslation("nav.invoices"), "invoices", VaadinIcon.FILE_TEXT)); } - + // Add children to "Benutzer" - treeData.addItem(benutzerItem, new MenuTreeItem(getTranslation("nav.profile"), "edit-profile", VaadinIcon.USER)); - treeData.addItem(benutzerItem, new MenuTreeItem(getTranslation("nav.myinvoices"), "my-invoices", VaadinIcon.FILE_TEXT)); - treeData.addItem(benutzerItem, new MenuTreeItem(getTranslation("nav.imprint"), "impressum", VaadinIcon.INFO_CIRCLE)); - + treeData.addItem(benutzerItem, + new MenuTreeItem(getTranslation("nav.profile"), "edit-profile", VaadinIcon.USER)); + treeData.addItem(benutzerItem, + new MenuTreeItem(getTranslation("nav.myinvoices"), "my-invoices", VaadinIcon.FILE_TEXT)); + treeData.addItem(benutzerItem, + new MenuTreeItem(getTranslation("nav.imprint"), "impressum", VaadinIcon.INFO_CIRCLE)); + // Create Tree tree = new TreeGrid<>(); tree.setDataProvider(new TreeDataProvider<>(treeData)); tree.addClassNames(Margin.Horizontal.MEDIUM); - + // Custom item renderer to show icon and label with badge tree.addComponentHierarchyColumn(item -> { HorizontalLayout row = new HorizontalLayout(); @@ -153,7 +162,7 @@ public final class MainLayout extends AppLayout { row.setPadding(false); row.setMargin(false); row.setWidthFull(); - + // Icon if (item.icon() != null) { Icon icon = item.icon().create(); @@ -161,13 +170,13 @@ public final class MainLayout extends AppLayout { icon.getStyle().set("min-width", "var(--lumo-icon-size-s)"); row.add(icon); } - + // Label Span label = new Span(item.label()); label.getStyle().set("font-size", "var(--lumo-font-size-s)"); row.add(label); row.setFlexGrow(1, label); - + // Badge for messages if (item == messagesTreeItem && item.badgeCount() > 0) { Span badge = new Span(String.valueOf(item.badgeCount())); @@ -183,10 +192,10 @@ public final class MainLayout extends AppLayout { badge.getStyle().set("margin-left", "auto"); row.add(badge); } - + return row; }); - + // Handle selection/navigation tree.addSelectionListener(event -> { event.getFirstSelectedItem().ifPresent(item -> { @@ -195,16 +204,16 @@ public final class MainLayout extends AppLayout { } }); }); - + // Expand management and user nodes by default tree.expand(verwaltungItem, benutzerItem); - + // Create container VerticalLayout navContainer = new VerticalLayout(); navContainer.setPadding(false); navContainer.setSpacing(false); navContainer.add(tree); - + return navContainer; } @@ -217,28 +226,22 @@ public final class MainLayout extends AppLayout { } long unreadCount = resolveUnreadMessageCount(); - + // Get current data provider and update the messages item TreeDataProvider dataProvider = (TreeDataProvider) tree.getDataProvider(); TreeData treeData = dataProvider.getTreeData(); - + // Find and update the messages item with new badge count - treeData.getChildren(null).stream() - .filter(item -> "messages".equals(item.path())) - .findFirst() - .ifPresent(oldItem -> { - MenuTreeItem newItem = new MenuTreeItem( - getTranslation("nav.messages"), - "messages", - VaadinIcon.ENVELOPE, - unreadCount - ); - messagesTreeItem = newItem; - // Refresh to show updated badge - dataProvider.refreshAll(); - }); + treeData.getChildren(null).stream().filter(item -> "messages".equals(item.path())).findFirst() + .ifPresent(oldItem -> { + MenuTreeItem newItem = new MenuTreeItem(getTranslation("nav.messages"), "messages", + VaadinIcon.ENVELOPE, unreadCount); + messagesTreeItem = newItem; + // Refresh to show updated badge + dataProvider.refreshAll(); + }); } - + /** * Record representing a menu item in the tree */ @@ -246,15 +249,17 @@ public final class MainLayout extends AppLayout { MenuTreeItem(String label, String path, VaadinIcon icon) { this(label, path, icon, 0); } - + @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; MenuTreeItem that = (MenuTreeItem) o; return Objects.equals(label, that.label) && Objects.equals(path, that.path); } - + @Override public int hashCode() { return Objects.hash(label, path); @@ -307,7 +312,8 @@ public final class MainLayout extends AppLayout { userMenuItem.add(userNameSpan); // Profil anzeigen mit Navigation - userMenuItem.getSubMenu().addItem(getTranslation("nav.showprofile"), e -> UI.getCurrent().navigate(EditProfileView.class)); + userMenuItem.getSubMenu().addItem(getTranslation("nav.showprofile"), + e -> UI.getCurrent().navigate(EditProfileView.class)); userMenuItem.getSubMenu().addItem(getTranslation("nav.settings")); userMenuItem.getSubMenu().addItem(getTranslation("nav.logout"), e -> securityService.logout()); @@ -359,10 +365,10 @@ 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. + * 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 { @@ -386,11 +392,11 @@ public final class MainLayout extends AppLayout { 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"); - default -> Locale.GERMAN; + case DE -> Locale.GERMAN; + case EN -> Locale.ENGLISH; + case FR -> Locale.FRENCH; + case ES -> Locale.of("es", "ES"); + default -> Locale.GERMAN; }; } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java index 2b84d24..33c9012 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java @@ -90,11 +90,13 @@ public class AddAppUserView extends VerticalLayout implements HasDynamicTitle { designationField.setPlaceholder("(HH H 000)"); designationField.setWidthFull(); designationField.setRequiredIndicatorVisible(true); - designationField.addBlurListener(e -> validateField(designationField, getTranslation("addappuser.validation.designation"))); + designationField.addBlurListener( + e -> validateField(designationField, getTranslation("addappuser.validation.designation"))); firstnameField.setWidthFull(); firstnameField.setRequiredIndicatorVisible(true); - firstnameField.addBlurListener(e -> validateField(firstnameField, getTranslation("profile.validation.firstname"))); + firstnameField + .addBlurListener(e -> validateField(firstnameField, getTranslation("profile.validation.firstname"))); lastnameField.setWidthFull(); lastnameField.setRequiredIndicatorVisible(true); @@ -162,8 +164,8 @@ public class AddAppUserView extends VerticalLayout implements HasDynamicTitle { binder.forField(lastnameField).bind(AppUser::getNachname, AppUser::setNachname); binder.forField(phoneField).bind(AppUser::getTelefon, AppUser::setTelefon); binder.forField(emailField).bind(AppUser::getEmail, AppUser::setEmail); - binder.forField(passwordField).asRequired(getTranslation("addappuser.validation.password.required")).bind(AppUser::getPassword, - AppUser::setPassword); + binder.forField(passwordField).asRequired(getTranslation("addappuser.validation.password.required")) + .bind(AppUser::getPassword, AppUser::setPassword); // Confirm password field validation binder.forField(confirmPasswordField).asRequired(getTranslation("addappuser.validation.password.confirm")) @@ -201,13 +203,13 @@ public class AddAppUserView extends VerticalLayout implements HasDynamicTitle { } catch (org.springframework.dao.DuplicateKeyException e) { // Handle duplicate email error if (e.getMessage().contains("email")) { - Notification.show(getTranslation("addappuser.notification.email.duplicate"), 5000, Notification.Position.MIDDLE); + Notification.show(getTranslation("addappuser.notification.email.duplicate"), 5000, + Notification.Position.MIDDLE); emailField.focus(); emailField.setInvalid(true); emailField.setErrorMessage(getTranslation("addappuser.notification.email.duplicate")); } else { - Notification.show(getTranslation("addappuser.notification.check"), 5000, - Notification.Position.MIDDLE); + Notification.show(getTranslation("addappuser.notification.check"), 5000, Notification.Position.MIDDLE); } } catch (Exception e) { Notification.show(getTranslation("addappuser.notification.error", e.getMessage()), 5000, diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java index 66b530f..d5ad1e5 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java @@ -54,7 +54,8 @@ public class AddCustomerView extends Main implements HasDynamicTitle { // Anrede (Dropdown) title = new ComboBox<>(getTranslation("addjob.address.salutation")); - title.setItems(getTranslation("addjob.salutation.mr"), getTranslation("addjob.salutation.ms"), getTranslation("addjob.salutation.other")); + title.setItems(getTranslation("addjob.salutation.mr"), getTranslation("addjob.salutation.ms"), + getTranslation("addjob.salutation.other")); title.setPlaceholder(getTranslation("addjob.address.salutation.placeholder")); title.setWidthFull(); @@ -160,16 +161,16 @@ public class AddCustomerView extends Main implements HasDynamicTitle { } private void configureBinder() { - binder.forField(companyName).asRequired(getTranslation("profile.validation.company.required")).bind(Customer::getCompanyName, - Customer::setCompanyName); + binder.forField(companyName).asRequired(getTranslation("profile.validation.company.required")) + .bind(Customer::getCompanyName, Customer::setCompanyName); binder.forField(title).bind(Customer::getTitle, Customer::setTitle); - binder.forField(firstName).asRequired(getTranslation("profile.validation.firstname.required")).bind(Customer::getFirstname, - Customer::setFirstname); + binder.forField(firstName).asRequired(getTranslation("profile.validation.firstname.required")) + .bind(Customer::getFirstname, Customer::setFirstname); - binder.forField(lastName).asRequired(getTranslation("profile.validation.lastname.required")).bind(Customer::getLastName, - Customer::setLastName); + binder.forField(lastName).asRequired(getTranslation("profile.validation.lastname.required")) + .bind(Customer::getLastName, Customer::setLastName); binder.forField(telephone).asRequired(getTranslation("profile.validation.phone")).bind(Customer::getTelephone, Customer::setTelephone); @@ -180,16 +181,19 @@ public class AddCustomerView extends Main implements HasDynamicTitle { .withValidator(email -> email.contains("@"), getTranslation("profile.validation.email.invalid")) .bind(Customer::getMail, Customer::setMail); - binder.forField(street).asRequired(getTranslation("profile.validation.street.required")).bind(Customer::getStreet, Customer::setStreet); + binder.forField(street).asRequired(getTranslation("profile.validation.street.required")) + .bind(Customer::getStreet, Customer::setStreet); - binder.forField(houseNumber).asRequired(getTranslation("profile.validation.housenr.required")).bind(Customer::getHouseNumber, - Customer::setHouseNumber); + binder.forField(houseNumber).asRequired(getTranslation("profile.validation.housenr.required")) + .bind(Customer::getHouseNumber, Customer::setHouseNumber); binder.forField(addressAddition).bind(Customer::getAddressAddition, Customer::setAddressAddition); - binder.forField(zip).asRequired(getTranslation("profile.validation.zip.required")).bind(Customer::getZip, Customer::setZip); + binder.forField(zip).asRequired(getTranslation("profile.validation.zip.required")).bind(Customer::getZip, + Customer::setZip); - binder.forField(city).asRequired(getTranslation("profile.validation.city.required")).bind(Customer::getCity, Customer::setCity); + binder.forField(city).asRequired(getTranslation("profile.validation.city.required")).bind(Customer::getCity, + Customer::setCity); } private void setTestData() { @@ -201,7 +205,8 @@ public class AddCustomerView extends Main implements HasDynamicTitle { boolean isValid = validateAllFields(); if (!isValid) { - com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.validation"), 3000, + com.vaadin.flow.component.notification.Notification.show( + getTranslation("addcustomer.notification.validation"), 3000, com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER); return; } @@ -212,16 +217,17 @@ public class AddCustomerView extends Main implements HasDynamicTitle { addCustomerService.addCustomer(customer); - com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.success"), 3000, - com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER); + com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.success"), + 3000, com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER); getUI().ifPresent(ui -> ui.navigate("customers")); } catch (ValidationException e) { - com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.check"), 3000, - com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER); + com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.check"), + 3000, com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER); } catch (Exception e) { - com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.error", 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); } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java index 336541f..6b95b1d 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -131,7 +131,7 @@ public class AddJobView extends Main implements HasDynamicTitle { private VerticalLayout routeInfoBox; private Span routeDistanceLabel; private Span routeDurationLabel; - + // Manuelle Eingabe für Route (wenn keine Berechnung erfolgt) private VerticalLayout manualRouteInputBox; private com.vaadin.flow.component.textfield.NumberField manualDistanceInput; @@ -320,7 +320,8 @@ public class AddJobView extends Main implements HasDynamicTitle { pickupCompany.setAllowCustomValue(true); setupCompanyAutocomplete(pickupCompany, true); // true für Pickup pickupSalutation = new ComboBox<>(getTranslation("addjob.address.salutation")); - pickupSalutation.setItems(getTranslation("addjob.salutation.mr"), getTranslation("addjob.salutation.ms"), getTranslation("addjob.salutation.other")); + pickupSalutation.setItems(getTranslation("addjob.salutation.mr"), getTranslation("addjob.salutation.ms"), + getTranslation("addjob.salutation.other")); pickupSalutation.setPlaceholder(getTranslation("addjob.address.salutation.placeholder")); pickupFirstName = new TextField(getTranslation("profile.firstname")); pickupFirstName.setPlaceholder(getTranslation("profile.firstname")); @@ -353,7 +354,8 @@ public class AddJobView extends Main implements HasDynamicTitle { deliveryCompany.setAllowCustomValue(true); setupCompanyAutocomplete(deliveryCompany, false); // false für Delivery deliverySalutation = new ComboBox<>(getTranslation("addjob.address.salutation")); - deliverySalutation.setItems(getTranslation("addjob.salutation.mr"), getTranslation("addjob.salutation.ms"), getTranslation("addjob.salutation.other")); + deliverySalutation.setItems(getTranslation("addjob.salutation.mr"), getTranslation("addjob.salutation.ms"), + getTranslation("addjob.salutation.other")); deliverySalutation.setPlaceholder(getTranslation("addjob.address.salutation.placeholder")); deliveryFirstName = new TextField(getTranslation("profile.firstname")); deliveryFirstName.setPlaceholder(getTranslation("profile.firstname")); @@ -678,7 +680,8 @@ public class AddJobView extends Main implements HasDynamicTitle { manualInputRow.setWidthFull(); manualInputRow.setSpacing(true); - manualDistanceInput = new com.vaadin.flow.component.textfield.NumberField(getTranslation("addjob.route.distance.km")); + manualDistanceInput = new com.vaadin.flow.component.textfield.NumberField( + getTranslation("addjob.route.distance.km")); manualDistanceInput.setWidthFull(); manualDistanceInput.setPlaceholder(getTranslation("addjob.route.distance.placeholder")); manualDistanceInput.setMin(0); @@ -692,7 +695,8 @@ public class AddJobView extends Main implements HasDynamicTitle { } }); - manualDurationInput = new com.vaadin.flow.component.textfield.IntegerField(getTranslation("addjob.route.duration.min")); + manualDurationInput = new com.vaadin.flow.component.textfield.IntegerField( + getTranslation("addjob.route.duration.min")); manualDurationInput.setWidthFull(); manualDurationInput.setPlaceholder(getTranslation("addjob.route.duration.placeholder")); manualDurationInput.setMin(0); @@ -740,7 +744,8 @@ public class AddJobView extends Main implements HasDynamicTitle { } // Show price info if no route calculated yet if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE && routeDistance == null) { - return service.getPricePerKilometer().setScale(2, RoundingMode.HALF_UP) + " €/km (" + getTranslation("addjob.services.route.missing") + ")"; + return service.getPricePerKilometer().setScale(2, RoundingMode.HALF_UP) + " €/km (" + + getTranslation("addjob.services.route.missing") + ")"; } return service.getEffectivePrice() != null ? service.getEffectivePrice().setScale(2, RoundingMode.HALF_UP) + " €" @@ -937,7 +942,8 @@ public class AddJobView extends Main implements HasDynamicTitle { if (service.getPricePer15Minutes() != null && durationSeconds != null && durationSeconds > 0) { // Dauer in 15-Minuten-Einheiten umrechnen (aufrunden) int units = durationSeconds / 900; // 900 Sekunden = 15 Minuten - if (durationSeconds % 900 > 0) units++; // Aufrunden + if (durationSeconds % 900 > 0) + units++; // Aufrunden return service.getPricePer15Minutes().multiply(BigDecimal.valueOf(units)); } return BigDecimal.ZERO; @@ -1245,8 +1251,7 @@ public class AddJobView extends Main implements HasDynamicTitle { boolean digital = Boolean.TRUE.equals(digitalProcessing.getValue()); boolean hasUser = selectedUserId != null && !selectedUserId.trim().isEmpty(); return !digital || hasUser; - }, getTranslation("addjob.validation.appuser.required")) - .bind(Job::getAppUser, Job::setAppUser); + }, getTranslation("addjob.validation.appuser.required")).bind(Job::getAppUser, Job::setAppUser); // Toggle required indicator and visibility for App-Nutzer based on // digitalProcessing @@ -1515,7 +1520,7 @@ public class AddJobView extends Main implements HasDynamicTitle { .map(s -> calculateServicePrice(s, routeDistance, durationSeconds)) .reduce(BigDecimal.ZERO, BigDecimal::add); job.setPrice(netTotal); - + // Store route distance and duration in job for invoice creation if (routeCalculationResult != null && routeCalculationResult.isValid()) { // Berechnete Route verwenden @@ -1525,14 +1530,15 @@ public class AddJobView extends Main implements HasDynamicTitle { // Manuelle Eingabe verwenden job.setRouteDistanceKm(manualDistanceInput.getValue()); if (manualDurationInput != null && manualDurationInput.getValue() != null) { - job.setRouteDurationSeconds(manualDurationInput.getValue() * 60); // Minuten in Sekunden umrechnen + job.setRouteDurationSeconds(manualDurationInput.getValue() * 60); // Minuten in Sekunden + // umrechnen } } // Additional validation: If digital processing is enabled, app user must be // selected if (digitalProcessing.getValue() && appUser.getValue() == null) { - Notification errorNotification = Notification.show( - getTranslation("addjob.validation.appuser.required")); + Notification errorNotification = Notification + .show(getTranslation("addjob.validation.appuser.required")); errorNotification.setDuration(5000); return; } @@ -1600,8 +1606,7 @@ public class AddJobView extends Main implements HasDynamicTitle { getUI().ifPresent(ui -> ui.navigate(JobSummaryView.class, savedJob.getId().toHexString())); } else { // Validation failed, show error message - Notification errorNotification = Notification - .show(getTranslation("addjob.validation.required.fields")); + Notification errorNotification = Notification.show(getTranslation("addjob.validation.required.fields")); errorNotification.setDuration(5000); } @@ -1612,7 +1617,8 @@ public class AddJobView extends Main implements HasDynamicTitle { cargoError.setVisible(false); if (cargoAreaContainer != null) cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - Notification errorNotification = Notification.show(getTranslation("addjob.notification.error", e.getMessage())); + Notification errorNotification = Notification + .show(getTranslation("addjob.notification.error", e.getMessage())); errorNotification.setDuration(5000); } } @@ -1659,8 +1665,7 @@ public class AddJobView extends Main implements HasDynamicTitle { loadJobIntoForm(draft); // Benutzer informieren - Notification notification = Notification - .show(getTranslation("addjob.notification.draft.restored")); + Notification notification = Notification.show(getTranslation("addjob.notification.draft.restored")); notification.setDuration(4000); } } @@ -1712,7 +1717,9 @@ public class AddJobView extends Main implements HasDynamicTitle { row.setAlignItems(FlexComponent.Alignment.END); ComboBox desc = new ComboBox<>(getTranslation("addjob.cargo.description")); - desc.setItems(getTranslation("addjob.cargo.europalette"), getTranslation("addjob.cargo.disposablepalette"), getTranslation("addjob.cargo.dusseldorfpalette"), getTranslation("addjob.cargo.gridboxpalette"), getTranslation("addjob.cargo.gridcart"), getTranslation("addjob.cargo.parcel")); + desc.setItems(getTranslation("addjob.cargo.europalette"), getTranslation("addjob.cargo.disposablepalette"), + getTranslation("addjob.cargo.dusseldorfpalette"), getTranslation("addjob.cargo.gridboxpalette"), + getTranslation("addjob.cargo.gridcart"), getTranslation("addjob.cargo.parcel")); desc.setAllowCustomValue(true); desc.setPlaceholder(getTranslation("addjob.cargo.description.placeholder")); desc.setWidth("40%"); @@ -2510,7 +2517,8 @@ public class AddJobView extends Main implements HasDynamicTitle { try { // Check if there are any tasks to save if (tasksState.isEmpty()) { - Notification.show(getTranslation("addjob.tasks.template.no.tasks"), 3000, Notification.Position.BOTTOM_END); + Notification.show(getTranslation("addjob.tasks.template.no.tasks"), 3000, + Notification.Position.BOTTOM_END); return; } @@ -2577,7 +2585,8 @@ public class AddJobView extends Main implements HasDynamicTitle { } catch (Exception e) { log.error("Error opening save template dialog", e); - Notification.show(getTranslation("addjob.tasks.template.dialog.error", e.getMessage()), 4000, Notification.Position.MIDDLE); + Notification.show(getTranslation("addjob.tasks.template.dialog.error", e.getMessage()), 4000, + Notification.Position.MIDDLE); } } @@ -2644,7 +2653,8 @@ public class AddJobView extends Main implements HasDynamicTitle { templateComboBox.setItems(templates); } catch (Exception e) { log.error("Error loading templates", e); - Notification.show(getTranslation("addjob.tasks.template.load.templates.error", e.getMessage()), 4000, Notification.Position.MIDDLE); + Notification.show(getTranslation("addjob.tasks.template.load.templates.error", e.getMessage()), 4000, + Notification.Position.MIDDLE); } } @@ -3075,10 +3085,12 @@ public class AddJobView extends Main implements HasDynamicTitle { // Abholadresse anzeigen if (pickupResult != null) { if (pickupResult.isValid()) { - pickupResultLabel.setText("✓ " + getTranslation("addjob.validation.pickup.address") + ": " + pickupResult.getFormattedAddress()); + pickupResultLabel.setText("✓ " + getTranslation("addjob.validation.pickup.address") + ": " + + pickupResult.getFormattedAddress()); pickupResultLabel.getStyle().set("color", "var(--lumo-success-text-color)"); } else { - pickupResultLabel.setText("⚠ " + getTranslation("addjob.validation.pickup.address") + ": " + pickupResult.getValidationMessage()); + pickupResultLabel.setText("⚠ " + getTranslation("addjob.validation.pickup.address") + ": " + + pickupResult.getValidationMessage()); pickupResultLabel.getStyle().set("color", "var(--lumo-error-text-color)"); hasInvalidAddress = true; bothAddressesValid = false; @@ -3090,10 +3102,12 @@ public class AddJobView extends Main implements HasDynamicTitle { // Lieferadresse anzeigen if (deliveryResult != null) { if (deliveryResult.isValid()) { - deliveryResultLabel.setText("✓ " + getTranslation("addjob.validation.delivery.address") + ": " + deliveryResult.getFormattedAddress()); + deliveryResultLabel.setText("✓ " + getTranslation("addjob.validation.delivery.address") + ": " + + deliveryResult.getFormattedAddress()); deliveryResultLabel.getStyle().set("color", "var(--lumo-success-text-color)"); } else { - deliveryResultLabel.setText("⚠ " + getTranslation("addjob.validation.delivery.address") + ": " + deliveryResult.getValidationMessage()); + deliveryResultLabel.setText("⚠ " + getTranslation("addjob.validation.delivery.address") + ": " + + deliveryResult.getValidationMessage()); deliveryResultLabel.getStyle().set("color", "var(--lumo-error-text-color)"); hasInvalidAddress = true; bothAddressesValid = false; @@ -3104,8 +3118,10 @@ public class AddJobView extends Main implements HasDynamicTitle { // Route anzeigen, wenn beide Adressen gültig sind if (bothAddressesValid && routeCalculationResult != null && routeCalculationResult.isValid()) { - routeResultLabel.setText("🚛 " + getTranslation("addjob.validation.route") + ": " + String.format("%.1f km", routeCalculationResult.getDistanceKm()) - + " (" + getTranslation("addjob.route.duration") + ": " + routeCalculationResult.getFormattedDurationLong() + ")"); + routeResultLabel.setText("🚛 " + getTranslation("addjob.validation.route") + ": " + + String.format("%.1f km", routeCalculationResult.getDistanceKm()) + " (" + + getTranslation("addjob.route.duration") + ": " + routeCalculationResult.getFormattedDurationLong() + + ")"); routeResultLabel.getStyle().set("color", "var(--lumo-primary-text-color)"); routeResultLabel.setVisible(true); } else { @@ -3234,9 +3250,11 @@ public class AddJobView extends Main implements HasDynamicTitle { } /** - * Gibt die berechnete oder manuell eingegebene Entfernung zurück (für Preisberechnungen). + * Gibt die berechnete oder manuell eingegebene Entfernung zurück (für + * Preisberechnungen). * - * @return Entfernung in km oder 0.0, wenn keine Berechnung oder Eingabe vorhanden ist + * @return Entfernung in km oder 0.0, wenn keine Berechnung oder Eingabe + * vorhanden ist */ public double getRouteDistanceKm() { if (routeCalculationResult != null && routeCalculationResult.isValid()) { diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AdminDashboardView.java b/src/main/java/de/assecutor/votianlt/pages/view/AdminDashboardView.java index be3283a..c8ee561 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AdminDashboardView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AdminDashboardView.java @@ -148,19 +148,23 @@ public class AdminDashboardView extends Main implements HasDynamicTitle { // Total jobs card long totalJobs = jobRepository.count(); - cards.add(createStatCard(getTranslation("admindashboard.stat.totaljobs"), String.valueOf(totalJobs), VaadinIcon.PACKAGE, "blue")); + cards.add(createStatCard(getTranslation("admindashboard.stat.totaljobs"), String.valueOf(totalJobs), + VaadinIcon.PACKAGE, "blue")); // Total users card long totalUsers = userRepository.count(); - cards.add(createStatCard(getTranslation("admindashboard.stat.users"), String.valueOf(totalUsers), VaadinIcon.USERS, "green")); + cards.add(createStatCard(getTranslation("admindashboard.stat.users"), String.valueOf(totalUsers), + VaadinIcon.USERS, "green")); // Total app users card long totalAppUsers = appUserRepository.count(); - cards.add(createStatCard(getTranslation("admindashboard.stat.appusers"), String.valueOf(totalAppUsers), VaadinIcon.MOBILE, "purple")); + cards.add(createStatCard(getTranslation("admindashboard.stat.appusers"), String.valueOf(totalAppUsers), + VaadinIcon.MOBILE, "purple")); // Current time String currentTime = DateTimeFormatUtil.formatDateTime(LocalDateTime.now()); - cards.add(createStatCard(getTranslation("admindashboard.stat.lastupdated"), currentTime, VaadinIcon.CLOCK, "gray")); + cards.add(createStatCard(getTranslation("admindashboard.stat.lastupdated"), currentTime, VaadinIcon.CLOCK, + "gray")); section.add(title, cards); return section; @@ -185,17 +189,22 @@ public class AdminDashboardView extends Main implements HasDynamicTitle { long inProgressJobs = jobRepository.countByStatus(JobStatus.IN_PROGRESS); long completedJobs = jobRepository.countByStatus(JobStatus.COMPLETED); - cards.add(createStatCard(getTranslation("admindashboard.stat.openjobs"), String.valueOf(openJobs), VaadinIcon.HOURGLASS_START, "orange")); - cards.add(createStatCard(getTranslation("admindashboard.stat.inprogress"), String.valueOf(inProgressJobs), VaadinIcon.PLAY, "blue")); - cards.add(createStatCard(getTranslation("admindashboard.stat.completed"), String.valueOf(completedJobs), VaadinIcon.CHECK_CIRCLE, "green")); + cards.add(createStatCard(getTranslation("admindashboard.stat.openjobs"), String.valueOf(openJobs), + VaadinIcon.HOURGLASS_START, "orange")); + cards.add(createStatCard(getTranslation("admindashboard.stat.inprogress"), String.valueOf(inProgressJobs), + VaadinIcon.PLAY, "blue")); + cards.add(createStatCard(getTranslation("admindashboard.stat.completed"), String.valueOf(completedJobs), + VaadinIcon.CHECK_CIRCLE, "green")); // Total cargo items long totalCargoItems = cargoItemRepository.count(); - cards.add(createStatCard(getTranslation("admindashboard.stat.cargo"), String.valueOf(totalCargoItems), VaadinIcon.CUBE, "purple")); + cards.add(createStatCard(getTranslation("admindashboard.stat.cargo"), String.valueOf(totalCargoItems), + VaadinIcon.CUBE, "purple")); } catch (Exception e) { log.warn("Could not load job statistics by status", e); - cards.add(createStatCard(getTranslation("admindashboard.stat.status.info"), getTranslation("admindashboard.stat.status.unavailable"), VaadinIcon.WARNING, "red")); + cards.add(createStatCard(getTranslation("admindashboard.stat.status.info"), + getTranslation("admindashboard.stat.status.unavailable"), VaadinIcon.WARNING, "red")); } section.add(title, cards); @@ -217,20 +226,23 @@ public class AdminDashboardView extends Main implements HasDynamicTitle { // Total tasks long totalTasks = taskRepository.count(); - cards.add(createStatCard(getTranslation("admindashboard.stat.totaltasks"), String.valueOf(totalTasks), VaadinIcon.TASKS, "blue")); + cards.add(createStatCard(getTranslation("admindashboard.stat.totaltasks"), String.valueOf(totalTasks), + VaadinIcon.TASKS, "blue")); // Completed tasks long completedTasks = taskRepository.countByCompleted(true); - cards.add(createStatCard(getTranslation("admindashboard.stat.completedtasks"), String.valueOf(completedTasks), VaadinIcon.CHECK, "green")); + cards.add(createStatCard(getTranslation("admindashboard.stat.completedtasks"), String.valueOf(completedTasks), + VaadinIcon.CHECK, "green")); // Pending tasks long pendingTasks = totalTasks - completedTasks; - cards.add(createStatCard(getTranslation("admindashboard.stat.pendingtasks"), String.valueOf(pendingTasks), VaadinIcon.CLOCK, "orange")); + cards.add(createStatCard(getTranslation("admindashboard.stat.pendingtasks"), String.valueOf(pendingTasks), + VaadinIcon.CLOCK, "orange")); // Completion rate double completionRate = totalTasks > 0 ? (completedTasks * 100.0 / totalTasks) : 0; - cards.add(createStatCard(getTranslation("admindashboard.stat.successrate"), String.format("%.1f%%", completionRate), VaadinIcon.TRENDING_UP, - "purple")); + cards.add(createStatCard(getTranslation("admindashboard.stat.successrate"), + String.format("%.1f%%", completionRate), VaadinIcon.TRENDING_UP, "purple")); section.add(title, cards); return section; @@ -251,16 +263,20 @@ public class AdminDashboardView extends Main implements HasDynamicTitle { // Content statistics long totalPhotos = photoRepository.count(); - cards.add(createStatCard(getTranslation("admindashboard.stat.photos"), String.valueOf(totalPhotos), VaadinIcon.CAMERA, "blue")); + cards.add(createStatCard(getTranslation("admindashboard.stat.photos"), String.valueOf(totalPhotos), + VaadinIcon.CAMERA, "blue")); long totalBarcodes = barcodeRepository.count(); - cards.add(createStatCard(getTranslation("admindashboard.stat.barcodes"), String.valueOf(totalBarcodes), VaadinIcon.BARCODE, "green")); + cards.add(createStatCard(getTranslation("admindashboard.stat.barcodes"), String.valueOf(totalBarcodes), + VaadinIcon.BARCODE, "green")); long totalSignatures = signatureRepository.count(); - cards.add(createStatCard(getTranslation("admindashboard.stat.signatures"), String.valueOf(totalSignatures), VaadinIcon.EDIT, "purple")); + cards.add(createStatCard(getTranslation("admindashboard.stat.signatures"), String.valueOf(totalSignatures), + VaadinIcon.EDIT, "purple")); long totalComments = commentRepository.count(); - cards.add(createStatCard(getTranslation("admindashboard.stat.comments"), String.valueOf(totalComments), VaadinIcon.COMMENT, "orange")); + cards.add(createStatCard(getTranslation("admindashboard.stat.comments"), String.valueOf(totalComments), + VaadinIcon.COMMENT, "orange")); section.add(title, cards); return section; @@ -282,16 +298,20 @@ public class AdminDashboardView extends Main implements HasDynamicTitle { // Database connection status try { userRepository.count(); // Test database connection - cards.add(createStatCard(getTranslation("admindashboard.stat.database"), getTranslation("admindashboard.stat.database.connected"), VaadinIcon.DATABASE, "green")); + cards.add(createStatCard(getTranslation("admindashboard.stat.database"), + getTranslation("admindashboard.stat.database.connected"), VaadinIcon.DATABASE, "green")); } catch (Exception e) { - cards.add(createStatCard(getTranslation("admindashboard.stat.database"), getTranslation("admindashboard.stat.database.error"), VaadinIcon.DATABASE, "red")); + cards.add(createStatCard(getTranslation("admindashboard.stat.database"), + getTranslation("admindashboard.stat.database.error"), VaadinIcon.DATABASE, "red")); } // Messaging status - cards.add(createStatCard(getTranslation("admindashboard.stat.websocket"), getTranslation("admindashboard.stat.websocket.active"), VaadinIcon.CONNECT, "green")); + cards.add(createStatCard(getTranslation("admindashboard.stat.websocket"), + getTranslation("admindashboard.stat.websocket.active"), VaadinIcon.CONNECT, "green")); // System uptime (placeholder) - cards.add(createStatCard(getTranslation("admindashboard.stat.app"), getTranslation("admindashboard.stat.app.running"), VaadinIcon.HEART, "green")); + cards.add(createStatCard(getTranslation("admindashboard.stat.app"), + getTranslation("admindashboard.stat.app.running"), VaadinIcon.HEART, "green")); // Memory usage (placeholder) Runtime runtime = Runtime.getRuntime(); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java b/src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java index 81e56a6..71e9f9e 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java @@ -75,9 +75,11 @@ public class AdminPricetableView extends VerticalLayout implements HasDynamicTit priceTable.setRevenueParticipation(revenueParticipation.getValue()); priceTableRepository.save(priceTable); - Notification.show(getTranslation("adminpricetable.notification.saved"), 3000, Notification.Position.BOTTOM_CENTER); + Notification.show(getTranslation("adminpricetable.notification.saved"), 3000, + Notification.Position.BOTTOM_CENTER); } catch (Exception ex) { - Notification.show(getTranslation("adminpricetable.notification.save.error", ex.getMessage()), 5000, Notification.Position.BOTTOM_CENTER); + Notification.show(getTranslation("adminpricetable.notification.save.error", ex.getMessage()), 5000, + Notification.Position.BOTTOM_CENTER); } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java b/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java index b5ed569..f595d85 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java @@ -52,11 +52,15 @@ public class AppUserView extends VerticalLayout implements HasDynamicTitle { appUserGrid.setSizeFull(); // Grid-Spalten konfigurieren - appUserGrid.addColumn(AppUser::getBezeichnung).setHeader(getTranslation("appuser.column.designation")).setAutoWidth(true); - appUserGrid.addColumn(AppUser::getVorname).setHeader(getTranslation("appuser.column.firstname")).setAutoWidth(true); - appUserGrid.addColumn(AppUser::getNachname).setHeader(getTranslation("appuser.column.lastname")).setAutoWidth(true); + appUserGrid.addColumn(AppUser::getBezeichnung).setHeader(getTranslation("appuser.column.designation")) + .setAutoWidth(true); + appUserGrid.addColumn(AppUser::getVorname).setHeader(getTranslation("appuser.column.firstname")) + .setAutoWidth(true); + appUserGrid.addColumn(AppUser::getNachname).setHeader(getTranslation("appuser.column.lastname")) + .setAutoWidth(true); appUserGrid.addColumn(AppUser::getTelefon).setHeader(getTranslation("appuser.column.phone")).setAutoWidth(true); - appUserGrid.addColumn(AppUser::getAppCode).setHeader(getTranslation("appuser.column.appcode")).setAutoWidth(true); + appUserGrid.addColumn(AppUser::getAppCode).setHeader(getTranslation("appuser.column.appcode")) + .setAutoWidth(true); appUserGrid.addColumn(AppUser::getEmail).setHeader(getTranslation("appuser.column.email")).setAutoWidth(true); // Make grid rows clickable diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java b/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java index 65f63ab..64b24a0 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java @@ -106,14 +106,11 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic // Feature Cards featuresGrid.add( - createFeatureCard(VaadinIcon.COG, - getTranslation("dashboard.feature.setup.title"), + createFeatureCard(VaadinIcon.COG, getTranslation("dashboard.feature.setup.title"), getTranslation("dashboard.feature.setup.desc")), - createFeatureCard(VaadinIcon.USERS, - getTranslation("dashboard.feature.customers.title"), + createFeatureCard(VaadinIcon.USERS, getTranslation("dashboard.feature.customers.title"), getTranslation("dashboard.feature.customers.desc")), - createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, - getTranslation("dashboard.feature.jobs.title"), + createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, getTranslation("dashboard.feature.jobs.title"), getTranslation("dashboard.feature.jobs.desc"))); systemSection.add(systemTitle, systemIntro, featuresGrid); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java index ac0a870..0f27812 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java @@ -188,11 +188,14 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter jobInfo.setSpacing(true); jobInfo.setWidthFull(); - jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.jobnumber")), new Span(currentJob.getJobNumber()))); + jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.jobnumber")), + new Span(currentJob.getJobNumber()))); jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.customer")), new Span(extractCompanyName(currentJob.getCustomerSelection())))); - jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.status")), new Span(currentJob.getStatus().toString()))); - jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.price")), new Span(currentJob.getPrice() + " €"))); + jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.status")), + new Span(currentJob.getStatus().toString()))); + jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.price")), + new Span(currentJob.getPrice() + " €"))); section.add(jobInfo); return section; @@ -307,11 +310,10 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter // Show only net sum, VAT sums, and total amount without individual services summaryInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.summary.net")), new Span(netAmount.setScale(2, RoundingMode.HALF_UP) + " €"))); - summaryInfo - .add(new HorizontalLayout( - new Span(getTranslation("createinvoice.summary.vat", - vatRate.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).toString())), - new Span(vatAmount.setScale(2, RoundingMode.HALF_UP) + " €"))); + summaryInfo.add(new HorizontalLayout( + new Span(getTranslation("createinvoice.summary.vat", + vatRate.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).toString())), + new Span(vatAmount.setScale(2, RoundingMode.HALF_UP) + " €"))); summaryInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.summary.total")), new Span(totalAmount.setScale(2, RoundingMode.HALF_UP) + " €"))); @@ -398,7 +400,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter private void createInvoice() { if (getSelectedServices().isEmpty()) { - Notification.show(getTranslation("createinvoice.notification.noservices"), 3000, Notification.Position.BOTTOM_END); + Notification.show(getTranslation("createinvoice.notification.noservices"), 3000, + Notification.Position.BOTTOM_END); return; } @@ -409,43 +412,52 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter // Get current user Optional currentUserOpt = securityService.getAuthenticatedUser() .flatMap(auth -> userRepository.findByEmail(auth.getUsername())); - + if (currentUserOpt.isEmpty()) { - Notification.show(getTranslation("createinvoice.notification.nouser"), 3000, Notification.Position.BOTTOM_END); + Notification.show(getTranslation("createinvoice.notification.nouser"), 3000, + Notification.Position.BOTTOM_END); return; } - + currentUser = currentUserOpt.get(); - + // Load invoice template from service - Optional templateOpt = invoiceTemplateService.getTemplateByUserId(currentUser.getId().toString()); + Optional templateOpt = invoiceTemplateService + .getTemplateByUserId(currentUser.getId().toString()); if (templateOpt.isEmpty()) { - Notification.show(getTranslation("createinvoice.notification.notemplate"), 3000, Notification.Position.BOTTOM_END); + Notification.show(getTranslation("createinvoice.notification.notemplate"), 3000, + Notification.Position.BOTTOM_END); return; } String templateData = templateOpt.get().getTemplateData(); - System.out.println("DEBUG CreateInvoiceView: Template data length: " + (templateData != null ? templateData.length() : 0)); - System.out.println("DEBUG CreateInvoiceView: Template data preview: " + (templateData != null ? templateData.substring(0, Math.min(200, templateData.length())) : "null")); + System.out.println("DEBUG CreateInvoiceView: Template data length: " + + (templateData != null ? templateData.length() : 0)); + System.out.println("DEBUG CreateInvoiceView: Template data preview: " + + (templateData != null ? templateData.substring(0, Math.min(200, templateData.length())) + : "null")); if (templateData == null || templateData.isBlank()) { - Notification.show(getTranslation("createinvoice.notification.notemplate"), 3000, Notification.Position.BOTTOM_END); + Notification.show(getTranslation("createinvoice.notification.notemplate"), 3000, + Notification.Position.BOTTOM_END); return; } - + // Generate PDF with template and actual job data byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, currentUser); - System.out.println("DEBUG CreateInvoiceView: PDF bytes generated: " + (pdfBytes != null ? pdfBytes.length : 0)); - + System.out.println( + "DEBUG CreateInvoiceView: PDF bytes generated: " + (pdfBytes != null ? pdfBytes.length : 0)); + // Show PDF in dialog showPdfInDialog(pdfBytes, "Rechnung " + currentJob.getJobNumber()); - + } catch (Exception ex) { log.error("Fehler beim Erstellen der Rechnung", ex); - Notification.show(getTranslation("createinvoice.notification.error", ex.getMessage()), 5000, Notification.Position.BOTTOM_END); + Notification.show(getTranslation("createinvoice.notification.error", ex.getMessage()), 5000, + Notification.Position.BOTTOM_END); } } - + /** * Generates an invoice PDF from the template with actual job data. */ @@ -455,10 +467,10 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter BigDecimal vatRate = calculateAverageVatRate(); BigDecimal vatAmount = netAmount.multiply(vatRate); BigDecimal totalAmount = netAmount.add(vatAmount); - + // Build variable substitution map Map variables = new HashMap<>(); - + // Master data (from user profile) variables.put("masterdata.company_name", safe(currentUser.getCompany())); variables.put("masterdata.contact_name", safe(currentUser.getFirstname()) + " " + safe(currentUser.getName())); @@ -466,7 +478,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter variables.put("masterdata.city", safe(currentUser.getZip()) + " " + safe(currentUser.getCity())); variables.put("masterdata.email", safe(currentUser.getEmail())); variables.put("masterdata.phone", safe(currentUser.getPhone())); - + // Customer data (from job) String customerSelection = currentJob.getCustomerSelection(); if (customerSelection != null && !customerSelection.isBlank()) { @@ -474,7 +486,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter String[] parts = customerSelection.split("\\|"); String companyName = parts.length > 0 ? parts[0].trim() : ""; variables.put("customer.company_name", companyName); - + // Extract other customer info if available if (parts.length > 1) { String remaining = parts[1].trim(); @@ -502,28 +514,32 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter variables.put("customer.street", ""); variables.put("customer.city", ""); } - + // Invoice data variables.put("invoice.number", currentJob.getJobNumber() + "-" + System.currentTimeMillis()); variables.put("invoice.date", java.time.LocalDate.now().toString()); - variables.put("invoice.net_total", netAmount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €"); - variables.put("invoice.vat_total", vatAmount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €"); - variables.put("invoice.gross_total", totalAmount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €"); - variables.put("invoice.vat_rate", vatRate.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + "%"); - + variables.put("invoice.net_total", + netAmount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €"); + variables.put("invoice.vat_total", + vatAmount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €"); + variables.put("invoice.gross_total", + totalAmount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €"); + variables.put("invoice.vat_rate", + vatRate.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + "%"); + // Job data if (currentJob.getRouteDistanceKm() != null) { variables.put("job.distance_km", String.format("%.1f", currentJob.getRouteDistanceKm())); } else { variables.put("job.distance_km", "-"); } - + // Services data - add as JSON array for the template List> servicesData = new ArrayList<>(); for (Service service : getSelectedServices()) { Map serviceData = new HashMap<>(); serviceData.put("name", service.getName()); - + // Calculate price based on calculation basis BigDecimal price = calculateServicePrice(service); if (price != null) { @@ -531,17 +547,18 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter } else { serviceData.put("netAmount", "0,00"); } - + // VAT rate if (service.getVatRate() != null) { - serviceData.put("vatRate", service.getVatRate().multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + "%"); + serviceData.put("vatRate", + service.getVatRate().multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + "%"); } else { serviceData.put("vatRate", "19%"); } - + servicesData.add(serviceData); } - + // Serialize services data to JSON and store in variables if (!servicesData.isEmpty()) { com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); @@ -551,25 +568,25 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter variables.put("services.json", "[]"); variables.put("services.count", "0"); } - + // Generate PDF using CustomerInvoiceService return customerInvoiceService.generatePdfFromCanvasTemplateWithData(templateData, variables, currentUser); } - + private String safe(String value) { return value != null ? value : ""; } - + private void showPdfInDialog(byte[] pdfBytes, String title) { // Create a stream resource for the PDF - StreamResource resource = new StreamResource(title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf", + StreamResource resource = new StreamResource(title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf", () -> new java.io.ByteArrayInputStream(pdfBytes)); resource.setContentType("application/pdf"); resource.setCacheTime(0); // Store resource in session for access VaadinSession.getCurrent().setAttribute("currentInvoicePdf", resource); - + // Create dialog Dialog pdfDialog = new Dialog(); pdfDialog.setHeaderTitle(title); @@ -580,7 +597,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter IFrame pdfFrame = new IFrame(); pdfFrame.setWidth("100%"); pdfFrame.setHeight("100%"); - + // Use data URL for PDF display String base64Pdf = java.util.Base64.getEncoder().encodeToString(pdfBytes); String dataUrl = "data:application/pdf;base64," + base64Pdf; @@ -593,11 +610,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter // Download button Button downloadButton = new Button(getTranslation("button.download"), e -> { - getElement() - .executeJs("const link = document.createElement('a');" + - "link.href = 'data:application/pdf;base64," + base64Pdf + "';" + - "link.download = '" + title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf';" + - "link.click();"); + getElement().executeJs("const link = document.createElement('a');" + + "link.href = 'data:application/pdf;base64," + base64Pdf + "';" + "link.download = '" + + title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf';" + "link.click();"); }); downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java index 337b757..912251c 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java @@ -164,7 +164,8 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter { if (appUser != null && appUser.getId() != null) { appUserService.deleteById(appUser.getId()); - Notification.show(getTranslation("editappuser.notification.deleted"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("editappuser.notification.deleted"), 3000, + Notification.Position.MIDDLE); confirmDialog.close(); navigateBack(); } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java index 67b33e6..fea95fd 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java @@ -151,7 +151,8 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter< customer = customerService.findById(customerId); if (customer == null) { - Notification.show(getTranslation("editcustomer.notification.notfound"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("editcustomer.notification.notfound"), 3000, + Notification.Position.MIDDLE); navigateBack(); return; } @@ -160,7 +161,8 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter< binder.readBean(customer); } catch (IllegalArgumentException e) { - Notification.show(getTranslation("editcustomer.notification.invalid.id"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("editcustomer.notification.invalid.id"), 3000, + Notification.Position.MIDDLE); navigateBack(); } } @@ -184,7 +186,8 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter< HorizontalLayout buttonLayout = new HorizontalLayout(); Button confirmDeleteButton = new Button(getTranslation("editcustomer.dialog.delete.confirm"), e -> { if (customer != null && customer.getId() != null) { - Notification.show(getTranslation("editcustomer.notification.deleted"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("editcustomer.notification.deleted"), 3000, + Notification.Position.MIDDLE); confirmDialog.close(); navigateBack(); } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java index c91aa17..d52df74 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java @@ -90,7 +90,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle this.invoiceTemplateService = invoiceTemplateService; this.currentUser = securityService.getCurrentDatabaseUser(); this.serviceRepository = serviceRepository; - + // Store the original language before any changes this.originalLanguage = this.currentUser != null ? this.currentUser.getLanguage() : Language.DE; setSizeFull(); @@ -126,7 +126,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle TextField companyAddField = new TextField(getTranslation("profile.companyadd")); TextField firstnameField = new TextField(getTranslation("profile.firstname")); - firstnameField.addBlurListener(e -> validateField(firstnameField, getTranslation("profile.validation.firstname"))); + firstnameField + .addBlurListener(e -> validateField(firstnameField, getTranslation("profile.validation.firstname"))); TextField lastnameField = new TextField(getTranslation("profile.lastname")); lastnameField.addBlurListener(e -> validateField(lastnameField, getTranslation("profile.validation.lastname"))); @@ -144,7 +145,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle streetField.addBlurListener(e -> validateField(streetField, getTranslation("profile.validation.street"))); TextField houseNumberField = new TextField(getTranslation("profile.housenr")); - houseNumberField.addBlurListener(e -> validateField(houseNumberField, getTranslation("profile.validation.housenr"))); + houseNumberField + .addBlurListener(e -> validateField(houseNumberField, getTranslation("profile.validation.housenr"))); TextField addressAddField = new TextField(getTranslation("profile.addressadd")); @@ -227,22 +229,29 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle cityField.setRequiredIndicatorVisible(true); // Hauptadresse binden - binder.forField(companyField).asRequired(getTranslation("profile.validation.company.required")).bind(User::getCompany, User::setCompany); + binder.forField(companyField).asRequired(getTranslation("profile.validation.company.required")) + .bind(User::getCompany, User::setCompany); binder.forField(companyAddField).bind(User::getCompanyAddition, User::setCompanyAddition); - binder.forField(streetField).asRequired(getTranslation("profile.validation.street.required")).bind(User::getStreet, User::setStreet); - binder.forField(houseNumberField).asRequired(getTranslation("profile.validation.housenr.required")).bind(User::getHouseNumber, - User::setHouseNumber); + binder.forField(streetField).asRequired(getTranslation("profile.validation.street.required")) + .bind(User::getStreet, User::setStreet); + binder.forField(houseNumberField).asRequired(getTranslation("profile.validation.housenr.required")) + .bind(User::getHouseNumber, User::setHouseNumber); binder.forField(addressAddField).bind(User::getAddressAddition, User::setAddressAddition); - binder.forField(zipField).asRequired(getTranslation("profile.validation.zip.required")).bind(User::getZip, User::setZip); - binder.forField(cityField).asRequired(getTranslation("profile.validation.city.required")).bind(User::getCity, User::setCity); + binder.forField(zipField).asRequired(getTranslation("profile.validation.zip.required")).bind(User::getZip, + User::setZip); + binder.forField(cityField).asRequired(getTranslation("profile.validation.city.required")).bind(User::getCity, + User::setCity); // Personendaten binden - binder.forField(firstnameField).asRequired(getTranslation("profile.validation.firstname.required")).bind(User::getFirstname, - User::setFirstname); - binder.forField(lastnameField).asRequired(getTranslation("profile.validation.lastname.required")).bind(User::getName, User::setName); - binder.forField(phoneField).asRequired(getTranslation("profile.validation.phone.required")).bind(User::getPhone, User::setPhone); + binder.forField(firstnameField).asRequired(getTranslation("profile.validation.firstname.required")) + .bind(User::getFirstname, User::setFirstname); + binder.forField(lastnameField).asRequired(getTranslation("profile.validation.lastname.required")) + .bind(User::getName, User::setName); + binder.forField(phoneField).asRequired(getTranslation("profile.validation.phone.required")).bind(User::getPhone, + User::setPhone); binder.forField(emailField).asRequired(getTranslation("profile.validation.email.required")) - .withValidator(new EmailValidator(getTranslation("profile.validation.email.invalid"))).bind(User::getEmail, User::setEmail); + .withValidator(new EmailValidator(getTranslation("profile.validation.email.invalid"))) + .bind(User::getEmail, User::setEmail); // Optionale Felder binder.forField(mobileField).bind(User::getPhone2, User::setPhone2); binder.forField(faxField).bind(User::getFax, User::setFax); @@ -494,16 +503,16 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle languageCombo.setItemLabelGenerator(language -> { // Flaggen-Emoji für jede Sprache String flag = switch (language) { - case DE -> "🇩🇪 "; - case EN -> "🇬🇧 "; - case FR -> "🇫🇷 "; - case ES -> "🇪🇸 "; - case TR -> "🇹🇷 "; - case PL -> "🇵🇱 "; - case RU -> "🇷🇺 "; - case EE -> "🇪🇪 "; - case LV -> "🇱🇻 "; - case LT -> "🇱🇹 "; + case DE -> "🇩🇪 "; + case EN -> "🇬🇧 "; + case FR -> "🇫🇷 "; + case ES -> "🇪🇸 "; + case TR -> "🇹🇷 "; + case PL -> "🇵🇱 "; + case RU -> "🇷🇺 "; + case EE -> "🇪🇪 "; + case LV -> "🇱🇻 "; + case LT -> "🇱🇹 "; }; return flag + language.getDisplayName(); }); @@ -513,7 +522,7 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle languageCombo.addValueChangeListener(e -> { // Language will be saved when the user clicks save button currentUser.setLanguage(e.getValue()); - + // Also set the language cookie so it's available before login setLanguageCookie(e.getValue()); }); @@ -590,7 +599,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle UI.getCurrent().getPage().reload(); } } catch (Exception ex) { - Notification.show(getTranslation("profile.save.error", ex.getMessage()), 4000, Notification.Position.MIDDLE); + Notification.show(getTranslation("profile.save.error", ex.getMessage()), 4000, + Notification.Position.MIDDLE); } } }); @@ -648,7 +658,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle } } catch (Exception e) { // Log error or show notification - Notification.show(getTranslation("profile.pdf.error", e.getMessage()), 3000, Notification.Position.BOTTOM_END); + Notification.show(getTranslation("profile.pdf.error", e.getMessage()), 3000, + Notification.Position.BOTTOM_END); } } @@ -863,7 +874,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle } }); } catch (Exception ex) { - Notification.show(getTranslation("profile.error", ex.getMessage()), 3000, Notification.Position.BOTTOM_CENTER); + Notification.show(getTranslation("profile.error", ex.getMessage()), 3000, + Notification.Position.BOTTOM_CENTER); } } @@ -930,18 +942,22 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle String email = safe(currentUser.getEmail()); String phone = safe(currentUser.getPhone()); - Div senderCompany = createVariableTemplate(getTranslation("profile.company"), VaadinIcon.OFFICE, "masterdata.company_name", + Div senderCompany = createVariableTemplate(getTranslation("profile.company"), VaadinIcon.OFFICE, + "masterdata.company_name", company.isEmpty() ? getTranslation("profile.invoice.placeholder.company") : company); - Div senderName = createVariableTemplate(getTranslation("profile.invoice.name"), VaadinIcon.USER, "masterdata.contact_name", + Div senderName = createVariableTemplate(getTranslation("profile.invoice.name"), VaadinIcon.USER, + "masterdata.contact_name", fullName.trim().isEmpty() ? getTranslation("profile.invoice.placeholder.name") : fullName.trim()); - Div senderAddress = createVariableTemplate(getTranslation("profile.street"), VaadinIcon.MAP_MARKER, "masterdata.street", + Div senderAddress = createVariableTemplate(getTranslation("profile.street"), VaadinIcon.MAP_MARKER, + "masterdata.street", street.trim().isEmpty() ? getTranslation("profile.invoice.placeholder.street") : street.trim()); - Div senderCity = createVariableTemplate(getTranslation("profile.invoice.city"), VaadinIcon.BUILDING, "masterdata.city", + Div senderCity = createVariableTemplate(getTranslation("profile.invoice.city"), VaadinIcon.BUILDING, + "masterdata.city", city.trim().isEmpty() ? getTranslation("profile.invoice.placeholder.city") : city.trim()); - Div senderEmail = createVariableTemplate(getTranslation("profile.invoice.email"), VaadinIcon.ENVELOPE, "masterdata.email", - email.isEmpty() ? getTranslation("profile.invoice.placeholder.email") : email); - Div senderPhone = createVariableTemplate(getTranslation("profile.invoice.phone"), VaadinIcon.PHONE, "masterdata.phone", - phone.isEmpty() ? getTranslation("profile.invoice.placeholder.phone") : phone); + Div senderEmail = createVariableTemplate(getTranslation("profile.invoice.email"), VaadinIcon.ENVELOPE, + "masterdata.email", email.isEmpty() ? getTranslation("profile.invoice.placeholder.email") : email); + Div senderPhone = createVariableTemplate(getTranslation("profile.invoice.phone"), VaadinIcon.PHONE, + "masterdata.phone", phone.isEmpty() ? getTranslation("profile.invoice.placeholder.phone") : phone); // Bereich 2: Leistungen Span servicesHeader = new Span(getTranslation("profile.services.label")); @@ -949,14 +965,14 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle .set("margin-top", "var(--lumo-space-m)"); // Leistungen als draggable Variable - Div servicesListBlock = createServicesVariableTemplate(getTranslation("profile.invoice.services.list"), VaadinIcon.LIST, "services.list", - "Artikel 1: 100,00 €\nArtikel 2: 50,00 €"); - Div servicesNetBlock = createServicesVariableTemplate(getTranslation("profile.invoice.net"), VaadinIcon.COIN_PILES, "services.net_total", - "150,00 €"); - Div servicesVatBlock = createServicesVariableTemplate(getTranslation("profile.invoice.vat"), VaadinIcon.COIN_PILES, - "services.vat_total", "28,50 €"); - Div servicesGrossBlock = createServicesVariableTemplate(getTranslation("profile.invoice.gross"), VaadinIcon.MONEY, "services.gross_total", - "178,50 €"); + Div servicesListBlock = createServicesVariableTemplate(getTranslation("profile.invoice.services.list"), + VaadinIcon.LIST, "services.list", "Artikel 1: 100,00 €\nArtikel 2: 50,00 €"); + Div servicesNetBlock = createServicesVariableTemplate(getTranslation("profile.invoice.net"), + VaadinIcon.COIN_PILES, "services.net_total", "150,00 €"); + Div servicesVatBlock = createServicesVariableTemplate(getTranslation("profile.invoice.vat"), + VaadinIcon.COIN_PILES, "services.vat_total", "28,50 €"); + Div servicesGrossBlock = createServicesVariableTemplate(getTranslation("profile.invoice.gross"), + VaadinIcon.MONEY, "services.gross_total", "178,50 €"); // Bereich 3: Kundendaten Span customerHeader = new Span(getTranslation("profile.invoice.customerdata")); @@ -964,18 +980,18 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle .set("margin-top", "var(--lumo-space-m)"); // Kundendaten als Variablen (grün hinterlegt) - Div customerCompany = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.company"), VaadinIcon.OFFICE, "customer.company_name", - "Kundenfirma GmbH"); - Div customerName = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.name"), VaadinIcon.USER, "customer.contact_name", - "Erika Mustermann"); - Div customerAddress = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.street"), VaadinIcon.MAP_MARKER, "customer.street", - "Kundenstraße 456"); - Div customerCity = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.city"), VaadinIcon.BUILDING, "customer.city", - "54321 Kundenstadt"); - Div customerEmail = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.email"), VaadinIcon.ENVELOPE, "customer.email", - "kunde@beispiel.de"); - Div customerPhone = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.phone"), VaadinIcon.PHONE, "customer.phone", - "0987 654321"); + Div customerCompany = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.company"), + VaadinIcon.OFFICE, "customer.company_name", "Kundenfirma GmbH"); + Div customerName = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.name"), + VaadinIcon.USER, "customer.contact_name", "Erika Mustermann"); + Div customerAddress = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.street"), + VaadinIcon.MAP_MARKER, "customer.street", "Kundenstraße 456"); + Div customerCity = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.city"), + VaadinIcon.BUILDING, "customer.city", "54321 Kundenstadt"); + Div customerEmail = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.email"), + VaadinIcon.ENVELOPE, "customer.email", "kunde@beispiel.de"); + Div customerPhone = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.phone"), + VaadinIcon.PHONE, "customer.phone", "0987 654321"); // Bereich 2: Freie Elemente Span freeHeader = new Span(getTranslation("profile.invoice.free.elements")); @@ -983,14 +999,22 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle "var(--lumo-space-m)"); // Draggable Templates - Div textBlock = createDraggableTemplate(getTranslation("profile.invoice.element.text"), VaadinIcon.TEXT_LABEL, "text"); - Div headerBlock = createDraggableTemplate(getTranslation("profile.invoice.element.header"), VaadinIcon.HEADER, "header"); - Div dateBlock = createDraggableTemplate(getTranslation("profile.invoice.element.date"), VaadinIcon.CALENDAR, "date"); - Div customerBlock = createDraggableTemplate(getTranslation("profile.invoice.element.customer"), VaadinIcon.USER, "customer"); - Div companyBlock = createDraggableTemplate(getTranslation("profile.invoice.element.company"), VaadinIcon.WORKPLACE, "company"); - Div amountBlock = createDraggableTemplate(getTranslation("profile.invoice.element.amount"), VaadinIcon.COIN_PILES, "amount"); - Div lineBlock = createDraggableTemplate(getTranslation("profile.invoice.element.line"), VaadinIcon.LINE_V, "line"); - Div imageBlock = createDraggableTemplate(getTranslation("profile.invoice.element.image"), VaadinIcon.PICTURE, "image"); + Div textBlock = createDraggableTemplate(getTranslation("profile.invoice.element.text"), VaadinIcon.TEXT_LABEL, + "text"); + Div headerBlock = createDraggableTemplate(getTranslation("profile.invoice.element.header"), VaadinIcon.HEADER, + "header"); + Div dateBlock = createDraggableTemplate(getTranslation("profile.invoice.element.date"), VaadinIcon.CALENDAR, + "date"); + Div customerBlock = createDraggableTemplate(getTranslation("profile.invoice.element.customer"), VaadinIcon.USER, + "customer"); + Div companyBlock = createDraggableTemplate(getTranslation("profile.invoice.element.company"), + VaadinIcon.WORKPLACE, "company"); + Div amountBlock = createDraggableTemplate(getTranslation("profile.invoice.element.amount"), + VaadinIcon.COIN_PILES, "amount"); + Div lineBlock = createDraggableTemplate(getTranslation("profile.invoice.element.line"), VaadinIcon.LINE_V, + "line"); + Div imageBlock = createDraggableTemplate(getTranslation("profile.invoice.element.image"), VaadinIcon.PICTURE, + "image"); panel.add(invoiceHeader, senderCompany, senderName, senderAddress, senderCity, senderEmail, senderPhone, servicesHeader, servicesListBlock, servicesNetBlock, servicesVatBlock, servicesGrossBlock, @@ -1208,7 +1232,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle getElement() .executeJs("if (window.updateProfileElementImage) { window.updateProfileElementImage('" + elementId + "', $0); }", dataUrl); - Notification.show(getTranslation("profile.invoice.image.uploaded"), 3000, Notification.Position.BOTTOM_CENTER); + Notification.show(getTranslation("profile.invoice.image.uploaded"), 3000, + Notification.Position.BOTTOM_CENTER); } catch (Exception ex) { Notification.show(getTranslation("profile.invoice.image.upload.error", ex.getMessage()), 3000, Notification.Position.BOTTOM_CENTER); @@ -1333,7 +1358,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle } // Löschen Button - Button deleteButton = new Button(getTranslation("profile.invoice.element.delete"), new Icon(VaadinIcon.TRASH)); + Button deleteButton = new Button(getTranslation("profile.invoice.element.delete"), + new Icon(VaadinIcon.TRASH)); deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); deleteButton.setWidthFull(); deleteButton.addClickListener(e -> { @@ -1498,8 +1524,10 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle return ""; }).setHeader(getTranslation("profile.services.vatrate")).setSortable(true); - servicesGrid.addColumn(service -> service.isMandatory() ? getTranslation("common.yes") : getTranslation("common.no")).setHeader(getTranslation("profile.services.mandatory")) - .setSortable(true); + servicesGrid + .addColumn( + service -> service.isMandatory() ? getTranslation("common.yes") : getTranslation("common.no")) + .setHeader(getTranslation("profile.services.mandatory")).setSortable(true); // Actions column with edit and delete buttons servicesGrid.addComponentColumn(service -> { @@ -1554,7 +1582,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle */ private void openServiceDialog(Service service) { Dialog dialog = new Dialog(); - dialog.setHeaderTitle(service == null ? getTranslation("profile.services.dialog.create") : getTranslation("profile.services.dialog.edit")); + dialog.setHeaderTitle(service == null ? getTranslation("profile.services.dialog.create") + : getTranslation("profile.services.dialog.edit")); dialog.setWidth("500px"); // Form layout @@ -1570,7 +1599,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle nameField.setRequiredIndicatorVisible(true); // Calculation basis combo box - ComboBox calculationBasisCombo = new ComboBox<>(getTranslation("profile.services.basis")); + ComboBox calculationBasisCombo = new ComboBox<>( + getTranslation("profile.services.basis")); calculationBasisCombo.setWidthFull(); calculationBasisCombo.setItems(Service.CalculationBasis.values()); calculationBasisCombo.setItemLabelGenerator(basis -> { @@ -1855,31 +1885,30 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle } /** - * Sets the language cookie via JavaScript so the selected language - * is available even before the user logs in (e.g., on the login page). + * Sets the language cookie via JavaScript so the selected language is available + * even before the user logs in (e.g., on the login page). * - * @param language the selected language + * @param language + * the selected language */ private void setLanguageCookie(Language language) { String languageCode = switch (language) { - case DE -> "DE"; - case EN -> "EN"; - case FR -> "FR"; - case ES -> "ES"; - case TR -> "TR"; - case PL -> "PL"; - case RU -> "RU"; - case EE -> "EE"; - case LV -> "LV"; - case LT -> "LT"; + case DE -> "DE"; + case EN -> "EN"; + case FR -> "FR"; + case ES -> "ES"; + case TR -> "TR"; + case PL -> "PL"; + case RU -> "RU"; + case EE -> "EE"; + case LV -> "LV"; + case LT -> "LT"; }; - + // Execute JavaScript to set the cookie - UI.getCurrent().getPage().executeJs( - "const maxAge = 365 * 24 * 60 * 60;" + - "document.cookie = 'votianlt.language=' + $0 + ';path=/;max-age=' + maxAge + ';SameSite=Lax';", - languageCode - ); + UI.getCurrent().getPage().executeJs("const maxAge = 365 * 24 * 60 * 60;" + + "document.cookie = 'votianlt.language=' + $0 + ';path=/;max-age=' + maxAge + ';SameSite=Lax';", + languageCode); } @Override diff --git a/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java b/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java index aff4a72..dc85d32 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java @@ -109,14 +109,22 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi header.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-l)"); // Draggable Templates - Div textBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.text"), VaadinIcon.TEXT_LABEL, "text"); - Div headerBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.header"), VaadinIcon.HEADER, "header"); - Div dateBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.date"), VaadinIcon.CALENDAR, "date"); - Div customerBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.customerinfo"), VaadinIcon.USER, "customer"); - Div companyBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.companyinfo"), VaadinIcon.OFFICE, "company"); - Div amountBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.amount"), VaadinIcon.COIN_PILES, "amount"); - Div lineBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.line"), VaadinIcon.LINE_V, "line"); - Div imageBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.image"), VaadinIcon.PICTURE, "image"); + Div textBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.text"), VaadinIcon.TEXT_LABEL, + "text"); + Div headerBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.header"), VaadinIcon.HEADER, + "header"); + Div dateBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.date"), VaadinIcon.CALENDAR, + "date"); + Div customerBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.customerinfo"), + VaadinIcon.USER, "customer"); + Div companyBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.companyinfo"), + VaadinIcon.OFFICE, "company"); + Div amountBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.amount"), + VaadinIcon.COIN_PILES, "amount"); + Div lineBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.line"), VaadinIcon.LINE_V, + "line"); + Div imageBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.image"), VaadinIcon.PICTURE, + "image"); panel.add(header, textBlock, headerBlock, dateBlock, customerBlock, companyBlock, amountBlock, lineBlock, imageBlock); @@ -238,7 +246,8 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.exportTemplate(); }"); }); - Button generatePdfButton = new Button(getTranslation("invoicegenerator.button.generatepdf"), new Icon(VaadinIcon.FILE_TEXT_O)); + Button generatePdfButton = new Button(getTranslation("invoicegenerator.button.generatepdf"), + new Icon(VaadinIcon.FILE_TEXT_O)); generatePdfButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS); generatePdfButton.addClickListener(e -> generatePdf()); @@ -289,7 +298,8 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData); showPdfInDialog(pdfBytes); } catch (Exception ex) { - showNotification(getTranslation("invoicegenerator.notification.preview.error", ex.getMessage())); + showNotification( + getTranslation("invoicegenerator.notification.preview.error", ex.getMessage())); } }); } catch (Exception ex) { @@ -557,7 +567,8 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi } // Löschen Button - Button deleteButton = new Button(getTranslation("invoicegenerator.button.delete"), new Icon(VaadinIcon.TRASH)); + Button deleteButton = new Button(getTranslation("invoicegenerator.button.delete"), + new Icon(VaadinIcon.TRASH)); deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); deleteButton.setWidthFull(); deleteButton.addClickListener(e -> { diff --git a/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java b/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java index b0949a5..91afc99 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java @@ -46,11 +46,16 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle { add(title); invoiceGrid = new Grid<>(SystemInvoice.class, false); - invoiceGrid.addColumn(SystemInvoice::getId).setHeader(getTranslation("invoices.column.number")).setAutoWidth(true); - invoiceGrid.addColumn(SystemInvoice::getKunde).setHeader(getTranslation("invoices.column.customer")).setAutoWidth(true); - invoiceGrid.addColumn(SystemInvoice::getDatum).setHeader(getTranslation("invoices.column.date")).setAutoWidth(true); - invoiceGrid.addColumn(SystemInvoice::getBetrag).setHeader(getTranslation("invoices.column.amount")).setAutoWidth(true); - invoiceGrid.addColumn(SystemInvoice::getBeschreibung).setHeader(getTranslation("invoices.column.description")).setAutoWidth(true); + invoiceGrid.addColumn(SystemInvoice::getId).setHeader(getTranslation("invoices.column.number")) + .setAutoWidth(true); + invoiceGrid.addColumn(SystemInvoice::getKunde).setHeader(getTranslation("invoices.column.customer")) + .setAutoWidth(true); + invoiceGrid.addColumn(SystemInvoice::getDatum).setHeader(getTranslation("invoices.column.date")) + .setAutoWidth(true); + invoiceGrid.addColumn(SystemInvoice::getBetrag).setHeader(getTranslation("invoices.column.amount")) + .setAutoWidth(true); + invoiceGrid.addColumn(SystemInvoice::getBeschreibung).setHeader(getTranslation("invoices.column.description")) + .setAutoWidth(true); invoiceGrid.setSelectionMode(Grid.SelectionMode.SINGLE); invoiceGrid.getStyle().set("cursor", "pointer"); @@ -88,7 +93,8 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle { UI.getCurrent().getPage().open(registration.getResourceUri().toString()); } catch (Exception e) { - Notification.show(getTranslation("invoices.notification.pdf.error", e.getMessage()), 5000, Notification.Position.MIDDLE); + Notification.show(getTranslation("invoices.notification.pdf.error", e.getMessage()), 5000, + Notification.Position.MIDDLE); } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java b/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java index 7dcb7d3..f532e02 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java @@ -183,7 +183,8 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has Icon typeIcon = getTypeIcon(entry.getChangeType()); typeIcon.getStyle().set("color", getTypeColor(entry.getChangeType())); - Span reason = new Span(entry.getReason() != null ? entry.getReason() : getTranslation("jobhistory.entry.unknown")); + Span reason = new Span( + entry.getReason() != null ? entry.getReason() : getTranslation("jobhistory.entry.unknown")); reason.getStyle().set("font-weight", "500"); Span timestamp = new Span(formatDateTime(entry.getTimestamp())); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java index 2b8947a..dd69854 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java @@ -149,7 +149,8 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has sendMessageButton.addClickListener(e -> { // Check if job has an app user assigned if (job.getAppUser() == null || job.getAppUser().isBlank()) { - Notification.show(getTranslation("jobsummary.notification.noappuser"), 3000, Notification.Position.MIDDLE) + Notification + .show(getTranslation("jobsummary.notification.noappuser"), 3000, Notification.Position.MIDDLE) .addThemeVariants(NotificationVariant.LUMO_ERROR); return; } @@ -189,7 +190,8 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has topRow.setSpacing(true); VerticalLayout pickupBox = borderedBox(); - pickupBox.add(new H3(getTranslation("jobsummary.section.pickup") + " " + formatDateWithTime(job.getPickupDate(), job.getPickupTime()))); + pickupBox.add(new H3(getTranslation("jobsummary.section.pickup") + " " + + formatDateWithTime(job.getPickupDate(), job.getPickupTime()))); pickupBox.add(new Span(valueOrEmpty(job.getPickupCompany()))); pickupBox.add(new Span(valueOrEmpty(job.getPickupSalutation()) + (job.getPickupSalutation() != null ? " " : "") + valueOrEmpty(job.getPickupFirstName()) + (job.getPickupFirstName() != null ? " " : "") @@ -198,7 +200,8 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has pickupBox.add(new Span(concatZipCity(job.getPickupZip(), job.getPickupCity()))); VerticalLayout deliveryBox = borderedBox(); - deliveryBox.add(new H3(getTranslation("jobsummary.section.delivery") + " " + formatDateWithTime(job.getDeliveryDate(), job.getDeliveryTime()))); + deliveryBox.add(new H3(getTranslation("jobsummary.section.delivery") + " " + + formatDateWithTime(job.getDeliveryDate(), job.getDeliveryTime()))); deliveryBox.add(new Span(valueOrEmpty(job.getDeliveryCompany()))); deliveryBox.add(new Span(valueOrEmpty(job.getDeliverySalutation()) + (job.getDeliverySalutation() != null ? " " : "") + valueOrEmpty(job.getDeliveryFirstName()) @@ -265,13 +268,13 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has VerticalLayout infoBox = borderedBox(); infoBox.add(new H3(getTranslation("jobsummary.section.info"))); - + // Preis basierend auf den hinterlegten Leistungen berechnen PriceCalculationResult priceResult = calculatePriceFromServices(job); infoBox.add(new Span(getTranslation("jobsummary.info.netto") + ": " + formatPrice(priceResult.netAmount()))); infoBox.add(new Span(getTranslation("jobsummary.info.ust") + ": " + formatPrice(priceResult.vatAmount()))); infoBox.add(new Span(getTranslation("jobsummary.info.gesamt") + ": " + formatPrice(priceResult.totalAmount()))); - + if (job.getRemark() != null && !job.getRemark().isBlank()) { infoBox.add(new Span(getTranslation("jobsummary.info.bemerkung") + ": " + job.getRemark())); } @@ -279,7 +282,8 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has infoBox.add(new Span(getTranslation("jobsummary.info.digital"))); } if (job.getAppUser() != null && !job.getAppUser().isBlank()) { - infoBox.add(new Span(getTranslation("jobsummary.info.appuser") + ": " + resolveAppUserName(job.getAppUser()))); + infoBox.add( + new Span(getTranslation("jobsummary.info.appuser") + ": " + resolveAppUserName(job.getAppUser()))); } cargoBox.setWidth("50%"); @@ -298,7 +302,8 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has buttonRow.setJustifyContentMode(HorizontalLayout.JustifyContentMode.CENTER); buttonRow.getStyle().set("margin-top", "var(--lumo-space-l)"); - Button completeButton = new Button(getTranslation("jobsummary.button.complete"), new Icon(VaadinIcon.CHECK_CIRCLE)); + Button completeButton = new Button(getTranslation("jobsummary.button.complete"), + new Icon(VaadinIcon.CHECK_CIRCLE)); completeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS); completeButton.addClickListener(e -> { ConfirmDialog dialog = new ConfirmDialog(); @@ -475,14 +480,15 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has Integer savedDuration = job.getRouteDurationSeconds(); boolean hasSavedRouteData = savedDistance != null && savedDuration != null; - String js = buildMapJs(origin, destination, hasPosition, position, appUserId, shouldUpdate, - hasSavedRouteData, savedDistance != null ? savedDistance : 0.0, savedDuration != null ? savedDuration : 0); + String js = buildMapJs(origin, destination, hasPosition, position, appUserId, shouldUpdate, hasSavedRouteData, + savedDistance != null ? savedDistance : 0.0, savedDuration != null ? savedDuration : 0); map.getElement().executeJs(js, map.getElement(), routeInfo.getElement()); } private String buildMapJs(String origin, String destination, boolean hasPosition, LocationPosition position, - String appUserId, boolean shouldUpdate, boolean hasSavedRouteData, double savedDistance, int savedDuration) { + String appUserId, boolean shouldUpdate, boolean hasSavedRouteData, double savedDistance, + int savedDuration) { String apiKey = getGoogleMapsApiKey(); // Explizit mit Punkt als Dezimaltrennzeichen formatieren String lat = hasPosition ? String.format(java.util.Locale.US, "%.6f", position.getLatitude()) : "0"; @@ -493,9 +499,8 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has // Gespeicherte Dauer formatieren int hours = savedDuration / 3600; int minutes = (savedDuration % 3600) / 60; - String savedDurationText = hours > 0 - ? String.format("%d Std. %d Min.", hours, minutes) - : String.format("%d Min.", minutes); + String savedDurationText = hours > 0 ? String.format("%d Std. %d Min.", hours, minutes) + : String.format("%d Min.", minutes); return """ (function(){ @@ -540,7 +545,7 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has }, function(res, status){ if(status === 'OK'){ infoEl.innerHTML = ''; - + // Gespeicherte Route-Daten anzeigen (wenn vorhanden) if(hasSavedRouteData){ var savedRouteDiv = document.createElement('div'); @@ -552,7 +557,7 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has savedRouteDiv.textContent = '📍 Geplante Route: ' + savedDistance.toFixed(1) + ' km • Fahrtzeit: ' + savedDurationText; infoEl.appendChild(savedRouteDiv); } - + var bounds = new google.maps.LatLngBounds(); // Nur die erste (beste) Route anzeigen @@ -1199,65 +1204,68 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has private PriceCalculationResult calculatePriceFromServices(Job job) { BigDecimal netTotal = BigDecimal.ZERO; BigDecimal vatTotal = BigDecimal.ZERO; - + List serviceIds = job.getServiceIds(); if (serviceIds == null || serviceIds.isEmpty()) { // Fallback auf gespeicherten Preis BigDecimal price = job.getPrice() != null ? job.getPrice() : BigDecimal.ZERO; return new PriceCalculationResult(price, BigDecimal.ZERO, price); } - + // Routendaten für die Berechnung Double routeDistance = job.getRouteDistanceKm(); Integer durationSeconds = job.getRouteDurationSeconds(); - + for (String serviceId : serviceIds) { Service service = serviceRepository.findById(serviceId).orElse(null); - if (service == null) continue; - + if (service == null) + continue; + BigDecimal price = calculateServicePrice(service, routeDistance, durationSeconds); BigDecimal vatRate = service.getVatRate() != null ? service.getVatRate() : new BigDecimal("0.19"); - + netTotal = netTotal.add(price); vatTotal = vatTotal.add(price.multiply(vatRate)); } - + BigDecimal totalAmount = netTotal.add(vatTotal); return new PriceCalculationResult(netTotal, vatTotal, totalAmount); } - + /** - * Berechnet den Preis für eine einzelne Leistung basierend auf ihrer Berechnungsgrundlage. + * Berechnet den Preis für eine einzelne Leistung basierend auf ihrer + * Berechnungsgrundlage. */ private BigDecimal calculateServicePrice(Service service, Double routeDistance, Integer durationSeconds) { if (service.getCalculationBasis() == null) { return BigDecimal.ZERO; } - + switch (service.getCalculationBasis()) { case FLAT_RATE: return service.getPrice() != null ? service.getPrice() : BigDecimal.ZERO; - + case DISTANCE: if (service.getPricePerKilometer() != null && routeDistance != null && routeDistance > 0) { return service.getPricePerKilometer().multiply(BigDecimal.valueOf(routeDistance)); } return BigDecimal.ZERO; - + case TIME: if (service.getPricePer15Minutes() != null && durationSeconds != null && durationSeconds > 0) { // Dauer in 15-Minuten-Einheiten umrechnen int units = durationSeconds / 900; // 900 Sekunden = 15 Minuten - if (durationSeconds % 900 > 0) units++; // Aufrunden + if (durationSeconds % 900 > 0) + units++; // Aufrunden return service.getPricePer15Minutes().multiply(BigDecimal.valueOf(units)); } return BigDecimal.ZERO; - + default: return BigDecimal.ZERO; } } - + /** * Record für die Preisberechnungsergebnisse. */ diff --git a/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java b/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java index 0782d7d..3723f20 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java @@ -184,8 +184,7 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af private void handleVerify2fa() { if (pendingAuth == null) { - Notification.show(getTranslation("login.2fa.no.credentials"), 3000, - Notification.Position.BOTTOM_CENTER); + Notification.show(getTranslation("login.2fa.no.credentials"), 3000, Notification.Position.BOTTOM_CENTER); return; } String username = pendingAuth.getName(); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/MessagesView.java b/src/main/java/de/assecutor/votianlt/pages/view/MessagesView.java index 28f1c7d..d1ee50a 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/MessagesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/MessagesView.java @@ -106,8 +106,10 @@ public class MessagesView extends Main implements HasDynamicTitle { return span; })).setHeader(getTranslation("messages.column.status")).setWidth("80px").setFlexGrow(0); - grid.addColumn(ClientMessageSummary::getClientName).setHeader(getTranslation("messages.column.client")).setAutoWidth(true); - grid.addColumn(ClientMessageSummary::getClientEmail).setHeader(getTranslation("messages.column.email")).setAutoWidth(true); + grid.addColumn(ClientMessageSummary::getClientName).setHeader(getTranslation("messages.column.client")) + .setAutoWidth(true); + grid.addColumn(ClientMessageSummary::getClientEmail).setHeader(getTranslation("messages.column.email")) + .setAutoWidth(true); grid.addColumn(new ComponentRenderer<>(summary -> { Span span = new Span(String.valueOf(summary.getTotalMessages())); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java b/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java index d2b64d1..8d55423 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java @@ -137,13 +137,14 @@ public class MyInvoicesView extends Main implements HasDynamicTitle { grid.addThemeVariants(GridVariant.LUMO_ROW_STRIPES, GridVariant.LUMO_COMPACT, GridVariant.LUMO_WRAP_CELL_CONTENT, GridVariant.LUMO_COLUMN_BORDERS); grid.setWidthFull(); - grid.addColumn(new ComponentRenderer<>(row -> statusBadge(row.status()))).setHeader(getTranslation("myinvoices.column.status")).setAutoWidth(true) - .setFlexGrow(0); - grid.addColumn(MyInvoicesView::formatInvoiceNumber).setHeader(getTranslation("myinvoices.column.number")).setAutoWidth(true); - grid.addColumn(row -> DateTimeFormatUtil.formatDate(row.date())).setHeader(getTranslation("myinvoices.column.date")).setAutoWidth(true) - .setFlexGrow(0); - grid.addColumn(row -> CURRENCY_FMT.format(row.amount())).setHeader(getTranslation("myinvoices.column.amount")).setAutoWidth(true) - .setTextAlign(ColumnTextAlign.END).setFlexGrow(0); + grid.addColumn(new ComponentRenderer<>(row -> statusBadge(row.status()))) + .setHeader(getTranslation("myinvoices.column.status")).setAutoWidth(true).setFlexGrow(0); + grid.addColumn(MyInvoicesView::formatInvoiceNumber).setHeader(getTranslation("myinvoices.column.number")) + .setAutoWidth(true); + grid.addColumn(row -> DateTimeFormatUtil.formatDate(row.date())) + .setHeader(getTranslation("myinvoices.column.date")).setAutoWidth(true).setFlexGrow(0); + grid.addColumn(row -> CURRENCY_FMT.format(row.amount())).setHeader(getTranslation("myinvoices.column.amount")) + .setAutoWidth(true).setTextAlign(ColumnTextAlign.END).setFlexGrow(0); grid.setAllRowsVisible(true); grid.setItems(allRows); // zunächst leer grid.addItemClickListener(event -> { diff --git a/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java b/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java index c9280f8..a1adbb6 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java @@ -164,7 +164,8 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle { resendButton.setVisible(false); // Zurück-Link - Button backButton = new Button(getTranslation("register.button.back"), event -> getUI().ifPresent(ui -> ui.navigate(""))); + Button backButton = new Button(getTranslation("register.button.back"), + event -> getUI().ifPresent(ui -> ui.navigate(""))); backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); backButton.setWidthFull(); @@ -223,12 +224,14 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle { // Validierung if (email.isEmpty()) { - Notification.show(getTranslation("register.notification.email.required"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.email.required"), 3000, + Notification.Position.MIDDLE); emailField.focus(); return; } if (!email.contains("@") || !email.contains(".")) { - Notification.show(getTranslation("register.notification.email.invalid"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.email.invalid"), 3000, + Notification.Position.MIDDLE); emailField.focus(); return; } @@ -238,7 +241,8 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle { return; } if (password.isEmpty()) { - Notification.show(getTranslation("register.notification.password.required"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.password.required"), 3000, + Notification.Position.MIDDLE); passwordField.focus(); return; } @@ -248,7 +252,8 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle { return; } if (!password.equals(confirmPassword)) { - Notification.show(getTranslation("register.notification.password.mismatch"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.password.mismatch"), 3000, + Notification.Position.MIDDLE); confirmPasswordField.focus(); return; } @@ -256,37 +261,43 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle { // Weitere Pflichtfelder prüfen (aus Edit-Profile) var firstName = firstNameField.getValue() != null ? firstNameField.getValue().trim() : ""; if (firstName.isEmpty()) { - Notification.show(getTranslation("register.notification.firstname.required"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.firstname.required"), 3000, + Notification.Position.MIDDLE); firstNameField.focus(); return; } var lastName = lastNameField.getValue() != null ? lastNameField.getValue().trim() : ""; if (lastName.isEmpty()) { - Notification.show(getTranslation("register.notification.lastname.required"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.lastname.required"), 3000, + Notification.Position.MIDDLE); lastNameField.focus(); return; } var phone = phoneField.getValue() != null ? phoneField.getValue().trim() : ""; if (phone.isEmpty()) { - Notification.show(getTranslation("register.notification.phone.required"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.phone.required"), 3000, + Notification.Position.MIDDLE); phoneField.focus(); return; } var company = companyField.getValue() != null ? companyField.getValue().trim() : ""; if (company.isEmpty()) { - Notification.show(getTranslation("register.notification.company.required"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.company.required"), 3000, + Notification.Position.MIDDLE); companyField.focus(); return; } var street = streetField.getValue() != null ? streetField.getValue().trim() : ""; if (street.isEmpty()) { - Notification.show(getTranslation("register.notification.street.required"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.street.required"), 3000, + Notification.Position.MIDDLE); streetField.focus(); return; } var houseNo = houseNumberField.getValue() != null ? houseNumberField.getValue().trim() : ""; if (houseNo.isEmpty()) { - Notification.show(getTranslation("register.notification.housenr.required"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.housenr.required"), 3000, + Notification.Position.MIDDLE); houseNumberField.focus(); return; } @@ -298,7 +309,8 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle { } var city = cityField.getValue() != null ? cityField.getValue().trim() : ""; if (city.isEmpty()) { - Notification.show(getTranslation("register.notification.city.required"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.city.required"), 3000, + Notification.Position.MIDDLE); cityField.focus(); return; } @@ -351,23 +363,25 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle { Notification.Position.MIDDLE); } catch (Exception e) { awaitingVerification = false; - Notification.show(getTranslation("register.notification.code.emailerror", e.getMessage()), 5000, Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.code.emailerror", e.getMessage()), 5000, + Notification.Position.MIDDLE); } } private void onVerifyCode() { if (!awaitingVerification) { - Notification.show(getTranslation("register.notification.code.startfirst"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.code.startfirst"), 3000, + Notification.Position.MIDDLE); return; } String entered = codeField.getValue() != null ? codeField.getValue().trim() : ""; if (!entered.matches("\\d{6}")) { - Notification.show(getTranslation("register.notification.code.required"), 3000, Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.code.required"), 3000, + Notification.Position.MIDDLE); return; } if (codeExpiresAt == null || LocalDateTime.now().isAfter(codeExpiresAt)) { - Notification.show(getTranslation("register.notification.code.expired"), 4000, - Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.code.expired"), 4000, Notification.Position.MIDDLE); return; } if (!entered.equals(pendingCode)) { @@ -397,11 +411,11 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle { user.setZip(zip); user.setCity(city); userService.save(user); - VaadinSession.getCurrent().setAttribute("flashMessage", - getTranslation("register.notification.success")); + VaadinSession.getCurrent().setAttribute("flashMessage", getTranslation("register.notification.success")); getUI().ifPresent(ui -> ui.navigate("login")); } catch (RuntimeException e) { - Notification.show(getTranslation("register.notification.failed", e.getMessage()), 5000, Notification.Position.MIDDLE); + Notification.show(getTranslation("register.notification.failed", e.getMessage()), 5000, + Notification.Position.MIDDLE); } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java b/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java index 13a2629..f9cd5ba 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java @@ -42,25 +42,28 @@ public class ShowCustomersView extends VerticalLayout implements HasDynamicTitle add(header); // Add hint text - var hintText = new com.vaadin.flow.component.html.Paragraph( - getTranslation("customers.hint.click")); + var hintText = new com.vaadin.flow.component.html.Paragraph(getTranslation("customers.hint.click")); hintText.getStyle().set("color", "var(--lumo-secondary-text-color)"); hintText.getStyle().set("font-size", "var(--lumo-font-size-s)"); add(hintText); // Configure grid columns - grid.addColumn(Customer::getCompanyName).setHeader(getTranslation("customers.column.company")).setAutoWidth(true).setFlexGrow(1).setSortable(true); - grid.addColumn(customer -> (customer.getFirstname() != null ? customer.getFirstname() : "") + " " - + (customer.getLastName() != null ? customer.getLastName() : "")).setHeader(getTranslation("customers.column.name")).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(getTranslation("customers.column.phone")).setAutoWidth(true).setSortable(true); - grid.addColumn(customer -> (customer.getStreet() != null ? customer.getStreet() : "") + " " - + (customer.getHouseNumber() != null ? customer.getHouseNumber() : "")).setHeader(getTranslation("customers.column.street")) + grid.addColumn(Customer::getCompanyName).setHeader(getTranslation("customers.column.company")) .setAutoWidth(true).setFlexGrow(1).setSortable(true); - grid.addColumn(customer -> (customer.getZip() != null ? customer.getZip() : "") + " " - + (customer.getCity() != null ? customer.getCity() : "")).setHeader(getTranslation("customers.column.city")).setAutoWidth(true) + grid.addColumn(customer -> (customer.getFirstname() != null ? customer.getFirstname() : "") + " " + + (customer.getLastName() != null ? customer.getLastName() : "")) + .setHeader(getTranslation("customers.column.name")).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(getTranslation("customers.column.phone")).setAutoWidth(true) + .setSortable(true); + grid.addColumn(customer -> (customer.getStreet() != null ? customer.getStreet() : "") + " " + + (customer.getHouseNumber() != null ? customer.getHouseNumber() : "")) + .setHeader(getTranslation("customers.column.street")).setAutoWidth(true).setFlexGrow(1) + .setSortable(true); + grid.addColumn(customer -> (customer.getZip() != null ? customer.getZip() : "") + " " + + (customer.getCity() != null ? customer.getCity() : "")) + .setHeader(getTranslation("customers.column.city")).setAutoWidth(true).setFlexGrow(1).setSortable(true); grid.setMultiSort(true); grid.setSizeFull(); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java b/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java index d11a21f..ba9a8cf 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java @@ -71,7 +71,8 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle { add(title); // Configure status filter - statusFilter.setItems(getTranslation("jobs.status.all"), getTranslation("jobs.status.open"), getTranslation("jobs.status.done")); + statusFilter.setItems(getTranslation("jobs.status.all"), getTranslation("jobs.status.open"), + getTranslation("jobs.status.done")); statusFilter.setValue(getTranslation("jobs.status.open")); statusFilter.setWidth("150px"); @@ -108,12 +109,14 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle { endDate.addValueChangeListener(e -> loadData()); // Configure grid columns: Auftraggeber, Auftragsnummer, Auftragsdatum, Zielort - grid.addColumn(job -> extractCompanyName(job.getCustomerSelection())).setHeader(getTranslation("jobs.column.customer")) - .setAutoWidth(true).setFlexGrow(1).setSortable(true); - grid.addColumn(Job::getJobNumber).setHeader(getTranslation("jobs.column.jobnumber")).setAutoWidth(true).setSortable(true); - grid.addColumn(job -> DateTimeFormatUtil.formatDateTime(job.getCreatedAt())).setHeader(getTranslation("jobs.column.jobdate")) - .setAutoWidth(true).setSortable(true); - grid.addColumn(Job::getDeliveryCity).setHeader(getTranslation("jobs.column.destination")).setAutoWidth(true).setFlexGrow(1).setSortable(true); + grid.addColumn(job -> extractCompanyName(job.getCustomerSelection())) + .setHeader(getTranslation("jobs.column.customer")).setAutoWidth(true).setFlexGrow(1).setSortable(true); + grid.addColumn(Job::getJobNumber).setHeader(getTranslation("jobs.column.jobnumber")).setAutoWidth(true) + .setSortable(true); + grid.addColumn(job -> DateTimeFormatUtil.formatDateTime(job.getCreatedAt())) + .setHeader(getTranslation("jobs.column.jobdate")).setAutoWidth(true).setSortable(true); + grid.addColumn(Job::getDeliveryCity).setHeader(getTranslation("jobs.column.destination")).setAutoWidth(true) + .setFlexGrow(1).setSortable(true); // Action column: manual completion for jobs without digital processing grid.addComponentColumn(job -> { @@ -201,8 +204,8 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle { Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_SUCCESS); loadData(); } catch (Exception ex) { - Notification.show(getTranslation("jobs.notification.complete.error", ex.getMessage()), 5000, Notification.Position.BOTTOM_END) - .addThemeVariants(NotificationVariant.LUMO_ERROR); + Notification.show(getTranslation("jobs.notification.complete.error", ex.getMessage()), 5000, + Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_ERROR); } }); dialog.open(); @@ -226,8 +229,8 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle { Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_SUCCESS); loadData(); } catch (Exception ex) { - Notification.show(getTranslation("jobs.notification.delete.error", ex.getMessage()), 5000, Notification.Position.BOTTOM_END) - .addThemeVariants(NotificationVariant.LUMO_ERROR); + Notification.show(getTranslation("jobs.notification.delete.error", ex.getMessage()), 5000, + Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_ERROR); } }); dialog.open(); @@ -314,10 +317,9 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle { private String generateCsv(java.util.List jobs) { StringBuilder csv = new StringBuilder(); // CSV Header - 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.append(getTranslation("csv.header.customer")).append(",").append(getTranslation("csv.header.jobnumber")) + .append(",").append(getTranslation("csv.header.jobdate")).append(",") + .append(getTranslation("csv.header.destination")).append("\n"); // CSV Data for (Job job : jobs) { diff --git a/src/main/java/de/assecutor/votianlt/pages/view/StartView.java b/src/main/java/de/assecutor/votianlt/pages/view/StartView.java index e38dca9..de7e7b0 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/StartView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/StartView.java @@ -107,79 +107,79 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha } private Button createLanguageSelector() { - // Aktuelle Sprache aus der UI-Locale ermitteln (wird vom LocaleVaadinInitListener gesetzt) + // Aktuelle Sprache aus der UI-Locale ermitteln (wird vom + // LocaleVaadinInitListener gesetzt) String currentLang = getCurrentLanguageFromLocale(); String flag = getFlagForLanguage(currentLang); - + Button languageBtn = new Button(flag + " " + currentLang); languageBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY); - + ContextMenu languageMenu = new ContextMenu(); languageMenu.setOpenOnClick(true); languageMenu.setTarget(languageBtn); - + // Alle Sprachen hinzufügen for (Language lang : Language.values()) { String langFlag = getFlagForLanguage(lang.name()); String langText = langFlag + " " + lang.getDisplayName(); - + languageMenu.addItem(langText, event -> { setLanguageCookie(lang.name()); // Seite neu laden um die neue Sprache anzuwenden UI.getCurrent().getPage().reload(); }); } - + return languageBtn; } - + private String getCurrentLanguageFromLocale() { - // Aktuelle UI-Locale verwenden (wird vom LocaleVaadinInitListener aus Cookie/Browser gesetzt) + // Aktuelle UI-Locale verwenden (wird vom LocaleVaadinInitListener aus + // Cookie/Browser gesetzt) Locale locale = UI.getCurrent().getLocale(); if (locale == null) { return "DE"; } - + String language = locale.getLanguage(); return switch (language) { - case "en" -> "EN"; - case "fr" -> "FR"; - case "es" -> "ES"; - case "tr" -> "TR"; - case "pl" -> "PL"; - case "ru" -> "RU"; - case "et" -> "EE"; // Estnisch - case "lv" -> "LV"; // Lettisch - case "lt" -> "LT"; // Litauisch - default -> "DE"; // German as default + case "en" -> "EN"; + case "fr" -> "FR"; + case "es" -> "ES"; + case "tr" -> "TR"; + case "pl" -> "PL"; + case "ru" -> "RU"; + case "et" -> "EE"; // Estnisch + case "lv" -> "LV"; // Lettisch + case "lt" -> "LT"; // Litauisch + default -> "DE"; // German as default }; } - + private String getFlagForLanguage(String languageCode) { if (languageCode == null) { return "🇩🇪"; } return switch (languageCode.toUpperCase()) { - case "DE" -> "🇩🇪"; - case "EN" -> "🇬🇧"; - case "FR" -> "🇫🇷"; - case "ES" -> "🇪🇸"; - case "TR" -> "🇹🇷"; - case "PL" -> "🇵🇱"; - case "RU" -> "🇷🇺"; - case "EE" -> "🇪🇪"; - case "LV" -> "🇱🇻"; - case "LT" -> "🇱🇹"; - default -> "🇩🇪"; + case "DE" -> "🇩🇪"; + case "EN" -> "🇬🇧"; + case "FR" -> "🇫🇷"; + case "ES" -> "🇪🇸"; + case "TR" -> "🇹🇷"; + case "PL" -> "🇵🇱"; + case "RU" -> "🇷🇺"; + case "EE" -> "🇪🇪"; + case "LV" -> "🇱🇻"; + case "LT" -> "🇱🇹"; + default -> "🇩🇪"; }; } - + private void setLanguageCookie(String languageCode) { - UI.getCurrent().getPage().executeJs( - "const maxAge = 365 * 24 * 60 * 60;" + - "document.cookie = 'votianlt.language=' + $0 + ';path=/;max-age=' + maxAge + ';SameSite=Lax';", - languageCode - ); + UI.getCurrent().getPage().executeJs("const maxAge = 365 * 24 * 60 * 60;" + + "document.cookie = 'votianlt.language=' + $0 + ';path=/;max-age=' + maxAge + ';SameSite=Lax';", + languageCode); } private Component createAuthenticatedNavigation() { @@ -307,8 +307,9 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha featuresGrid.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.STRETCH); // Feature Cards - featuresGrid.add(createFeatureCard(VaadinIcon.COG, getTranslation("start.feature.setup.title"), - getTranslation("start.feature.setup.desc")), + featuresGrid.add( + createFeatureCard(VaadinIcon.COG, getTranslation("start.feature.setup.title"), + getTranslation("start.feature.setup.desc")), createFeatureCard(VaadinIcon.USERS, getTranslation("start.feature.customers.title"), getTranslation("start.feature.customers.desc")), createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, getTranslation("start.feature.jobs.title"), @@ -389,8 +390,10 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha companyInfo.setPadding(false); companyInfo.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - companyInfo.add(new Paragraph(getTranslation("start.imprint.company")), new Paragraph(getTranslation("start.imprint.address")), - new Paragraph(getTranslation("start.imprint.phone")), new Paragraph(getTranslation("start.imprint.email"))); + companyInfo.add(new Paragraph(getTranslation("start.imprint.company")), + new Paragraph(getTranslation("start.imprint.address")), + new Paragraph(getTranslation("start.imprint.phone")), + new Paragraph(getTranslation("start.imprint.email"))); // Call to Action Paragraph ctaText = new Paragraph(getTranslation("start.cta.text")); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java b/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java index d31a791..70119eb 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java @@ -135,8 +135,8 @@ public class UserMessagesView extends Main implements HasUrlParameter, H LocalDateTime lastMessageTime = latest != null ? latest.getCreatedAt() : null; String preview = resolvePreview(latest); - section.add(createMessageCard(getTranslation("usermessages.general.conversation"), preview, lastMessageTime, messageCount, unreadCount, - "general")); + section.add(createMessageCard(getTranslation("usermessages.general.conversation"), preview, lastMessageTime, + messageCount, unreadCount, "general")); return section; } @@ -233,9 +233,8 @@ public class UserMessagesView extends Main implements HasUrlParameter, H titleRow.expand(titleSpan); // Preview text - Span preview = new Span( - Optional.ofNullable(lastMessagePreview).filter(s -> !s.isBlank()) - .orElse(getTranslation("usermessages.preview.empty"))); + Span preview = new Span(Optional.ofNullable(lastMessagePreview).filter(s -> !s.isBlank()) + .orElse(getTranslation("usermessages.preview.empty"))); preview.getStyle().set("color", "#666666"); preview.getStyle().set("font-size", "14px"); diff --git a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java index 8e2d264..076c219 100644 --- a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java +++ b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java @@ -457,99 +457,110 @@ public class CustomerInvoiceService { */ private String generateServicesTableHtml(double widthMm) { StringBuilder html = new StringBuilder(); - + // Sample data for preview (will be replaced with actual job data later) - String[][] sampleData = { - {"Umzugsleistung inkl. Verpackung", "19%", "450,00 €"}, - {"Entsorgung Möbel", "19%", "85,00 €"}, - {"Montage/De-Montage", "19%", "120,00 €"} - }; - + String[][] sampleData = { { "Umzugsleistung inkl. Verpackung", "19%", "450,00 €" }, + { "Entsorgung Möbel", "19%", "85,00 €" }, { "Montage/De-Montage", "19%", "120,00 €" } }; + // Calculate totals double netTotal = 655.00; double vatTotal = 124.45; double grossTotal = 779.45; - + // Wrapper div html.append("
"); - + // Table html.append(""); - + // Header row html.append(""); - html.append(""); - html.append(""); - html.append(""); + html.append( + ""); + html.append( + ""); + html.append( + ""); html.append(""); - + // Data rows for (int i = 0; i < sampleData.length; i++) { String bgColor = (i % 2 == 1) ? "background-color:rgba(0,0,0,0.02);" : ""; html.append(""); - html.append(""); - html.append(""); - html.append(""); + html.append( + ""); + html.append(""); + html.append(""); html.append(""); } - + html.append("
NameSteuersatzNettobetragNameSteuersatzNettobetrag
").append(sampleData[i][0]).append("").append(sampleData[i][1]).append("").append(sampleData[i][2]).append("") + .append(sampleData[i][0]).append("").append(sampleData[i][1]) + .append("").append(sampleData[i][2]) + .append("
"); - + // Summary section html.append("
"); - + // Summary table for alignment with proper column separation html.append(""); - + // Nettosumme - label in col 2, value in col 3 html.append(""); html.append(""); html.append(""); - html.append(""); + html.append(""); html.append(""); - + // Umsatzsteuer - label in col 2, value in col 3 html.append(""); html.append(""); html.append(""); - html.append(""); + html.append(""); html.append(""); - + // Gesamtsumme - label in col 2, value in col 3 html.append(""); html.append(""); - html.append(""); - html.append(""); + html.append( + ""); + html.append( + ""); html.append(""); - + html.append("
Nettosumme:").append(String.format(java.util.Locale.GERMANY, "%,.2f €", netTotal)).append("") + .append(String.format(java.util.Locale.GERMANY, "%,.2f €", netTotal)).append("
zzgl. 19% USt:").append(String.format(java.util.Locale.GERMANY, "%,.2f €", vatTotal)).append("") + .append(String.format(java.util.Locale.GERMANY, "%,.2f €", vatTotal)).append("
Gesamtsumme:").append(String.format(java.util.Locale.GERMANY, "%,.2f €", grossTotal)).append("Gesamtsumme:") + .append(String.format(java.util.Locale.GERMANY, "%,.2f €", grossTotal)).append("
"); html.append("
"); html.append("
"); - + return html.toString(); } /** - * Generate a PDF preview from canvas template data with pre-built variables map. - * This version accepts a pre-built variables map instead of building it from the User object. + * Generate a PDF preview from canvas template data with pre-built variables + * map. This version accepts a pre-built variables map instead of building it + * from the User object. */ - public byte[] generatePdfFromCanvasTemplateWithData(String jsonTemplateData, + public byte[] generatePdfFromCanvasTemplateWithData(String jsonTemplateData, java.util.Map variables, de.assecutor.votianlt.model.User user) throws Exception { - + System.out.println("DEBUG: generatePdfFromCanvasTemplateWithData called"); System.out.println("DEBUG: templateData length: " + (jsonTemplateData != null ? jsonTemplateData.length() : 0)); - + // Parse the JSON template data - JSON is stored as a string in the database com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); - - // The template data is stored as a JSON string, so we need to parse it as a string first + + // The template data is stored as a JSON string, so we need to parse it as a + // string first String jsonContent = mapper.readValue(jsonTemplateData, String.class); com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonContent); - + System.out.println("DEBUG: rootNode has 'elements': " + rootNode.has("elements")); - + com.fasterxml.jackson.databind.JsonNode elements = rootNode.get("elements"); - + System.out.println("DEBUG: elements isArray: " + (elements != null ? elements.isArray() : false)); if (elements != null && !elements.isMissingNode()) { System.out.println("DEBUG: elements nodeType: " + elements.getNodeType()); @@ -576,10 +587,10 @@ public class CustomerInvoiceService { // Handle elements - could be an array or an object with element IDs as keys if (elements != null && !elements.isMissingNode() && !elements.isNull()) { int elementCount = 0; - + // Convert elements to a list we can iterate over java.util.List elementList = new java.util.ArrayList<>(); - + if (elements.isArray()) { // Already an array - iterate directly elements.forEach(elementList::add); @@ -590,16 +601,18 @@ public class CustomerInvoiceService { } else { System.out.println("DEBUG: Unexpected elements type: " + elements.getNodeType()); } - + for (com.fasterxml.jackson.databind.JsonNode element : elementList) { elementCount++; String type = element.has("type") ? element.get("type").asText("text") : "text"; String variable = element.has("variable") ? element.get("variable").asText(null) : null; String text = ""; - - System.out.println("DEBUG: Processing element " + elementCount + ", type=" + type + ", variable=" + variable); - // Use percentage values if available, otherwise fall back to legacy pixel values + System.out.println( + "DEBUG: Processing element " + elementCount + ", type=" + type + ", variable=" + variable); + + // Use percentage values if available, otherwise fall back to legacy pixel + // values double xPercent, yPercent, widthPercent, heightPercent; if (element.has("xPercent")) { xPercent = element.get("xPercent").asDouble(0); @@ -664,14 +677,15 @@ public class CustomerInvoiceService { if (element.has("text") && !element.get("text").asText("").isEmpty()) { text = element.get("text").asText(""); } - + // Replace variables with actual values if (variable != null && variables.containsKey(variable)) { text = variables.get(variable); System.out.println("DEBUG: Replaced variable " + variable + " with: " + text); } else if (variable != null) { // Variable exists but no value provided - use placeholder - text = "[" + variable.replace("masterdata.", "").replace("customer.", "").replace("invoice.", "").replace("job.", "") + "]"; + text = "[" + variable.replace("masterdata.", "").replace("customer.", "").replace("invoice.", "") + .replace("job.", "") + "]"; System.out.println("DEBUG: No value for variable " + variable + ", using placeholder"); } else { System.out.println("DEBUG: Using static text: " + text); @@ -718,7 +732,7 @@ public class CustomerInvoiceService { } htmlBuilder.append(""); - + String htmlOutput = htmlBuilder.toString(); System.out.println("DEBUG: Generated HTML length: " + htmlOutput.length()); System.out.println("DEBUG: HTML preview: " + htmlOutput.substring(0, Math.min(500, htmlOutput.length()))); @@ -731,45 +745,49 @@ public class CustomerInvoiceService { */ private String generateServicesTableHtmlWithData(double widthMm, java.util.Map variables) { StringBuilder html = new StringBuilder(); - + // Get invoice data from variables String netTotal = variables.getOrDefault("invoice.net_total", "0,00 €"); String vatTotal = variables.getOrDefault("invoice.vat_total", "0,00 €"); String grossTotal = variables.getOrDefault("invoice.gross_total", "0,00 €"); String vatRate = variables.getOrDefault("invoice.vat_rate", "19%"); - + // Parse services JSON from variables java.util.List> servicesData = new java.util.ArrayList<>(); String servicesJson = variables.get("services.json"); if (servicesJson != null && !servicesJson.isEmpty() && !servicesJson.equals("[]")) { try { com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); - com.fasterxml.jackson.core.type.TypeReference>> typeRef = - new com.fasterxml.jackson.core.type.TypeReference<>() {}; + com.fasterxml.jackson.core.type.TypeReference>> typeRef = new com.fasterxml.jackson.core.type.TypeReference<>() { + }; servicesData = mapper.readValue(servicesJson, typeRef); } catch (Exception e) { System.err.println("DEBUG: Failed to parse services JSON: " + e.getMessage()); } } - + // Wrapper div html.append("
"); - + // Table html.append(""); - + // Header row html.append(""); - html.append(""); - html.append(""); - html.append(""); + html.append( + ""); + html.append( + ""); + html.append( + ""); html.append(""); - + // Data rows - use actual service data from the job if (servicesData.isEmpty()) { // Fallback: show a single row with no data html.append(""); - html.append(""); + html.append( + ""); html.append(""); } else { for (int i = 0; i < servicesData.size(); i++) { @@ -777,53 +795,63 @@ public class CustomerInvoiceService { String name = service.getOrDefault("name", "Unbekannte Leistung"); String serviceVatRate = service.getOrDefault("vatRate", vatRate); String netAmount = service.getOrDefault("netAmount", "0,00"); - + String bgColor = (i % 2 == 1) ? "background-color:rgba(0,0,0,0.02);" : ""; html.append(""); - html.append(""); - html.append(""); - html.append(""); + html.append( + ""); + html.append(""); + html.append(""); html.append(""); } } - + html.append("
NameSteuersatzNettobetragNameSteuersatzNettobetrag
Keine Leistungen vorhandenKeine Leistungen vorhanden
").append(escapeHtml(name)).append("").append(serviceVatRate).append("").append(netAmount).append(" €") + .append(escapeHtml(name)).append("").append(serviceVatRate) + .append("").append(netAmount) + .append(" €
"); - + // Summary section with actual totals from variables html.append("
"); html.append(""); - + // Extract numeric value from VAT rate for display String vatPercent = vatRate.replace(" %", "").replace("%", ""); - + // Nettosumme html.append(""); html.append(""); html.append(""); - html.append(""); + html.append(""); html.append(""); - + // Umsatzsteuer html.append(""); html.append(""); - html.append(""); - html.append(""); + html.append(""); + html.append(""); html.append(""); - + // Gesamtsumme html.append(""); html.append(""); - html.append(""); - html.append(""); + html.append( + ""); + html.append( + ""); html.append(""); - + html.append("
Nettosumme:").append(netTotal).append("") + .append(netTotal).append("
zzgl. ").append(vatPercent).append("% USt:").append(vatTotal).append("zzgl. ") + .append(vatPercent).append("% USt:") + .append(vatTotal).append("
Gesamtsumme:").append(grossTotal).append("Gesamtsumme:") + .append(grossTotal).append("
"); html.append("
"); html.append("
"); - + return html.toString(); } - + /** * Escape HTML special characters to prevent XSS. */ @@ -831,10 +859,7 @@ public class CustomerInvoiceService { if (input == null) { return ""; } - return input.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'"); + return input.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """) + .replace("'", "'"); } } diff --git a/src/main/java/de/assecutor/votianlt/service/LanguageService.java b/src/main/java/de/assecutor/votianlt/service/LanguageService.java index 6f5ab50..2e5caea 100644 --- a/src/main/java/de/assecutor/votianlt/service/LanguageService.java +++ b/src/main/java/de/assecutor/votianlt/service/LanguageService.java @@ -33,20 +33,20 @@ public class LanguageService { try { Locale locale; switch (language) { - case DE: - locale = Locale.GERMAN; - break; - case EN: - locale = Locale.ENGLISH; - break; - case FR: - locale = Locale.FRENCH; - break; + case DE: + locale = Locale.GERMAN; + break; + case EN: + locale = Locale.ENGLISH; + break; + case FR: + locale = Locale.FRENCH; + break; case ES: locale = Locale.of("es", "ES"); break; - default: - locale = Locale.GERMAN; + default: + locale = Locale.GERMAN; } ResourceBundle bundle = ResourceBundle.getBundle("messages", locale); @@ -74,11 +74,11 @@ public class LanguageService { 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"); - default -> Locale.GERMAN; + case DE -> Locale.GERMAN; + case EN -> Locale.ENGLISH; + case FR -> Locale.FRENCH; + case ES -> Locale.of("es", "ES"); + default -> Locale.GERMAN; }; }