Erweiterungen

This commit is contained in:
2026-02-20 09:14:36 +01:00
parent 1c9c1c67e1
commit a4c3c67f8a
30 changed files with 821 additions and 647 deletions

View File

@@ -39,18 +39,18 @@ export function clearLanguageCookie(): void {
* Maps Language enum values to locale strings * Maps Language enum values to locale strings
*/ */
export const languageToLocale: Record<string, string> = { export const languageToLocale: Record<string, string> = {
'DE': 'de', DE: 'de',
'EN': 'en', EN: 'en',
'FR': 'fr', FR: 'fr',
'ES': 'es' ES: 'es',
}; };
/** /**
* Maps locale strings to Language enum values * Maps locale strings to Language enum values
*/ */
export const localeToLanguage: Record<string, string> = { export const localeToLanguage: Record<string, string> = {
'de': 'DE', de: 'DE',
'en': 'EN', en: 'EN',
'fr': 'FR', fr: 'FR',
'es': 'ES' es: 'ES',
}; };

View File

@@ -16,12 +16,13 @@ import java.util.Locale;
/** /**
* Sets the user's preferred locale on the UI BEFORE any layout or view is * Sets the user's preferred locale on the UI BEFORE any layout or view is
* constructed. Registered via {@code UIInitListener} → {@code BeforeEnterListener}, * constructed. Registered via {@code UIInitListener} →
* which fires prior to the router creating the layout component tree. * {@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 authenticated users: Uses the language preference from the user profile.
* For anonymous users: Uses the language from the 'votianlt.language' cookie * For anonymous users: Uses the language from the 'votianlt.language' cookie or
* or falls back to the browser's preferred locale. * falls back to the browser's preferred locale.
*/ */
@Component @Component
@Slf4j @Slf4j

View File

@@ -4,6 +4,7 @@ import com.vaadin.flow.i18n.I18NProvider;
import de.assecutor.votianlt.model.Language; import de.assecutor.votianlt.model.Language;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.text.MessageFormat;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -22,20 +23,19 @@ public class TranslationProvider implements I18NProvider {
public List<Locale> getCandidateLocales(String baseName, Locale locale) { public List<Locale> getCandidateLocales(String baseName, Locale locale) {
// Map Estonian "et" to "ee" file, Latvian "lv" to "lv", Lithuanian "lt" to "lt" // Map Estonian "et" to "ee" file, Latvian "lv" to "lv", Lithuanian "lt" to "lt"
String language = locale.getLanguage(); String language = locale.getLanguage();
String country = locale.getCountry();
// Create a locale that matches our file naming convention // Create a locale that matches our file naming convention
Locale mappedLocale = switch (language) { Locale mappedLocale = switch (language) {
case "et" -> new Locale("ee"); // Estonian -> messages_ee.properties case "et" -> Locale.of("ee"); // Estonian -> messages_ee.properties
case "lv" -> new Locale("lv"); // Latvian -> messages_lv.properties case "lv" -> Locale.of("lv"); // Latvian -> messages_lv.properties
case "lt" -> new Locale("lt"); // Lithuanian -> messages_lt.properties case "lt" -> Locale.of("lt"); // Lithuanian -> messages_lt.properties
case "ru" -> new Locale("ru"); // Russian -> messages_ru.properties case "ru" -> Locale.of("ru"); // Russian -> messages_ru.properties
case "pl" -> new Locale("pl"); // Polish -> messages_pl.properties case "pl" -> Locale.of("pl"); // Polish -> messages_pl.properties
case "tr" -> new Locale("tr"); // Turkish -> messages_tr.properties case "tr" -> Locale.of("tr"); // Turkish -> messages_tr.properties
case "es" -> new Locale("es"); // Spanish -> messages_es.properties case "es" -> Locale.of("es"); // Spanish -> messages_es.properties
case "fr" -> new Locale("fr"); // French -> messages_fr.properties case "fr" -> Locale.of("fr"); // French -> messages_fr.properties
case "en" -> new Locale("en"); // English -> messages_en.properties case "en" -> Locale.of("en"); // English -> messages_en.properties
case "de" -> new Locale("de"); // German -> messages.properties (default) case "de" -> Locale.of("de"); // German -> messages.properties (default)
default -> locale; default -> locale;
}; };
@@ -45,18 +45,9 @@ public class TranslationProvider implements I18NProvider {
@Override @Override
public List<Locale> getProvidedLocales() { public List<Locale> getProvidedLocales() {
return Collections.unmodifiableList(Arrays.asList( return Collections.unmodifiableList(Arrays.asList(Locale.GERMAN, Locale.ENGLISH, Locale.FRENCH,
Locale.GERMAN, Locale.of("es", "ES"), Locale.of("tr", "TR"), Locale.of("pl", "PL"), Locale.of("ru", "RU"),
Locale.ENGLISH, Locale.of("et", "EE"), Locale.of("lv", "LV"), Locale.of("lt", "LT")));
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 @Override
@@ -70,7 +61,7 @@ public class TranslationProvider implements I18NProvider {
String value = bundle.getString(key); String value = bundle.getString(key);
if (params.length > 0) { if (params.length > 0) {
value = String.format(value, params); value = MessageFormat.format(value, params);
} }
return value; return value;

View File

@@ -111,8 +111,10 @@ public final class MainLayout extends AppLayout {
TreeData<MenuTreeItem> treeData = new TreeData<>(); TreeData<MenuTreeItem> treeData = new TreeData<>();
// Root nodes // Root nodes
MenuTreeItem auftragserstellungItem = new MenuTreeItem(getTranslation("nav.job.create"), "add_job", VaadinIcon.PLUS_CIRCLE); MenuTreeItem auftragserstellungItem = new MenuTreeItem(getTranslation("nav.job.create"), "add_job",
MenuTreeItem nachrichtenItem = new MenuTreeItem(getTranslation("nav.messages"), "messages", VaadinIcon.ENVELOPE); VaadinIcon.PLUS_CIRCLE);
MenuTreeItem nachrichtenItem = new MenuTreeItem(getTranslation("nav.messages"), "messages",
VaadinIcon.ENVELOPE);
MenuTreeItem verwaltungItem = new MenuTreeItem(getTranslation("nav.management"), null, VaadinIcon.COG); MenuTreeItem verwaltungItem = new MenuTreeItem(getTranslation("nav.management"), null, VaadinIcon.COG);
MenuTreeItem benutzerItem = new MenuTreeItem(getTranslation("nav.users"), null, VaadinIcon.USER); MenuTreeItem benutzerItem = new MenuTreeItem(getTranslation("nav.users"), null, VaadinIcon.USER);
@@ -126,19 +128,26 @@ public final class MainLayout extends AppLayout {
treeData.addItem(null, benutzerItem); treeData.addItem(null, benutzerItem);
// Add children to "Verwaltung" // Add children to "Verwaltung"
treeData.addItem(verwaltungItem, new MenuTreeItem(getTranslation("nav.customers"), "customers", VaadinIcon.USERS)); treeData.addItem(verwaltungItem,
treeData.addItem(verwaltungItem, new MenuTreeItem(getTranslation("nav.appusers"), "app-user", VaadinIcon.USERS)); new MenuTreeItem(getTranslation("nav.customers"), "customers", VaadinIcon.USERS));
treeData.addItem(verwaltungItem, new MenuTreeItem(getTranslation("nav.statistics"), "statistics", VaadinIcon.BAR_CHART)); 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 // Add invoices only if billing is enabled
if (isBillingEnabledForCurrentUser()) { 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" // Add children to "Benutzer"
treeData.addItem(benutzerItem, new MenuTreeItem(getTranslation("nav.profile"), "edit-profile", VaadinIcon.USER)); treeData.addItem(benutzerItem,
treeData.addItem(benutzerItem, new MenuTreeItem(getTranslation("nav.myinvoices"), "my-invoices", VaadinIcon.FILE_TEXT)); new MenuTreeItem(getTranslation("nav.profile"), "edit-profile", VaadinIcon.USER));
treeData.addItem(benutzerItem, new MenuTreeItem(getTranslation("nav.imprint"), "impressum", VaadinIcon.INFO_CIRCLE)); 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 // Create Tree
tree = new TreeGrid<>(); tree = new TreeGrid<>();
@@ -223,16 +232,10 @@ public final class MainLayout extends AppLayout {
TreeData<MenuTreeItem> treeData = dataProvider.getTreeData(); TreeData<MenuTreeItem> treeData = dataProvider.getTreeData();
// Find and update the messages item with new badge count // Find and update the messages item with new badge count
treeData.getChildren(null).stream() treeData.getChildren(null).stream().filter(item -> "messages".equals(item.path())).findFirst()
.filter(item -> "messages".equals(item.path()))
.findFirst()
.ifPresent(oldItem -> { .ifPresent(oldItem -> {
MenuTreeItem newItem = new MenuTreeItem( MenuTreeItem newItem = new MenuTreeItem(getTranslation("nav.messages"), "messages",
getTranslation("nav.messages"), VaadinIcon.ENVELOPE, unreadCount);
"messages",
VaadinIcon.ENVELOPE,
unreadCount
);
messagesTreeItem = newItem; messagesTreeItem = newItem;
// Refresh to show updated badge // Refresh to show updated badge
dataProvider.refreshAll(); dataProvider.refreshAll();
@@ -249,8 +252,10 @@ public final class MainLayout extends AppLayout {
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o)
if (o == null || getClass() != o.getClass()) return false; return true;
if (o == null || getClass() != o.getClass())
return false;
MenuTreeItem that = (MenuTreeItem) o; MenuTreeItem that = (MenuTreeItem) o;
return Objects.equals(label, that.label) && Objects.equals(path, that.path); return Objects.equals(label, that.label) && Objects.equals(path, that.path);
} }
@@ -307,7 +312,8 @@ public final class MainLayout extends AppLayout {
userMenuItem.add(userNameSpan); userMenuItem.add(userNameSpan);
// Profil anzeigen mit Navigation // 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.settings"));
userMenuItem.getSubMenu().addItem(getTranslation("nav.logout"), e -> securityService.logout()); 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. * Applies the user's preferred language if it differs from the current UI
* The primary locale setup happens in {@code LocaleVaadinInitListener} before the * locale. The primary locale setup happens in {@code LocaleVaadinInitListener}
* layout is constructed. This method handles edge cases such as language changes * before the layout is constructed. This method handles edge cases such as
* within an active session. * language changes within an active session.
*/ */
private void applyUserLanguagePreference() { private void applyUserLanguagePreference() {
try { try {

View File

@@ -90,11 +90,13 @@ public class AddAppUserView extends VerticalLayout implements HasDynamicTitle {
designationField.setPlaceholder("(HH H 000)"); designationField.setPlaceholder("(HH H 000)");
designationField.setWidthFull(); designationField.setWidthFull();
designationField.setRequiredIndicatorVisible(true); designationField.setRequiredIndicatorVisible(true);
designationField.addBlurListener(e -> validateField(designationField, getTranslation("addappuser.validation.designation"))); designationField.addBlurListener(
e -> validateField(designationField, getTranslation("addappuser.validation.designation")));
firstnameField.setWidthFull(); firstnameField.setWidthFull();
firstnameField.setRequiredIndicatorVisible(true); firstnameField.setRequiredIndicatorVisible(true);
firstnameField.addBlurListener(e -> validateField(firstnameField, getTranslation("profile.validation.firstname"))); firstnameField
.addBlurListener(e -> validateField(firstnameField, getTranslation("profile.validation.firstname")));
lastnameField.setWidthFull(); lastnameField.setWidthFull();
lastnameField.setRequiredIndicatorVisible(true); lastnameField.setRequiredIndicatorVisible(true);
@@ -162,8 +164,8 @@ public class AddAppUserView extends VerticalLayout implements HasDynamicTitle {
binder.forField(lastnameField).bind(AppUser::getNachname, AppUser::setNachname); binder.forField(lastnameField).bind(AppUser::getNachname, AppUser::setNachname);
binder.forField(phoneField).bind(AppUser::getTelefon, AppUser::setTelefon); binder.forField(phoneField).bind(AppUser::getTelefon, AppUser::setTelefon);
binder.forField(emailField).bind(AppUser::getEmail, AppUser::setEmail); binder.forField(emailField).bind(AppUser::getEmail, AppUser::setEmail);
binder.forField(passwordField).asRequired(getTranslation("addappuser.validation.password.required")).bind(AppUser::getPassword, binder.forField(passwordField).asRequired(getTranslation("addappuser.validation.password.required"))
AppUser::setPassword); .bind(AppUser::getPassword, AppUser::setPassword);
// Confirm password field validation // Confirm password field validation
binder.forField(confirmPasswordField).asRequired(getTranslation("addappuser.validation.password.confirm")) 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) { } catch (org.springframework.dao.DuplicateKeyException e) {
// Handle duplicate email error // Handle duplicate email error
if (e.getMessage().contains("email")) { if (e.getMessage().contains("email")) {
Notification.show(getTranslation("addappuser.notification.email.duplicate"), 5000, Notification.Position.MIDDLE); Notification.show(getTranslation("addappuser.notification.email.duplicate"), 5000,
Notification.Position.MIDDLE);
emailField.focus(); emailField.focus();
emailField.setInvalid(true); emailField.setInvalid(true);
emailField.setErrorMessage(getTranslation("addappuser.notification.email.duplicate")); emailField.setErrorMessage(getTranslation("addappuser.notification.email.duplicate"));
} else { } else {
Notification.show(getTranslation("addappuser.notification.check"), 5000, Notification.show(getTranslation("addappuser.notification.check"), 5000, Notification.Position.MIDDLE);
Notification.Position.MIDDLE);
} }
} catch (Exception e) { } catch (Exception e) {
Notification.show(getTranslation("addappuser.notification.error", e.getMessage()), 5000, Notification.show(getTranslation("addappuser.notification.error", e.getMessage()), 5000,

View File

@@ -54,7 +54,8 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
// Anrede (Dropdown) // Anrede (Dropdown)
title = new ComboBox<>(getTranslation("addjob.address.salutation")); 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.setPlaceholder(getTranslation("addjob.address.salutation.placeholder"));
title.setWidthFull(); title.setWidthFull();
@@ -160,16 +161,16 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
} }
private void configureBinder() { private void configureBinder() {
binder.forField(companyName).asRequired(getTranslation("profile.validation.company.required")).bind(Customer::getCompanyName, binder.forField(companyName).asRequired(getTranslation("profile.validation.company.required"))
Customer::setCompanyName); .bind(Customer::getCompanyName, Customer::setCompanyName);
binder.forField(title).bind(Customer::getTitle, Customer::setTitle); binder.forField(title).bind(Customer::getTitle, Customer::setTitle);
binder.forField(firstName).asRequired(getTranslation("profile.validation.firstname.required")).bind(Customer::getFirstname, binder.forField(firstName).asRequired(getTranslation("profile.validation.firstname.required"))
Customer::setFirstname); .bind(Customer::getFirstname, Customer::setFirstname);
binder.forField(lastName).asRequired(getTranslation("profile.validation.lastname.required")).bind(Customer::getLastName, binder.forField(lastName).asRequired(getTranslation("profile.validation.lastname.required"))
Customer::setLastName); .bind(Customer::getLastName, Customer::setLastName);
binder.forField(telephone).asRequired(getTranslation("profile.validation.phone")).bind(Customer::getTelephone, binder.forField(telephone).asRequired(getTranslation("profile.validation.phone")).bind(Customer::getTelephone,
Customer::setTelephone); Customer::setTelephone);
@@ -180,16 +181,19 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
.withValidator(email -> email.contains("@"), getTranslation("profile.validation.email.invalid")) .withValidator(email -> email.contains("@"), getTranslation("profile.validation.email.invalid"))
.bind(Customer::getMail, Customer::setMail); .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, binder.forField(houseNumber).asRequired(getTranslation("profile.validation.housenr.required"))
Customer::setHouseNumber); .bind(Customer::getHouseNumber, Customer::setHouseNumber);
binder.forField(addressAddition).bind(Customer::getAddressAddition, Customer::setAddressAddition); 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() { private void setTestData() {
@@ -201,7 +205,8 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
boolean isValid = validateAllFields(); boolean isValid = validateAllFields();
if (!isValid) { 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); com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
return; return;
} }
@@ -212,16 +217,17 @@ public class AddCustomerView extends Main implements HasDynamicTitle {
addCustomerService.addCustomer(customer); addCustomerService.addCustomer(customer);
com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.success"), 3000, com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.success"),
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER); 3000, com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
getUI().ifPresent(ui -> ui.navigate("customers")); getUI().ifPresent(ui -> ui.navigate("customers"));
} catch (ValidationException e) { } catch (ValidationException e) {
com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.check"), 3000, com.vaadin.flow.component.notification.Notification.show(getTranslation("addcustomer.notification.check"),
com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER); 3000, com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
} catch (Exception e) { } 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); com.vaadin.flow.component.notification.Notification.Position.TOP_CENTER);
} }
} }

View File

@@ -320,7 +320,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
pickupCompany.setAllowCustomValue(true); pickupCompany.setAllowCustomValue(true);
setupCompanyAutocomplete(pickupCompany, true); // true für Pickup setupCompanyAutocomplete(pickupCompany, true); // true für Pickup
pickupSalutation = new ComboBox<>(getTranslation("addjob.address.salutation")); 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")); pickupSalutation.setPlaceholder(getTranslation("addjob.address.salutation.placeholder"));
pickupFirstName = new TextField(getTranslation("profile.firstname")); pickupFirstName = new TextField(getTranslation("profile.firstname"));
pickupFirstName.setPlaceholder(getTranslation("profile.firstname")); pickupFirstName.setPlaceholder(getTranslation("profile.firstname"));
@@ -353,7 +354,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
deliveryCompany.setAllowCustomValue(true); deliveryCompany.setAllowCustomValue(true);
setupCompanyAutocomplete(deliveryCompany, false); // false für Delivery setupCompanyAutocomplete(deliveryCompany, false); // false für Delivery
deliverySalutation = new ComboBox<>(getTranslation("addjob.address.salutation")); 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")); deliverySalutation.setPlaceholder(getTranslation("addjob.address.salutation.placeholder"));
deliveryFirstName = new TextField(getTranslation("profile.firstname")); deliveryFirstName = new TextField(getTranslation("profile.firstname"));
deliveryFirstName.setPlaceholder(getTranslation("profile.firstname")); deliveryFirstName.setPlaceholder(getTranslation("profile.firstname"));
@@ -678,7 +680,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
manualInputRow.setWidthFull(); manualInputRow.setWidthFull();
manualInputRow.setSpacing(true); 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.setWidthFull();
manualDistanceInput.setPlaceholder(getTranslation("addjob.route.distance.placeholder")); manualDistanceInput.setPlaceholder(getTranslation("addjob.route.distance.placeholder"));
manualDistanceInput.setMin(0); 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.setWidthFull();
manualDurationInput.setPlaceholder(getTranslation("addjob.route.duration.placeholder")); manualDurationInput.setPlaceholder(getTranslation("addjob.route.duration.placeholder"));
manualDurationInput.setMin(0); manualDurationInput.setMin(0);
@@ -740,7 +744,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
} }
// Show price info if no route calculated yet // Show price info if no route calculated yet
if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE && routeDistance == null) { if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE && routeDistance == null) {
return service.getPricePerKilometer().setScale(2, RoundingMode.HALF_UP) + " €/km (" + getTranslation("addjob.services.route.missing") + ")"; return service.getPricePerKilometer().setScale(2, RoundingMode.HALF_UP) + " €/km ("
+ getTranslation("addjob.services.route.missing") + ")";
} }
return service.getEffectivePrice() != null return service.getEffectivePrice() != null
? service.getEffectivePrice().setScale(2, RoundingMode.HALF_UP) + "" ? service.getEffectivePrice().setScale(2, RoundingMode.HALF_UP) + ""
@@ -937,7 +942,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
if (service.getPricePer15Minutes() != null && durationSeconds != null && durationSeconds > 0) { if (service.getPricePer15Minutes() != null && durationSeconds != null && durationSeconds > 0) {
// Dauer in 15-Minuten-Einheiten umrechnen (aufrunden) // Dauer in 15-Minuten-Einheiten umrechnen (aufrunden)
int units = durationSeconds / 900; // 900 Sekunden = 15 Minuten 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 service.getPricePer15Minutes().multiply(BigDecimal.valueOf(units));
} }
return BigDecimal.ZERO; return BigDecimal.ZERO;
@@ -1245,8 +1251,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
boolean digital = Boolean.TRUE.equals(digitalProcessing.getValue()); boolean digital = Boolean.TRUE.equals(digitalProcessing.getValue());
boolean hasUser = selectedUserId != null && !selectedUserId.trim().isEmpty(); boolean hasUser = selectedUserId != null && !selectedUserId.trim().isEmpty();
return !digital || hasUser; return !digital || hasUser;
}, getTranslation("addjob.validation.appuser.required")) }, getTranslation("addjob.validation.appuser.required")).bind(Job::getAppUser, Job::setAppUser);
.bind(Job::getAppUser, Job::setAppUser);
// Toggle required indicator and visibility for App-Nutzer based on // Toggle required indicator and visibility for App-Nutzer based on
// digitalProcessing // digitalProcessing
@@ -1525,14 +1530,15 @@ public class AddJobView extends Main implements HasDynamicTitle {
// Manuelle Eingabe verwenden // Manuelle Eingabe verwenden
job.setRouteDistanceKm(manualDistanceInput.getValue()); job.setRouteDistanceKm(manualDistanceInput.getValue());
if (manualDurationInput != null && manualDurationInput.getValue() != null) { 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 // Additional validation: If digital processing is enabled, app user must be
// selected // selected
if (digitalProcessing.getValue() && appUser.getValue() == null) { if (digitalProcessing.getValue() && appUser.getValue() == null) {
Notification errorNotification = Notification.show( Notification errorNotification = Notification
getTranslation("addjob.validation.appuser.required")); .show(getTranslation("addjob.validation.appuser.required"));
errorNotification.setDuration(5000); errorNotification.setDuration(5000);
return; return;
} }
@@ -1600,8 +1606,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
getUI().ifPresent(ui -> ui.navigate(JobSummaryView.class, savedJob.getId().toHexString())); getUI().ifPresent(ui -> ui.navigate(JobSummaryView.class, savedJob.getId().toHexString()));
} else { } else {
// Validation failed, show error message // Validation failed, show error message
Notification errorNotification = Notification Notification errorNotification = Notification.show(getTranslation("addjob.validation.required.fields"));
.show(getTranslation("addjob.validation.required.fields"));
errorNotification.setDuration(5000); errorNotification.setDuration(5000);
} }
@@ -1612,7 +1617,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
cargoError.setVisible(false); cargoError.setVisible(false);
if (cargoAreaContainer != null) if (cargoAreaContainer != null)
cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
Notification errorNotification = Notification.show(getTranslation("addjob.notification.error", e.getMessage())); Notification errorNotification = Notification
.show(getTranslation("addjob.notification.error", e.getMessage()));
errorNotification.setDuration(5000); errorNotification.setDuration(5000);
} }
} }
@@ -1659,8 +1665,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
loadJobIntoForm(draft); loadJobIntoForm(draft);
// Benutzer informieren // Benutzer informieren
Notification notification = Notification Notification notification = Notification.show(getTranslation("addjob.notification.draft.restored"));
.show(getTranslation("addjob.notification.draft.restored"));
notification.setDuration(4000); notification.setDuration(4000);
} }
} }
@@ -1712,7 +1717,9 @@ public class AddJobView extends Main implements HasDynamicTitle {
row.setAlignItems(FlexComponent.Alignment.END); row.setAlignItems(FlexComponent.Alignment.END);
ComboBox<String> desc = new ComboBox<>(getTranslation("addjob.cargo.description")); ComboBox<String> desc = new ComboBox<>(getTranslation("addjob.cargo.description"));
desc.setItems(getTranslation("addjob.cargo.europalette"), getTranslation("addjob.cargo.disposablepalette"), getTranslation("addjob.cargo.dusseldorfpalette"), getTranslation("addjob.cargo.gridboxpalette"), getTranslation("addjob.cargo.gridcart"), getTranslation("addjob.cargo.parcel")); desc.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.setAllowCustomValue(true);
desc.setPlaceholder(getTranslation("addjob.cargo.description.placeholder")); desc.setPlaceholder(getTranslation("addjob.cargo.description.placeholder"));
desc.setWidth("40%"); desc.setWidth("40%");
@@ -2510,7 +2517,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
try { try {
// Check if there are any tasks to save // Check if there are any tasks to save
if (tasksState.isEmpty()) { if (tasksState.isEmpty()) {
Notification.show(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; return;
} }
@@ -2577,7 +2585,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
} catch (Exception e) { } catch (Exception e) {
log.error("Error opening save template dialog", 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); templateComboBox.setItems(templates);
} catch (Exception e) { } catch (Exception e) {
log.error("Error loading templates", 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 // Abholadresse anzeigen
if (pickupResult != null) { if (pickupResult != null) {
if (pickupResult.isValid()) { 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)"); pickupResultLabel.getStyle().set("color", "var(--lumo-success-text-color)");
} else { } 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)"); pickupResultLabel.getStyle().set("color", "var(--lumo-error-text-color)");
hasInvalidAddress = true; hasInvalidAddress = true;
bothAddressesValid = false; bothAddressesValid = false;
@@ -3090,10 +3102,12 @@ public class AddJobView extends Main implements HasDynamicTitle {
// Lieferadresse anzeigen // Lieferadresse anzeigen
if (deliveryResult != null) { if (deliveryResult != null) {
if (deliveryResult.isValid()) { 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)"); deliveryResultLabel.getStyle().set("color", "var(--lumo-success-text-color)");
} else { } 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)"); deliveryResultLabel.getStyle().set("color", "var(--lumo-error-text-color)");
hasInvalidAddress = true; hasInvalidAddress = true;
bothAddressesValid = false; bothAddressesValid = false;
@@ -3104,8 +3118,10 @@ public class AddJobView extends Main implements HasDynamicTitle {
// Route anzeigen, wenn beide Adressen gültig sind // Route anzeigen, wenn beide Adressen gültig sind
if (bothAddressesValid && routeCalculationResult != null && routeCalculationResult.isValid()) { if (bothAddressesValid && routeCalculationResult != null && routeCalculationResult.isValid()) {
routeResultLabel.setText("🚛 " + getTranslation("addjob.validation.route") + ": " + String.format("%.1f km", routeCalculationResult.getDistanceKm()) routeResultLabel.setText("🚛 " + getTranslation("addjob.validation.route") + ": "
+ " (" + getTranslation("addjob.route.duration") + ": " + routeCalculationResult.getFormattedDurationLong() + ")"); + String.format("%.1f km", routeCalculationResult.getDistanceKm()) + " ("
+ getTranslation("addjob.route.duration") + ": " + routeCalculationResult.getFormattedDurationLong()
+ ")");
routeResultLabel.getStyle().set("color", "var(--lumo-primary-text-color)"); routeResultLabel.getStyle().set("color", "var(--lumo-primary-text-color)");
routeResultLabel.setVisible(true); routeResultLabel.setVisible(true);
} else { } else {
@@ -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() { public double getRouteDistanceKm() {
if (routeCalculationResult != null && routeCalculationResult.isValid()) { if (routeCalculationResult != null && routeCalculationResult.isValid()) {

View File

@@ -148,19 +148,23 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
// Total jobs card // Total jobs card
long totalJobs = jobRepository.count(); 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 // Total users card
long totalUsers = userRepository.count(); 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 // Total app users card
long totalAppUsers = appUserRepository.count(); 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 // Current time
String currentTime = DateTimeFormatUtil.formatDateTime(LocalDateTime.now()); 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); section.add(title, cards);
return section; return section;
@@ -185,17 +189,22 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
long inProgressJobs = jobRepository.countByStatus(JobStatus.IN_PROGRESS); long inProgressJobs = jobRepository.countByStatus(JobStatus.IN_PROGRESS);
long completedJobs = jobRepository.countByStatus(JobStatus.COMPLETED); long completedJobs = jobRepository.countByStatus(JobStatus.COMPLETED);
cards.add(createStatCard(getTranslation("admindashboard.stat.openjobs"), String.valueOf(openJobs), VaadinIcon.HOURGLASS_START, "orange")); cards.add(createStatCard(getTranslation("admindashboard.stat.openjobs"), String.valueOf(openJobs),
cards.add(createStatCard(getTranslation("admindashboard.stat.inprogress"), String.valueOf(inProgressJobs), VaadinIcon.PLAY, "blue")); VaadinIcon.HOURGLASS_START, "orange"));
cards.add(createStatCard(getTranslation("admindashboard.stat.completed"), String.valueOf(completedJobs), VaadinIcon.CHECK_CIRCLE, "green")); 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 // Total cargo items
long totalCargoItems = cargoItemRepository.count(); 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) { } catch (Exception e) {
log.warn("Could not load job statistics by status", e); log.warn("Could not load job statistics by status", e);
cards.add(createStatCard(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); section.add(title, cards);
@@ -217,20 +226,23 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
// Total tasks // Total tasks
long totalTasks = taskRepository.count(); 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 // Completed tasks
long completedTasks = taskRepository.countByCompleted(true); 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 // Pending tasks
long pendingTasks = totalTasks - completedTasks; 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 // Completion rate
double completionRate = totalTasks > 0 ? (completedTasks * 100.0 / totalTasks) : 0; double completionRate = totalTasks > 0 ? (completedTasks * 100.0 / totalTasks) : 0;
cards.add(createStatCard(getTranslation("admindashboard.stat.successrate"), String.format("%.1f%%", completionRate), VaadinIcon.TRENDING_UP, cards.add(createStatCard(getTranslation("admindashboard.stat.successrate"),
"purple")); String.format("%.1f%%", completionRate), VaadinIcon.TRENDING_UP, "purple"));
section.add(title, cards); section.add(title, cards);
return section; return section;
@@ -251,16 +263,20 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
// Content statistics // Content statistics
long totalPhotos = photoRepository.count(); 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(); 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(); 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(); 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); section.add(title, cards);
return section; return section;
@@ -282,16 +298,20 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
// Database connection status // Database connection status
try { try {
userRepository.count(); // Test database connection 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) { } 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 // 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) // 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) // Memory usage (placeholder)
Runtime runtime = Runtime.getRuntime(); Runtime runtime = Runtime.getRuntime();

View File

@@ -75,9 +75,11 @@ public class AdminPricetableView extends VerticalLayout implements HasDynamicTit
priceTable.setRevenueParticipation(revenueParticipation.getValue()); priceTable.setRevenueParticipation(revenueParticipation.getValue());
priceTableRepository.save(priceTable); 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) { } 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);
} }
} }

View File

@@ -52,11 +52,15 @@ public class AppUserView extends VerticalLayout implements HasDynamicTitle {
appUserGrid.setSizeFull(); appUserGrid.setSizeFull();
// Grid-Spalten konfigurieren // Grid-Spalten konfigurieren
appUserGrid.addColumn(AppUser::getBezeichnung).setHeader(getTranslation("appuser.column.designation")).setAutoWidth(true); appUserGrid.addColumn(AppUser::getBezeichnung).setHeader(getTranslation("appuser.column.designation"))
appUserGrid.addColumn(AppUser::getVorname).setHeader(getTranslation("appuser.column.firstname")).setAutoWidth(true); .setAutoWidth(true);
appUserGrid.addColumn(AppUser::getNachname).setHeader(getTranslation("appuser.column.lastname")).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::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); appUserGrid.addColumn(AppUser::getEmail).setHeader(getTranslation("appuser.column.email")).setAutoWidth(true);
// Make grid rows clickable // Make grid rows clickable

View File

@@ -106,14 +106,11 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic
// Feature Cards // Feature Cards
featuresGrid.add( featuresGrid.add(
createFeatureCard(VaadinIcon.COG, createFeatureCard(VaadinIcon.COG, getTranslation("dashboard.feature.setup.title"),
getTranslation("dashboard.feature.setup.title"),
getTranslation("dashboard.feature.setup.desc")), getTranslation("dashboard.feature.setup.desc")),
createFeatureCard(VaadinIcon.USERS, createFeatureCard(VaadinIcon.USERS, getTranslation("dashboard.feature.customers.title"),
getTranslation("dashboard.feature.customers.title"),
getTranslation("dashboard.feature.customers.desc")), getTranslation("dashboard.feature.customers.desc")),
createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, getTranslation("dashboard.feature.jobs.title"),
getTranslation("dashboard.feature.jobs.title"),
getTranslation("dashboard.feature.jobs.desc"))); getTranslation("dashboard.feature.jobs.desc")));
systemSection.add(systemTitle, systemIntro, featuresGrid); systemSection.add(systemTitle, systemIntro, featuresGrid);

View File

@@ -188,11 +188,14 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
jobInfo.setSpacing(true); jobInfo.setSpacing(true);
jobInfo.setWidthFull(); 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")), jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.customer")),
new Span(extractCompanyName(currentJob.getCustomerSelection())))); new Span(extractCompanyName(currentJob.getCustomerSelection()))));
jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.status")), new Span(currentJob.getStatus().toString()))); jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.status")),
jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.price")), new Span(currentJob.getPrice() + ""))); new Span(currentJob.getStatus().toString())));
jobInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.field.price")),
new Span(currentJob.getPrice() + "")));
section.add(jobInfo); section.add(jobInfo);
return section; return section;
@@ -307,8 +310,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
// Show only net sum, VAT sums, and total amount without individual services // Show only net sum, VAT sums, and total amount without individual services
summaryInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.summary.net")), summaryInfo.add(new HorizontalLayout(new Span(getTranslation("createinvoice.summary.net")),
new Span(netAmount.setScale(2, RoundingMode.HALF_UP) + ""))); new Span(netAmount.setScale(2, RoundingMode.HALF_UP) + "")));
summaryInfo summaryInfo.add(new HorizontalLayout(
.add(new HorizontalLayout(
new Span(getTranslation("createinvoice.summary.vat", new Span(getTranslation("createinvoice.summary.vat",
vatRate.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).toString())), vatRate.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP).toString())),
new Span(vatAmount.setScale(2, RoundingMode.HALF_UP) + ""))); new Span(vatAmount.setScale(2, RoundingMode.HALF_UP) + "")));
@@ -398,7 +400,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
private void createInvoice() { private void createInvoice() {
if (getSelectedServices().isEmpty()) { 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; return;
} }
@@ -411,38 +414,47 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
.flatMap(auth -> userRepository.findByEmail(auth.getUsername())); .flatMap(auth -> userRepository.findByEmail(auth.getUsername()));
if (currentUserOpt.isEmpty()) { if (currentUserOpt.isEmpty()) {
Notification.show(getTranslation("createinvoice.notification.nouser"), 3000, Notification.Position.BOTTOM_END); Notification.show(getTranslation("createinvoice.notification.nouser"), 3000,
Notification.Position.BOTTOM_END);
return; return;
} }
currentUser = currentUserOpt.get(); currentUser = currentUserOpt.get();
// Load invoice template from service // Load invoice template from service
Optional<InvoiceTemplate> templateOpt = invoiceTemplateService.getTemplateByUserId(currentUser.getId().toString()); Optional<InvoiceTemplate> templateOpt = invoiceTemplateService
.getTemplateByUserId(currentUser.getId().toString());
if (templateOpt.isEmpty()) { if (templateOpt.isEmpty()) {
Notification.show(getTranslation("createinvoice.notification.notemplate"), 3000, Notification.Position.BOTTOM_END); Notification.show(getTranslation("createinvoice.notification.notemplate"), 3000,
Notification.Position.BOTTOM_END);
return; return;
} }
String templateData = templateOpt.get().getTemplateData(); 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 length: "
System.out.println("DEBUG CreateInvoiceView: Template data preview: " + (templateData != null ? templateData.substring(0, Math.min(200, templateData.length())) : "null")); + (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()) { 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; return;
} }
// Generate PDF with template and actual job data // Generate PDF with template and actual job data
byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, currentUser); 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 // Show PDF in dialog
showPdfInDialog(pdfBytes, "Rechnung " + currentJob.getJobNumber()); showPdfInDialog(pdfBytes, "Rechnung " + currentJob.getJobNumber());
} catch (Exception ex) { } catch (Exception ex) {
log.error("Fehler beim Erstellen der Rechnung", 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);
} }
} }
@@ -506,10 +518,14 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
// Invoice data // Invoice data
variables.put("invoice.number", currentJob.getJobNumber() + "-" + System.currentTimeMillis()); variables.put("invoice.number", currentJob.getJobNumber() + "-" + System.currentTimeMillis());
variables.put("invoice.date", java.time.LocalDate.now().toString()); 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.net_total",
variables.put("invoice.vat_total", vatAmount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + ""); netAmount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + "");
variables.put("invoice.gross_total", totalAmount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + ""); variables.put("invoice.vat_total",
variables.put("invoice.vat_rate", vatRate.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + "%"); 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 // Job data
if (currentJob.getRouteDistanceKm() != null) { if (currentJob.getRouteDistanceKm() != null) {
@@ -534,7 +550,8 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
// VAT rate // VAT rate
if (service.getVatRate() != null) { 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 { } else {
serviceData.put("vatRate", "19%"); serviceData.put("vatRate", "19%");
} }
@@ -593,11 +610,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
// Download button // Download button
Button downloadButton = new Button(getTranslation("button.download"), e -> { Button downloadButton = new Button(getTranslation("button.download"), e -> {
getElement() getElement().executeJs("const link = document.createElement('a');"
.executeJs("const link = document.createElement('a');" + + "link.href = 'data:application/pdf;base64," + base64Pdf + "';" + "link.download = '"
"link.href = 'data:application/pdf;base64," + base64Pdf + "';" + + title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf';" + "link.click();");
"link.download = '" + title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf';" +
"link.click();");
}); });
downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);

View File

@@ -164,7 +164,8 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
binder.readBean(appUser); binder.readBean(appUser);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Notification.show(getTranslation("editappuser.notification.invalid.id"), 3000, Notification.Position.MIDDLE); Notification.show(getTranslation("editappuser.notification.invalid.id"), 3000,
Notification.Position.MIDDLE);
navigateBack(); navigateBack();
} }
} }
@@ -191,7 +192,8 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
// Passwords match, set new password for hashing // Passwords match, set new password for hashing
appUser.setPassword(newPassword); appUser.setPassword(newPassword);
} else { } else {
Notification.show(getTranslation("editappuser.notification.password.mismatch"), 3000, Notification.Position.MIDDLE); Notification.show(getTranslation("editappuser.notification.password.mismatch"), 3000,
Notification.Position.MIDDLE);
return; return;
} }
} else { } else {
@@ -216,18 +218,21 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
boolean confirmPasswordFilled = confirmPassword != null && !confirmPassword.trim().isEmpty(); boolean confirmPasswordFilled = confirmPassword != null && !confirmPassword.trim().isEmpty();
if (newPasswordFilled && !confirmPasswordFilled) { if (newPasswordFilled && !confirmPasswordFilled) {
Notification.show(getTranslation("editappuser.notification.password.confirm"), 3000, Notification.Position.MIDDLE); Notification.show(getTranslation("editappuser.notification.password.confirm"), 3000,
Notification.Position.MIDDLE);
return false; return false;
} }
if (!newPasswordFilled && confirmPasswordFilled) { if (!newPasswordFilled && confirmPasswordFilled) {
Notification.show(getTranslation("editappuser.notification.password.enter"), 3000, Notification.Position.MIDDLE); Notification.show(getTranslation("editappuser.notification.password.enter"), 3000,
Notification.Position.MIDDLE);
return false; return false;
} }
// If both are filled, they must match // If both are filled, they must match
if (newPasswordFilled && confirmPasswordFilled && newPassword != null && !newPassword.equals(confirmPassword)) { if (newPasswordFilled && confirmPasswordFilled && newPassword != null && !newPassword.equals(confirmPassword)) {
Notification.show(getTranslation("editappuser.notification.password.mismatch"), 3000, Notification.Position.MIDDLE); Notification.show(getTranslation("editappuser.notification.password.mismatch"), 3000,
Notification.Position.MIDDLE);
return false; return false;
} }
@@ -243,7 +248,8 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter<S
Button confirmDeleteButton = new Button(getTranslation("editappuser.dialog.delete.confirm"), e -> { Button confirmDeleteButton = new Button(getTranslation("editappuser.dialog.delete.confirm"), e -> {
if (appUser != null && appUser.getId() != null) { if (appUser != null && appUser.getId() != null) {
appUserService.deleteById(appUser.getId()); appUserService.deleteById(appUser.getId());
Notification.show(getTranslation("editappuser.notification.deleted"), 3000, Notification.Position.MIDDLE); Notification.show(getTranslation("editappuser.notification.deleted"), 3000,
Notification.Position.MIDDLE);
confirmDialog.close(); confirmDialog.close();
navigateBack(); navigateBack();
} }

View File

@@ -151,7 +151,8 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
customer = customerService.findById(customerId); customer = customerService.findById(customerId);
if (customer == null) { if (customer == null) {
Notification.show(getTranslation("editcustomer.notification.notfound"), 3000, Notification.Position.MIDDLE); Notification.show(getTranslation("editcustomer.notification.notfound"), 3000,
Notification.Position.MIDDLE);
navigateBack(); navigateBack();
return; return;
} }
@@ -160,7 +161,8 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
binder.readBean(customer); binder.readBean(customer);
} catch (IllegalArgumentException e) { } 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(); navigateBack();
} }
} }
@@ -184,7 +186,8 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter<
HorizontalLayout buttonLayout = new HorizontalLayout(); HorizontalLayout buttonLayout = new HorizontalLayout();
Button confirmDeleteButton = new Button(getTranslation("editcustomer.dialog.delete.confirm"), e -> { Button confirmDeleteButton = new Button(getTranslation("editcustomer.dialog.delete.confirm"), e -> {
if (customer != null && customer.getId() != null) { if (customer != null && customer.getId() != null) {
Notification.show(getTranslation("editcustomer.notification.deleted"), 3000, Notification.Position.MIDDLE); Notification.show(getTranslation("editcustomer.notification.deleted"), 3000,
Notification.Position.MIDDLE);
confirmDialog.close(); confirmDialog.close();
navigateBack(); navigateBack();
} }

View File

@@ -126,7 +126,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
TextField companyAddField = new TextField(getTranslation("profile.companyadd")); TextField companyAddField = new TextField(getTranslation("profile.companyadd"));
TextField firstnameField = new TextField(getTranslation("profile.firstname")); 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")); TextField lastnameField = new TextField(getTranslation("profile.lastname"));
lastnameField.addBlurListener(e -> validateField(lastnameField, getTranslation("profile.validation.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"))); streetField.addBlurListener(e -> validateField(streetField, getTranslation("profile.validation.street")));
TextField houseNumberField = new TextField(getTranslation("profile.housenr")); 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")); TextField addressAddField = new TextField(getTranslation("profile.addressadd"));
@@ -227,22 +229,29 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
cityField.setRequiredIndicatorVisible(true); cityField.setRequiredIndicatorVisible(true);
// Hauptadresse binden // 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(companyAddField).bind(User::getCompanyAddition, User::setCompanyAddition);
binder.forField(streetField).asRequired(getTranslation("profile.validation.street.required")).bind(User::getStreet, User::setStreet); binder.forField(streetField).asRequired(getTranslation("profile.validation.street.required"))
binder.forField(houseNumberField).asRequired(getTranslation("profile.validation.housenr.required")).bind(User::getHouseNumber, .bind(User::getStreet, User::setStreet);
User::setHouseNumber); binder.forField(houseNumberField).asRequired(getTranslation("profile.validation.housenr.required"))
.bind(User::getHouseNumber, User::setHouseNumber);
binder.forField(addressAddField).bind(User::getAddressAddition, User::setAddressAddition); binder.forField(addressAddField).bind(User::getAddressAddition, User::setAddressAddition);
binder.forField(zipField).asRequired(getTranslation("profile.validation.zip.required")).bind(User::getZip, User::setZip); binder.forField(zipField).asRequired(getTranslation("profile.validation.zip.required")).bind(User::getZip,
binder.forField(cityField).asRequired(getTranslation("profile.validation.city.required")).bind(User::getCity, User::setCity); User::setZip);
binder.forField(cityField).asRequired(getTranslation("profile.validation.city.required")).bind(User::getCity,
User::setCity);
// Personendaten binden // Personendaten binden
binder.forField(firstnameField).asRequired(getTranslation("profile.validation.firstname.required")).bind(User::getFirstname, binder.forField(firstnameField).asRequired(getTranslation("profile.validation.firstname.required"))
User::setFirstname); .bind(User::getFirstname, User::setFirstname);
binder.forField(lastnameField).asRequired(getTranslation("profile.validation.lastname.required")).bind(User::getName, User::setName); binder.forField(lastnameField).asRequired(getTranslation("profile.validation.lastname.required"))
binder.forField(phoneField).asRequired(getTranslation("profile.validation.phone.required")).bind(User::getPhone, User::setPhone); .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")) 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 // Optionale Felder
binder.forField(mobileField).bind(User::getPhone2, User::setPhone2); binder.forField(mobileField).bind(User::getPhone2, User::setPhone2);
binder.forField(faxField).bind(User::getFax, User::setFax); binder.forField(faxField).bind(User::getFax, User::setFax);
@@ -590,7 +599,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
UI.getCurrent().getPage().reload(); UI.getCurrent().getPage().reload();
} }
} catch (Exception ex) { } 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) { } catch (Exception e) {
// Log error or show notification // 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) { } 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 email = safe(currentUser.getEmail());
String phone = safe(currentUser.getPhone()); 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); 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()); 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()); 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()); city.trim().isEmpty() ? getTranslation("profile.invoice.placeholder.city") : city.trim());
Div senderEmail = createVariableTemplate(getTranslation("profile.invoice.email"), VaadinIcon.ENVELOPE, "masterdata.email", Div senderEmail = createVariableTemplate(getTranslation("profile.invoice.email"), VaadinIcon.ENVELOPE,
email.isEmpty() ? getTranslation("profile.invoice.placeholder.email") : email); "masterdata.email", email.isEmpty() ? getTranslation("profile.invoice.placeholder.email") : email);
Div senderPhone = createVariableTemplate(getTranslation("profile.invoice.phone"), VaadinIcon.PHONE, "masterdata.phone", Div senderPhone = createVariableTemplate(getTranslation("profile.invoice.phone"), VaadinIcon.PHONE,
phone.isEmpty() ? getTranslation("profile.invoice.placeholder.phone") : phone); "masterdata.phone", phone.isEmpty() ? getTranslation("profile.invoice.placeholder.phone") : phone);
// Bereich 2: Leistungen // Bereich 2: Leistungen
Span servicesHeader = new Span(getTranslation("profile.services.label")); 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)"); .set("margin-top", "var(--lumo-space-m)");
// Leistungen als draggable Variable // Leistungen als draggable Variable
Div servicesListBlock = createServicesVariableTemplate(getTranslation("profile.invoice.services.list"), VaadinIcon.LIST, "services.list", Div servicesListBlock = createServicesVariableTemplate(getTranslation("profile.invoice.services.list"),
"Artikel 1: 100,00 €\nArtikel 2: 50,00 €"); 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", Div servicesNetBlock = createServicesVariableTemplate(getTranslation("profile.invoice.net"),
"150,00 €"); VaadinIcon.COIN_PILES, "services.net_total", "150,00 €");
Div servicesVatBlock = createServicesVariableTemplate(getTranslation("profile.invoice.vat"), VaadinIcon.COIN_PILES, Div servicesVatBlock = createServicesVariableTemplate(getTranslation("profile.invoice.vat"),
"services.vat_total", "28,50 €"); VaadinIcon.COIN_PILES, "services.vat_total", "28,50 €");
Div servicesGrossBlock = createServicesVariableTemplate(getTranslation("profile.invoice.gross"), VaadinIcon.MONEY, "services.gross_total", Div servicesGrossBlock = createServicesVariableTemplate(getTranslation("profile.invoice.gross"),
"178,50 €"); VaadinIcon.MONEY, "services.gross_total", "178,50 €");
// Bereich 3: Kundendaten // Bereich 3: Kundendaten
Span customerHeader = new Span(getTranslation("profile.invoice.customerdata")); 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)"); .set("margin-top", "var(--lumo-space-m)");
// Kundendaten als Variablen (grün hinterlegt) // Kundendaten als Variablen (grün hinterlegt)
Div customerCompany = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.company"), VaadinIcon.OFFICE, "customer.company_name", Div customerCompany = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.company"),
"Kundenfirma GmbH"); VaadinIcon.OFFICE, "customer.company_name", "Kundenfirma GmbH");
Div customerName = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.name"), VaadinIcon.USER, "customer.contact_name", Div customerName = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.name"),
"Erika Mustermann"); VaadinIcon.USER, "customer.contact_name", "Erika Mustermann");
Div customerAddress = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.street"), VaadinIcon.MAP_MARKER, "customer.street", Div customerAddress = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.street"),
"Kundenstraße 456"); VaadinIcon.MAP_MARKER, "customer.street", "Kundenstraße 456");
Div customerCity = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.city"), VaadinIcon.BUILDING, "customer.city", Div customerCity = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.city"),
"54321 Kundenstadt"); VaadinIcon.BUILDING, "customer.city", "54321 Kundenstadt");
Div customerEmail = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.email"), VaadinIcon.ENVELOPE, "customer.email", Div customerEmail = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.email"),
"kunde@beispiel.de"); VaadinIcon.ENVELOPE, "customer.email", "kunde@beispiel.de");
Div customerPhone = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.phone"), VaadinIcon.PHONE, "customer.phone", Div customerPhone = createCustomerVariableTemplate(getTranslation("profile.invoice.customer.phone"),
"0987 654321"); VaadinIcon.PHONE, "customer.phone", "0987 654321");
// Bereich 2: Freie Elemente // Bereich 2: Freie Elemente
Span freeHeader = new Span(getTranslation("profile.invoice.free.elements")); Span freeHeader = new Span(getTranslation("profile.invoice.free.elements"));
@@ -983,14 +999,22 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
"var(--lumo-space-m)"); "var(--lumo-space-m)");
// Draggable Templates // Draggable Templates
Div textBlock = createDraggableTemplate(getTranslation("profile.invoice.element.text"), VaadinIcon.TEXT_LABEL, "text"); Div textBlock = createDraggableTemplate(getTranslation("profile.invoice.element.text"), VaadinIcon.TEXT_LABEL,
Div headerBlock = createDraggableTemplate(getTranslation("profile.invoice.element.header"), VaadinIcon.HEADER, "header"); "text");
Div dateBlock = createDraggableTemplate(getTranslation("profile.invoice.element.date"), VaadinIcon.CALENDAR, "date"); Div headerBlock = createDraggableTemplate(getTranslation("profile.invoice.element.header"), VaadinIcon.HEADER,
Div customerBlock = createDraggableTemplate(getTranslation("profile.invoice.element.customer"), VaadinIcon.USER, "customer"); "header");
Div companyBlock = createDraggableTemplate(getTranslation("profile.invoice.element.company"), VaadinIcon.WORKPLACE, "company"); Div dateBlock = createDraggableTemplate(getTranslation("profile.invoice.element.date"), VaadinIcon.CALENDAR,
Div amountBlock = createDraggableTemplate(getTranslation("profile.invoice.element.amount"), VaadinIcon.COIN_PILES, "amount"); "date");
Div lineBlock = createDraggableTemplate(getTranslation("profile.invoice.element.line"), VaadinIcon.LINE_V, "line"); Div customerBlock = createDraggableTemplate(getTranslation("profile.invoice.element.customer"), VaadinIcon.USER,
Div imageBlock = createDraggableTemplate(getTranslation("profile.invoice.element.image"), VaadinIcon.PICTURE, "image"); "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, panel.add(invoiceHeader, senderCompany, senderName, senderAddress, senderCity, senderEmail, senderPhone,
servicesHeader, servicesListBlock, servicesNetBlock, servicesVatBlock, servicesGrossBlock, servicesHeader, servicesListBlock, servicesNetBlock, servicesVatBlock, servicesGrossBlock,
@@ -1208,7 +1232,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
getElement() getElement()
.executeJs("if (window.updateProfileElementImage) { window.updateProfileElementImage('" .executeJs("if (window.updateProfileElementImage) { window.updateProfileElementImage('"
+ elementId + "', $0); }", dataUrl); + 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) { } catch (Exception ex) {
Notification.show(getTranslation("profile.invoice.image.upload.error", ex.getMessage()), 3000, Notification.show(getTranslation("profile.invoice.image.upload.error", ex.getMessage()), 3000,
Notification.Position.BOTTOM_CENTER); Notification.Position.BOTTOM_CENTER);
@@ -1333,7 +1358,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
} }
// Löschen Button // 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.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
deleteButton.setWidthFull(); deleteButton.setWidthFull();
deleteButton.addClickListener(e -> { deleteButton.addClickListener(e -> {
@@ -1498,8 +1524,10 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
return ""; return "";
}).setHeader(getTranslation("profile.services.vatrate")).setSortable(true); }).setHeader(getTranslation("profile.services.vatrate")).setSortable(true);
servicesGrid.addColumn(service -> service.isMandatory() ? getTranslation("common.yes") : getTranslation("common.no")).setHeader(getTranslation("profile.services.mandatory")) servicesGrid
.setSortable(true); .addColumn(
service -> service.isMandatory() ? getTranslation("common.yes") : getTranslation("common.no"))
.setHeader(getTranslation("profile.services.mandatory")).setSortable(true);
// Actions column with edit and delete buttons // Actions column with edit and delete buttons
servicesGrid.addComponentColumn(service -> { servicesGrid.addComponentColumn(service -> {
@@ -1554,7 +1582,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
*/ */
private void openServiceDialog(Service service) { private void openServiceDialog(Service service) {
Dialog dialog = new Dialog(); 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"); dialog.setWidth("500px");
// Form layout // Form layout
@@ -1570,7 +1599,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
nameField.setRequiredIndicatorVisible(true); nameField.setRequiredIndicatorVisible(true);
// Calculation basis combo box // Calculation basis combo box
ComboBox<Service.CalculationBasis> calculationBasisCombo = new ComboBox<>(getTranslation("profile.services.basis")); ComboBox<Service.CalculationBasis> calculationBasisCombo = new ComboBox<>(
getTranslation("profile.services.basis"));
calculationBasisCombo.setWidthFull(); calculationBasisCombo.setWidthFull();
calculationBasisCombo.setItems(Service.CalculationBasis.values()); calculationBasisCombo.setItems(Service.CalculationBasis.values());
calculationBasisCombo.setItemLabelGenerator(basis -> { calculationBasisCombo.setItemLabelGenerator(basis -> {
@@ -1855,10 +1885,11 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
} }
/** /**
* Sets the language cookie via JavaScript so the selected language * Sets the language cookie via JavaScript so the selected language is available
* is available even before the user logs in (e.g., on the login page). * 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) { private void setLanguageCookie(Language language) {
String languageCode = switch (language) { String languageCode = switch (language) {
@@ -1875,11 +1906,9 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
}; };
// Execute JavaScript to set the cookie // Execute JavaScript to set the cookie
UI.getCurrent().getPage().executeJs( UI.getCurrent().getPage().executeJs("const maxAge = 365 * 24 * 60 * 60;"
"const maxAge = 365 * 24 * 60 * 60;" + + "document.cookie = 'votianlt.language=' + $0 + ';path=/;max-age=' + maxAge + ';SameSite=Lax';",
"document.cookie = 'votianlt.language=' + $0 + ';path=/;max-age=' + maxAge + ';SameSite=Lax';", languageCode);
languageCode
);
} }
@Override @Override

View File

@@ -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)"); header.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-l)");
// Draggable Templates // Draggable Templates
Div textBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.text"), VaadinIcon.TEXT_LABEL, "text"); Div textBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.text"), VaadinIcon.TEXT_LABEL,
Div headerBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.header"), VaadinIcon.HEADER, "header"); "text");
Div dateBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.date"), VaadinIcon.CALENDAR, "date"); Div headerBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.header"), VaadinIcon.HEADER,
Div customerBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.customerinfo"), VaadinIcon.USER, "customer"); "header");
Div companyBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.companyinfo"), VaadinIcon.OFFICE, "company"); Div dateBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.date"), VaadinIcon.CALENDAR,
Div amountBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.amount"), VaadinIcon.COIN_PILES, "amount"); "date");
Div lineBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.line"), VaadinIcon.LINE_V, "line"); Div customerBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.customerinfo"),
Div imageBlock = createDraggableTemplate(getTranslation("invoicegenerator.template.image"), VaadinIcon.PICTURE, "image"); 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, panel.add(header, textBlock, headerBlock, dateBlock, customerBlock, companyBlock, amountBlock, lineBlock,
imageBlock); imageBlock);
@@ -238,7 +246,8 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.exportTemplate(); }"); 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.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
generatePdfButton.addClickListener(e -> generatePdf()); generatePdfButton.addClickListener(e -> generatePdf());
@@ -289,7 +298,8 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData); byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData);
showPdfInDialog(pdfBytes); showPdfInDialog(pdfBytes);
} catch (Exception ex) { } catch (Exception ex) {
showNotification(getTranslation("invoicegenerator.notification.preview.error", ex.getMessage())); showNotification(
getTranslation("invoicegenerator.notification.preview.error", ex.getMessage()));
} }
}); });
} catch (Exception ex) { } catch (Exception ex) {
@@ -557,7 +567,8 @@ public class InvoiceGeneratorView extends VerticalLayout implements HasDynamicTi
} }
// Löschen Button // 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.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
deleteButton.setWidthFull(); deleteButton.setWidthFull();
deleteButton.addClickListener(e -> { deleteButton.addClickListener(e -> {

View File

@@ -46,11 +46,16 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
add(title); add(title);
invoiceGrid = new Grid<>(SystemInvoice.class, false); invoiceGrid = new Grid<>(SystemInvoice.class, false);
invoiceGrid.addColumn(SystemInvoice::getId).setHeader(getTranslation("invoices.column.number")).setAutoWidth(true); invoiceGrid.addColumn(SystemInvoice::getId).setHeader(getTranslation("invoices.column.number"))
invoiceGrid.addColumn(SystemInvoice::getKunde).setHeader(getTranslation("invoices.column.customer")).setAutoWidth(true); .setAutoWidth(true);
invoiceGrid.addColumn(SystemInvoice::getDatum).setHeader(getTranslation("invoices.column.date")).setAutoWidth(true); invoiceGrid.addColumn(SystemInvoice::getKunde).setHeader(getTranslation("invoices.column.customer"))
invoiceGrid.addColumn(SystemInvoice::getBetrag).setHeader(getTranslation("invoices.column.amount")).setAutoWidth(true); .setAutoWidth(true);
invoiceGrid.addColumn(SystemInvoice::getBeschreibung).setHeader(getTranslation("invoices.column.description")).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.setSelectionMode(Grid.SelectionMode.SINGLE);
invoiceGrid.getStyle().set("cursor", "pointer"); invoiceGrid.getStyle().set("cursor", "pointer");
@@ -88,7 +93,8 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle {
UI.getCurrent().getPage().open(registration.getResourceUri().toString()); UI.getCurrent().getPage().open(registration.getResourceUri().toString());
} catch (Exception e) { } 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);
} }
} }

View File

@@ -183,7 +183,8 @@ public class JobHistoryView extends Main implements HasUrlParameter<String>, Has
Icon typeIcon = getTypeIcon(entry.getChangeType()); Icon typeIcon = getTypeIcon(entry.getChangeType());
typeIcon.getStyle().set("color", getTypeColor(entry.getChangeType())); typeIcon.getStyle().set("color", getTypeColor(entry.getChangeType()));
Span reason = new Span(entry.getReason() != null ? entry.getReason() : getTranslation("jobhistory.entry.unknown")); Span reason = new Span(
entry.getReason() != null ? entry.getReason() : getTranslation("jobhistory.entry.unknown"));
reason.getStyle().set("font-weight", "500"); reason.getStyle().set("font-weight", "500");
Span timestamp = new Span(formatDateTime(entry.getTimestamp())); Span timestamp = new Span(formatDateTime(entry.getTimestamp()));

View File

@@ -149,7 +149,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
sendMessageButton.addClickListener(e -> { sendMessageButton.addClickListener(e -> {
// Check if job has an app user assigned // Check if job has an app user assigned
if (job.getAppUser() == null || job.getAppUser().isBlank()) { if (job.getAppUser() == null || job.getAppUser().isBlank()) {
Notification.show(getTranslation("jobsummary.notification.noappuser"), 3000, Notification.Position.MIDDLE) Notification
.show(getTranslation("jobsummary.notification.noappuser"), 3000, Notification.Position.MIDDLE)
.addThemeVariants(NotificationVariant.LUMO_ERROR); .addThemeVariants(NotificationVariant.LUMO_ERROR);
return; return;
} }
@@ -189,7 +190,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
topRow.setSpacing(true); topRow.setSpacing(true);
VerticalLayout pickupBox = borderedBox(); 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.getPickupCompany())));
pickupBox.add(new Span(valueOrEmpty(job.getPickupSalutation()) + (job.getPickupSalutation() != null ? " " : "") pickupBox.add(new Span(valueOrEmpty(job.getPickupSalutation()) + (job.getPickupSalutation() != null ? " " : "")
+ valueOrEmpty(job.getPickupFirstName()) + (job.getPickupFirstName() != null ? " " : "") + valueOrEmpty(job.getPickupFirstName()) + (job.getPickupFirstName() != null ? " " : "")
@@ -198,7 +200,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
pickupBox.add(new Span(concatZipCity(job.getPickupZip(), job.getPickupCity()))); pickupBox.add(new Span(concatZipCity(job.getPickupZip(), job.getPickupCity())));
VerticalLayout deliveryBox = borderedBox(); VerticalLayout deliveryBox = borderedBox();
deliveryBox.add(new H3(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.getDeliveryCompany())));
deliveryBox.add(new Span(valueOrEmpty(job.getDeliverySalutation()) deliveryBox.add(new Span(valueOrEmpty(job.getDeliverySalutation())
+ (job.getDeliverySalutation() != null ? " " : "") + valueOrEmpty(job.getDeliveryFirstName()) + (job.getDeliverySalutation() != null ? " " : "") + valueOrEmpty(job.getDeliveryFirstName())
@@ -279,7 +282,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
infoBox.add(new Span(getTranslation("jobsummary.info.digital"))); infoBox.add(new Span(getTranslation("jobsummary.info.digital")));
} }
if (job.getAppUser() != null && !job.getAppUser().isBlank()) { if (job.getAppUser() != null && !job.getAppUser().isBlank()) {
infoBox.add(new Span(getTranslation("jobsummary.info.appuser") + ": " + resolveAppUserName(job.getAppUser()))); infoBox.add(
new Span(getTranslation("jobsummary.info.appuser") + ": " + resolveAppUserName(job.getAppUser())));
} }
cargoBox.setWidth("50%"); cargoBox.setWidth("50%");
@@ -298,7 +302,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
buttonRow.setJustifyContentMode(HorizontalLayout.JustifyContentMode.CENTER); buttonRow.setJustifyContentMode(HorizontalLayout.JustifyContentMode.CENTER);
buttonRow.getStyle().set("margin-top", "var(--lumo-space-l)"); buttonRow.getStyle().set("margin-top", "var(--lumo-space-l)");
Button completeButton = new Button(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.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
completeButton.addClickListener(e -> { completeButton.addClickListener(e -> {
ConfirmDialog dialog = new ConfirmDialog(); ConfirmDialog dialog = new ConfirmDialog();
@@ -475,14 +480,15 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
Integer savedDuration = job.getRouteDurationSeconds(); Integer savedDuration = job.getRouteDurationSeconds();
boolean hasSavedRouteData = savedDistance != null && savedDuration != null; boolean hasSavedRouteData = savedDistance != null && savedDuration != null;
String js = buildMapJs(origin, destination, hasPosition, position, appUserId, shouldUpdate, String js = buildMapJs(origin, destination, hasPosition, position, appUserId, shouldUpdate, hasSavedRouteData,
hasSavedRouteData, savedDistance != null ? savedDistance : 0.0, savedDuration != null ? savedDuration : 0); savedDistance != null ? savedDistance : 0.0, savedDuration != null ? savedDuration : 0);
map.getElement().executeJs(js, map.getElement(), routeInfo.getElement()); map.getElement().executeJs(js, map.getElement(), routeInfo.getElement());
} }
private String buildMapJs(String origin, String destination, boolean hasPosition, LocationPosition position, 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(); String apiKey = getGoogleMapsApiKey();
// Explizit mit Punkt als Dezimaltrennzeichen formatieren // Explizit mit Punkt als Dezimaltrennzeichen formatieren
String lat = hasPosition ? String.format(java.util.Locale.US, "%.6f", position.getLatitude()) : "0"; String lat = hasPosition ? String.format(java.util.Locale.US, "%.6f", position.getLatitude()) : "0";
@@ -493,8 +499,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
// Gespeicherte Dauer formatieren // Gespeicherte Dauer formatieren
int hours = savedDuration / 3600; int hours = savedDuration / 3600;
int minutes = (savedDuration % 3600) / 60; int minutes = (savedDuration % 3600) / 60;
String savedDurationText = hours > 0 String savedDurationText = hours > 0 ? String.format("%d Std. %d Min.", hours, minutes)
? String.format("%d Std. %d Min.", hours, minutes)
: String.format("%d Min.", minutes); : String.format("%d Min.", minutes);
return """ return """
@@ -1213,7 +1218,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
for (String serviceId : serviceIds) { for (String serviceId : serviceIds) {
Service service = serviceRepository.findById(serviceId).orElse(null); Service service = serviceRepository.findById(serviceId).orElse(null);
if (service == null) continue; if (service == null)
continue;
BigDecimal price = calculateServicePrice(service, routeDistance, durationSeconds); BigDecimal price = calculateServicePrice(service, routeDistance, durationSeconds);
BigDecimal vatRate = service.getVatRate() != null ? service.getVatRate() : new BigDecimal("0.19"); BigDecimal vatRate = service.getVatRate() != null ? service.getVatRate() : new BigDecimal("0.19");
@@ -1227,7 +1233,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
} }
/** /**
* 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) { private BigDecimal calculateServicePrice(Service service, Double routeDistance, Integer durationSeconds) {
if (service.getCalculationBasis() == null) { if (service.getCalculationBasis() == null) {
@@ -1248,7 +1255,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
if (service.getPricePer15Minutes() != null && durationSeconds != null && durationSeconds > 0) { if (service.getPricePer15Minutes() != null && durationSeconds != null && durationSeconds > 0) {
// Dauer in 15-Minuten-Einheiten umrechnen // Dauer in 15-Minuten-Einheiten umrechnen
int units = durationSeconds / 900; // 900 Sekunden = 15 Minuten 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 service.getPricePer15Minutes().multiply(BigDecimal.valueOf(units));
} }
return BigDecimal.ZERO; return BigDecimal.ZERO;

View File

@@ -184,8 +184,7 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af
private void handleVerify2fa() { private void handleVerify2fa() {
if (pendingAuth == null) { if (pendingAuth == null) {
Notification.show(getTranslation("login.2fa.no.credentials"), 3000, Notification.show(getTranslation("login.2fa.no.credentials"), 3000, Notification.Position.BOTTOM_CENTER);
Notification.Position.BOTTOM_CENTER);
return; return;
} }
String username = pendingAuth.getName(); String username = pendingAuth.getName();

View File

@@ -106,8 +106,10 @@ public class MessagesView extends Main implements HasDynamicTitle {
return span; return span;
})).setHeader(getTranslation("messages.column.status")).setWidth("80px").setFlexGrow(0); })).setHeader(getTranslation("messages.column.status")).setWidth("80px").setFlexGrow(0);
grid.addColumn(ClientMessageSummary::getClientName).setHeader(getTranslation("messages.column.client")).setAutoWidth(true); grid.addColumn(ClientMessageSummary::getClientName).setHeader(getTranslation("messages.column.client"))
grid.addColumn(ClientMessageSummary::getClientEmail).setHeader(getTranslation("messages.column.email")).setAutoWidth(true); .setAutoWidth(true);
grid.addColumn(ClientMessageSummary::getClientEmail).setHeader(getTranslation("messages.column.email"))
.setAutoWidth(true);
grid.addColumn(new ComponentRenderer<>(summary -> { grid.addColumn(new ComponentRenderer<>(summary -> {
Span span = new Span(String.valueOf(summary.getTotalMessages())); Span span = new Span(String.valueOf(summary.getTotalMessages()));

View File

@@ -137,13 +137,14 @@ public class MyInvoicesView extends Main implements HasDynamicTitle {
grid.addThemeVariants(GridVariant.LUMO_ROW_STRIPES, GridVariant.LUMO_COMPACT, grid.addThemeVariants(GridVariant.LUMO_ROW_STRIPES, GridVariant.LUMO_COMPACT,
GridVariant.LUMO_WRAP_CELL_CONTENT, GridVariant.LUMO_COLUMN_BORDERS); GridVariant.LUMO_WRAP_CELL_CONTENT, GridVariant.LUMO_COLUMN_BORDERS);
grid.setWidthFull(); grid.setWidthFull();
grid.addColumn(new ComponentRenderer<>(row -> statusBadge(row.status()))).setHeader(getTranslation("myinvoices.column.status")).setAutoWidth(true) grid.addColumn(new ComponentRenderer<>(row -> statusBadge(row.status())))
.setFlexGrow(0); .setHeader(getTranslation("myinvoices.column.status")).setAutoWidth(true).setFlexGrow(0);
grid.addColumn(MyInvoicesView::formatInvoiceNumber).setHeader(getTranslation("myinvoices.column.number")).setAutoWidth(true); grid.addColumn(MyInvoicesView::formatInvoiceNumber).setHeader(getTranslation("myinvoices.column.number"))
grid.addColumn(row -> DateTimeFormatUtil.formatDate(row.date())).setHeader(getTranslation("myinvoices.column.date")).setAutoWidth(true) .setAutoWidth(true);
.setFlexGrow(0); grid.addColumn(row -> DateTimeFormatUtil.formatDate(row.date()))
grid.addColumn(row -> CURRENCY_FMT.format(row.amount())).setHeader(getTranslation("myinvoices.column.amount")).setAutoWidth(true) .setHeader(getTranslation("myinvoices.column.date")).setAutoWidth(true).setFlexGrow(0);
.setTextAlign(ColumnTextAlign.END).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.setAllRowsVisible(true);
grid.setItems(allRows); // zunächst leer grid.setItems(allRows); // zunächst leer
grid.addItemClickListener(event -> { grid.addItemClickListener(event -> {

View File

@@ -164,7 +164,8 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle {
resendButton.setVisible(false); resendButton.setVisible(false);
// Zurück-Link // 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.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
backButton.setWidthFull(); backButton.setWidthFull();
@@ -223,12 +224,14 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle {
// Validierung // Validierung
if (email.isEmpty()) { 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(); emailField.focus();
return; return;
} }
if (!email.contains("@") || !email.contains(".")) { 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(); emailField.focus();
return; return;
} }
@@ -238,7 +241,8 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle {
return; return;
} }
if (password.isEmpty()) { 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(); passwordField.focus();
return; return;
} }
@@ -248,7 +252,8 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle {
return; return;
} }
if (!password.equals(confirmPassword)) { 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(); confirmPasswordField.focus();
return; return;
} }
@@ -256,37 +261,43 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle {
// Weitere Pflichtfelder prüfen (aus Edit-Profile) // Weitere Pflichtfelder prüfen (aus Edit-Profile)
var firstName = firstNameField.getValue() != null ? firstNameField.getValue().trim() : ""; var firstName = firstNameField.getValue() != null ? firstNameField.getValue().trim() : "";
if (firstName.isEmpty()) { 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(); firstNameField.focus();
return; return;
} }
var lastName = lastNameField.getValue() != null ? lastNameField.getValue().trim() : ""; var lastName = lastNameField.getValue() != null ? lastNameField.getValue().trim() : "";
if (lastName.isEmpty()) { 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(); lastNameField.focus();
return; return;
} }
var phone = phoneField.getValue() != null ? phoneField.getValue().trim() : ""; var phone = phoneField.getValue() != null ? phoneField.getValue().trim() : "";
if (phone.isEmpty()) { 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(); phoneField.focus();
return; return;
} }
var company = companyField.getValue() != null ? companyField.getValue().trim() : ""; var company = companyField.getValue() != null ? companyField.getValue().trim() : "";
if (company.isEmpty()) { 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(); companyField.focus();
return; return;
} }
var street = streetField.getValue() != null ? streetField.getValue().trim() : ""; var street = streetField.getValue() != null ? streetField.getValue().trim() : "";
if (street.isEmpty()) { 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(); streetField.focus();
return; return;
} }
var houseNo = houseNumberField.getValue() != null ? houseNumberField.getValue().trim() : ""; var houseNo = houseNumberField.getValue() != null ? houseNumberField.getValue().trim() : "";
if (houseNo.isEmpty()) { 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(); houseNumberField.focus();
return; return;
} }
@@ -298,7 +309,8 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle {
} }
var city = cityField.getValue() != null ? cityField.getValue().trim() : ""; var city = cityField.getValue() != null ? cityField.getValue().trim() : "";
if (city.isEmpty()) { 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(); cityField.focus();
return; return;
} }
@@ -351,23 +363,25 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle {
Notification.Position.MIDDLE); Notification.Position.MIDDLE);
} catch (Exception e) { } catch (Exception e) {
awaitingVerification = false; 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() { private void onVerifyCode() {
if (!awaitingVerification) { 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; return;
} }
String entered = codeField.getValue() != null ? codeField.getValue().trim() : ""; String entered = codeField.getValue() != null ? codeField.getValue().trim() : "";
if (!entered.matches("\\d{6}")) { 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; return;
} }
if (codeExpiresAt == null || LocalDateTime.now().isAfter(codeExpiresAt)) { if (codeExpiresAt == null || LocalDateTime.now().isAfter(codeExpiresAt)) {
Notification.show(getTranslation("register.notification.code.expired"), 4000, Notification.show(getTranslation("register.notification.code.expired"), 4000, Notification.Position.MIDDLE);
Notification.Position.MIDDLE);
return; return;
} }
if (!entered.equals(pendingCode)) { if (!entered.equals(pendingCode)) {
@@ -397,11 +411,11 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle {
user.setZip(zip); user.setZip(zip);
user.setCity(city); user.setCity(city);
userService.save(user); userService.save(user);
VaadinSession.getCurrent().setAttribute("flashMessage", VaadinSession.getCurrent().setAttribute("flashMessage", getTranslation("register.notification.success"));
getTranslation("register.notification.success"));
getUI().ifPresent(ui -> ui.navigate("login")); getUI().ifPresent(ui -> ui.navigate("login"));
} catch (RuntimeException e) { } 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);
} }
} }

View File

@@ -42,25 +42,28 @@ public class ShowCustomersView extends VerticalLayout implements HasDynamicTitle
add(header); add(header);
// Add hint text // Add hint text
var hintText = new com.vaadin.flow.component.html.Paragraph( var hintText = new com.vaadin.flow.component.html.Paragraph(getTranslation("customers.hint.click"));
getTranslation("customers.hint.click"));
hintText.getStyle().set("color", "var(--lumo-secondary-text-color)"); hintText.getStyle().set("color", "var(--lumo-secondary-text-color)");
hintText.getStyle().set("font-size", "var(--lumo-font-size-s)"); hintText.getStyle().set("font-size", "var(--lumo-font-size-s)");
add(hintText); add(hintText);
// Configure grid columns // Configure grid columns
grid.addColumn(Customer::getCompanyName).setHeader(getTranslation("customers.column.company")).setAutoWidth(true).setFlexGrow(1).setSortable(true); grid.addColumn(Customer::getCompanyName).setHeader(getTranslation("customers.column.company"))
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); .setAutoWidth(true).setFlexGrow(1).setSortable(true);
grid.addColumn(customer -> (customer.getZip() != null ? customer.getZip() : "") + " " grid.addColumn(customer -> (customer.getFirstname() != null ? customer.getFirstname() : "") + " "
+ (customer.getCity() != null ? customer.getCity() : "")).setHeader(getTranslation("customers.column.city")).setAutoWidth(true) + (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); .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.setMultiSort(true);
grid.setSizeFull(); grid.setSizeFull();

View File

@@ -71,7 +71,8 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
add(title); add(title);
// Configure status filter // 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.setValue(getTranslation("jobs.status.open"));
statusFilter.setWidth("150px"); statusFilter.setWidth("150px");
@@ -108,12 +109,14 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
endDate.addValueChangeListener(e -> loadData()); endDate.addValueChangeListener(e -> loadData());
// Configure grid columns: Auftraggeber, Auftragsnummer, Auftragsdatum, Zielort // Configure grid columns: Auftraggeber, Auftragsnummer, Auftragsdatum, Zielort
grid.addColumn(job -> extractCompanyName(job.getCustomerSelection())).setHeader(getTranslation("jobs.column.customer")) grid.addColumn(job -> extractCompanyName(job.getCustomerSelection()))
.setAutoWidth(true).setFlexGrow(1).setSortable(true); .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::getJobNumber).setHeader(getTranslation("jobs.column.jobnumber")).setAutoWidth(true)
grid.addColumn(job -> DateTimeFormatUtil.formatDateTime(job.getCreatedAt())).setHeader(getTranslation("jobs.column.jobdate")) .setSortable(true);
.setAutoWidth(true).setSortable(true); grid.addColumn(job -> DateTimeFormatUtil.formatDateTime(job.getCreatedAt()))
grid.addColumn(Job::getDeliveryCity).setHeader(getTranslation("jobs.column.destination")).setAutoWidth(true).setFlexGrow(1).setSortable(true); .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 // Action column: manual completion for jobs without digital processing
grid.addComponentColumn(job -> { grid.addComponentColumn(job -> {
@@ -201,8 +204,8 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_SUCCESS); Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_SUCCESS);
loadData(); loadData();
} catch (Exception ex) { } catch (Exception ex) {
Notification.show(getTranslation("jobs.notification.complete.error", ex.getMessage()), 5000, Notification.Position.BOTTOM_END) Notification.show(getTranslation("jobs.notification.complete.error", ex.getMessage()), 5000,
.addThemeVariants(NotificationVariant.LUMO_ERROR); Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_ERROR);
} }
}); });
dialog.open(); dialog.open();
@@ -226,8 +229,8 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_SUCCESS); Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_SUCCESS);
loadData(); loadData();
} catch (Exception ex) { } catch (Exception ex) {
Notification.show(getTranslation("jobs.notification.delete.error", ex.getMessage()), 5000, Notification.Position.BOTTOM_END) Notification.show(getTranslation("jobs.notification.delete.error", ex.getMessage()), 5000,
.addThemeVariants(NotificationVariant.LUMO_ERROR); Notification.Position.BOTTOM_END).addThemeVariants(NotificationVariant.LUMO_ERROR);
} }
}); });
dialog.open(); dialog.open();
@@ -314,9 +317,8 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
private String generateCsv(java.util.List<Job> jobs) { private String generateCsv(java.util.List<Job> jobs) {
StringBuilder csv = new StringBuilder(); StringBuilder csv = new StringBuilder();
// CSV Header // CSV Header
csv.append(getTranslation("csv.header.customer")).append(",") csv.append(getTranslation("csv.header.customer")).append(",").append(getTranslation("csv.header.jobnumber"))
.append(getTranslation("csv.header.jobnumber")).append(",") .append(",").append(getTranslation("csv.header.jobdate")).append(",")
.append(getTranslation("csv.header.jobdate")).append(",")
.append(getTranslation("csv.header.destination")).append("\n"); .append(getTranslation("csv.header.destination")).append("\n");
// CSV Data // CSV Data

View File

@@ -107,7 +107,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
} }
private Button createLanguageSelector() { 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 currentLang = getCurrentLanguageFromLocale();
String flag = getFlagForLanguage(currentLang); String flag = getFlagForLanguage(currentLang);
@@ -134,7 +135,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
} }
private String getCurrentLanguageFromLocale() { 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(); Locale locale = UI.getCurrent().getLocale();
if (locale == null) { if (locale == null) {
return "DE"; return "DE";
@@ -175,11 +177,9 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
} }
private void setLanguageCookie(String languageCode) { private void setLanguageCookie(String languageCode) {
UI.getCurrent().getPage().executeJs( UI.getCurrent().getPage().executeJs("const maxAge = 365 * 24 * 60 * 60;"
"const maxAge = 365 * 24 * 60 * 60;" + + "document.cookie = 'votianlt.language=' + $0 + ';path=/;max-age=' + maxAge + ';SameSite=Lax';",
"document.cookie = 'votianlt.language=' + $0 + ';path=/;max-age=' + maxAge + ';SameSite=Lax';", languageCode);
languageCode
);
} }
private Component createAuthenticatedNavigation() { private Component createAuthenticatedNavigation() {
@@ -307,7 +307,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
featuresGrid.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.STRETCH); featuresGrid.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.STRETCH);
// Feature Cards // Feature Cards
featuresGrid.add(createFeatureCard(VaadinIcon.COG, getTranslation("start.feature.setup.title"), featuresGrid.add(
createFeatureCard(VaadinIcon.COG, getTranslation("start.feature.setup.title"),
getTranslation("start.feature.setup.desc")), getTranslation("start.feature.setup.desc")),
createFeatureCard(VaadinIcon.USERS, getTranslation("start.feature.customers.title"), createFeatureCard(VaadinIcon.USERS, getTranslation("start.feature.customers.title"),
getTranslation("start.feature.customers.desc")), getTranslation("start.feature.customers.desc")),
@@ -389,8 +390,10 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
companyInfo.setPadding(false); companyInfo.setPadding(false);
companyInfo.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); companyInfo.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
companyInfo.add(new Paragraph(getTranslation("start.imprint.company")), new Paragraph(getTranslation("start.imprint.address")), companyInfo.add(new Paragraph(getTranslation("start.imprint.company")),
new Paragraph(getTranslation("start.imprint.phone")), new Paragraph(getTranslation("start.imprint.email"))); new Paragraph(getTranslation("start.imprint.address")),
new Paragraph(getTranslation("start.imprint.phone")),
new Paragraph(getTranslation("start.imprint.email")));
// Call to Action // Call to Action
Paragraph ctaText = new Paragraph(getTranslation("start.cta.text")); Paragraph ctaText = new Paragraph(getTranslation("start.cta.text"));

View File

@@ -135,8 +135,8 @@ public class UserMessagesView extends Main implements HasUrlParameter<String>, H
LocalDateTime lastMessageTime = latest != null ? latest.getCreatedAt() : null; LocalDateTime lastMessageTime = latest != null ? latest.getCreatedAt() : null;
String preview = resolvePreview(latest); String preview = resolvePreview(latest);
section.add(createMessageCard(getTranslation("usermessages.general.conversation"), preview, lastMessageTime, messageCount, unreadCount, section.add(createMessageCard(getTranslation("usermessages.general.conversation"), preview, lastMessageTime,
"general")); messageCount, unreadCount, "general"));
return section; return section;
} }
@@ -233,8 +233,7 @@ public class UserMessagesView extends Main implements HasUrlParameter<String>, H
titleRow.expand(titleSpan); titleRow.expand(titleSpan);
// Preview text // Preview text
Span preview = new Span( Span preview = new Span(Optional.ofNullable(lastMessagePreview).filter(s -> !s.isBlank())
Optional.ofNullable(lastMessagePreview).filter(s -> !s.isBlank())
.orElse(getTranslation("usermessages.preview.empty"))); .orElse(getTranslation("usermessages.preview.empty")));
preview.getStyle().set("color", "#666666"); preview.getStyle().set("color", "#666666");
preview.getStyle().set("font-size", "14px"); preview.getStyle().set("font-size", "14px");

View File

@@ -459,11 +459,8 @@ public class CustomerInvoiceService {
StringBuilder html = new StringBuilder(); StringBuilder html = new StringBuilder();
// Sample data for preview (will be replaced with actual job data later) // Sample data for preview (will be replaced with actual job data later)
String[][] sampleData = { String[][] sampleData = { { "Umzugsleistung inkl. Verpackung", "19%", "450,00 €" },
{"Umzugsleistung inkl. Verpackung", "19%", "450,00 €"}, { "Entsorgung Möbel", "19%", "85,00 €" }, { "Montage/De-Montage", "19%", "120,00 €" } };
{"Entsorgung Möbel", "19%", "85,00 €"},
{"Montage/De-Montage", "19%", "120,00 €"}
};
// Calculate totals // Calculate totals
double netTotal = 655.00; double netTotal = 655.00;
@@ -478,18 +475,25 @@ public class CustomerInvoiceService {
// Header row // Header row
html.append("<tr style='background-color:#f5f5f5;border-bottom:1px solid #cccccc;'>"); html.append("<tr style='background-color:#f5f5f5;border-bottom:1px solid #cccccc;'>");
html.append("<th style='text-align:left;padding:4px 8px;font-weight:bold;width:55%;white-space:nowrap;'>Name</th>"); html.append(
html.append("<th style='text-align:right;padding:4px 8px;font-weight:bold;width:20%;white-space:nowrap;'>Steuersatz</th>"); "<th style='text-align:left;padding:4px 8px;font-weight:bold;width:55%;white-space:nowrap;'>Name</th>");
html.append("<th style='text-align:right;padding:4px 8px;font-weight:bold;width:25%;white-space:nowrap;'>Nettobetrag</th>"); html.append(
"<th style='text-align:right;padding:4px 8px;font-weight:bold;width:20%;white-space:nowrap;'>Steuersatz</th>");
html.append(
"<th style='text-align:right;padding:4px 8px;font-weight:bold;width:25%;white-space:nowrap;'>Nettobetrag</th>");
html.append("</tr>"); html.append("</tr>");
// Data rows // Data rows
for (int i = 0; i < sampleData.length; i++) { for (int i = 0; i < sampleData.length; i++) {
String bgColor = (i % 2 == 1) ? "background-color:rgba(0,0,0,0.02);" : ""; String bgColor = (i % 2 == 1) ? "background-color:rgba(0,0,0,0.02);" : "";
html.append("<tr style='").append(bgColor).append("border-bottom:1px solid #eeeeee;'>"); html.append("<tr style='").append(bgColor).append("border-bottom:1px solid #eeeeee;'>");
html.append("<td style='text-align:left;padding:4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'>").append(sampleData[i][0]).append("</td>"); html.append(
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(sampleData[i][1]).append("</td>"); "<td style='text-align:left;padding:4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'>")
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(sampleData[i][2]).append("</td>"); .append(sampleData[i][0]).append("</td>");
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(sampleData[i][1])
.append("</td>");
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(sampleData[i][2])
.append("</td>");
html.append("</tr>"); html.append("</tr>");
} }
@@ -505,21 +509,26 @@ public class CustomerInvoiceService {
html.append("<tr>"); html.append("<tr>");
html.append("<td style='width:55%;padding:2px 0;'></td>"); html.append("<td style='width:55%;padding:2px 0;'></td>");
html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>Nettosumme:</td>"); html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>Nettosumme:</td>");
html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>").append(String.format(java.util.Locale.GERMANY, "%,.2f €", netTotal)).append("</td>"); html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>")
.append(String.format(java.util.Locale.GERMANY, "%,.2f €", netTotal)).append("</td>");
html.append("</tr>"); html.append("</tr>");
// Umsatzsteuer - label in col 2, value in col 3 // Umsatzsteuer - label in col 2, value in col 3
html.append("<tr>"); html.append("<tr>");
html.append("<td style='width:55%;padding:2px 0;'></td>"); html.append("<td style='width:55%;padding:2px 0;'></td>");
html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>zzgl. 19% USt:</td>"); html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>zzgl. 19% USt:</td>");
html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>").append(String.format(java.util.Locale.GERMANY, "%,.2f €", vatTotal)).append("</td>"); html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>")
.append(String.format(java.util.Locale.GERMANY, "%,.2f €", vatTotal)).append("</td>");
html.append("</tr>"); html.append("</tr>");
// Gesamtsumme - label in col 2, value in col 3 // Gesamtsumme - label in col 2, value in col 3
html.append("<tr>"); html.append("<tr>");
html.append("<td style='width:55%;padding:2px 0;'></td>"); html.append("<td style='width:55%;padding:2px 0;'></td>");
html.append("<td style='width:20%;text-align:left;padding:4px 8px;white-space:nowrap;font-weight:bold;font-size:1.05em;'>Gesamtsumme:</td>"); html.append(
html.append("<td style='width:25%;text-align:right;padding:4px 8px;white-space:nowrap;font-weight:bold;font-size:1.05em;'>").append(String.format(java.util.Locale.GERMANY, "%,.2f €", grossTotal)).append("</td>"); "<td style='width:20%;text-align:left;padding:4px 8px;white-space:nowrap;font-weight:bold;font-size:1.05em;'>Gesamtsumme:</td>");
html.append(
"<td style='width:25%;text-align:right;padding:4px 8px;white-space:nowrap;font-weight:bold;font-size:1.05em;'>")
.append(String.format(java.util.Locale.GERMANY, "%,.2f €", grossTotal)).append("</td>");
html.append("</tr>"); html.append("</tr>");
html.append("</table>"); html.append("</table>");
@@ -530,8 +539,9 @@ public class CustomerInvoiceService {
} }
/** /**
* Generate a PDF preview from canvas template data with pre-built variables map. * Generate a PDF preview from canvas template data with pre-built variables
* This version accepts a pre-built variables map instead of building it from the User object. * 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<String, String> variables, de.assecutor.votianlt.model.User user) throws Exception { java.util.Map<String, String> variables, de.assecutor.votianlt.model.User user) throws Exception {
@@ -542,7 +552,8 @@ public class CustomerInvoiceService {
// Parse the JSON template data - JSON is stored as a string in the database // 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(); 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); String jsonContent = mapper.readValue(jsonTemplateData, String.class);
com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonContent); com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonContent);
@@ -597,9 +608,11 @@ public class CustomerInvoiceService {
String variable = element.has("variable") ? element.get("variable").asText(null) : null; String variable = element.has("variable") ? element.get("variable").asText(null) : null;
String text = ""; String text = "";
System.out.println("DEBUG: Processing element " + elementCount + ", type=" + type + ", variable=" + variable); System.out.println(
"DEBUG: Processing element " + elementCount + ", type=" + type + ", variable=" + variable);
// Use percentage values if available, otherwise fall back to legacy pixel values // Use percentage values if available, otherwise fall back to legacy pixel
// values
double xPercent, yPercent, widthPercent, heightPercent; double xPercent, yPercent, widthPercent, heightPercent;
if (element.has("xPercent")) { if (element.has("xPercent")) {
xPercent = element.get("xPercent").asDouble(0); xPercent = element.get("xPercent").asDouble(0);
@@ -671,7 +684,8 @@ public class CustomerInvoiceService {
System.out.println("DEBUG: Replaced variable " + variable + " with: " + text); System.out.println("DEBUG: Replaced variable " + variable + " with: " + text);
} else if (variable != null) { } else if (variable != null) {
// Variable exists but no value provided - use placeholder // 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"); System.out.println("DEBUG: No value for variable " + variable + ", using placeholder");
} else { } else {
System.out.println("DEBUG: Using static text: " + text); System.out.println("DEBUG: Using static text: " + text);
@@ -744,8 +758,8 @@ public class CustomerInvoiceService {
if (servicesJson != null && !servicesJson.isEmpty() && !servicesJson.equals("[]")) { if (servicesJson != null && !servicesJson.isEmpty() && !servicesJson.equals("[]")) {
try { try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
com.fasterxml.jackson.core.type.TypeReference<java.util.List<java.util.Map<String, String>>> typeRef = com.fasterxml.jackson.core.type.TypeReference<java.util.List<java.util.Map<String, String>>> typeRef = new com.fasterxml.jackson.core.type.TypeReference<>() {
new com.fasterxml.jackson.core.type.TypeReference<>() {}; };
servicesData = mapper.readValue(servicesJson, typeRef); servicesData = mapper.readValue(servicesJson, typeRef);
} catch (Exception e) { } catch (Exception e) {
System.err.println("DEBUG: Failed to parse services JSON: " + e.getMessage()); System.err.println("DEBUG: Failed to parse services JSON: " + e.getMessage());
@@ -760,16 +774,20 @@ public class CustomerInvoiceService {
// Header row // Header row
html.append("<tr style='background-color:#f5f5f5;border-bottom:1px solid #cccccc;'>"); html.append("<tr style='background-color:#f5f5f5;border-bottom:1px solid #cccccc;'>");
html.append("<th style='text-align:left;padding:4px 8px;font-weight:bold;width:55%;white-space:nowrap;'>Name</th>"); html.append(
html.append("<th style='text-align:right;padding:4px 8px;font-weight:bold;width:20%;white-space:nowrap;'>Steuersatz</th>"); "<th style='text-align:left;padding:4px 8px;font-weight:bold;width:55%;white-space:nowrap;'>Name</th>");
html.append("<th style='text-align:right;padding:4px 8px;font-weight:bold;width:25%;white-space:nowrap;'>Nettobetrag</th>"); html.append(
"<th style='text-align:right;padding:4px 8px;font-weight:bold;width:20%;white-space:nowrap;'>Steuersatz</th>");
html.append(
"<th style='text-align:right;padding:4px 8px;font-weight:bold;width:25%;white-space:nowrap;'>Nettobetrag</th>");
html.append("</tr>"); html.append("</tr>");
// Data rows - use actual service data from the job // Data rows - use actual service data from the job
if (servicesData.isEmpty()) { if (servicesData.isEmpty()) {
// Fallback: show a single row with no data // Fallback: show a single row with no data
html.append("<tr style='border-bottom:1px solid #eeeeee;'>"); html.append("<tr style='border-bottom:1px solid #eeeeee;'>");
html.append("<td colspan='3' style='text-align:center;padding:4px 8px;white-space:nowrap;'>Keine Leistungen vorhanden</td>"); html.append(
"<td colspan='3' style='text-align:center;padding:4px 8px;white-space:nowrap;'>Keine Leistungen vorhanden</td>");
html.append("</tr>"); html.append("</tr>");
} else { } else {
for (int i = 0; i < servicesData.size(); i++) { for (int i = 0; i < servicesData.size(); i++) {
@@ -780,9 +798,13 @@ public class CustomerInvoiceService {
String bgColor = (i % 2 == 1) ? "background-color:rgba(0,0,0,0.02);" : ""; String bgColor = (i % 2 == 1) ? "background-color:rgba(0,0,0,0.02);" : "";
html.append("<tr style='").append(bgColor).append("border-bottom:1px solid #eeeeee;'>"); html.append("<tr style='").append(bgColor).append("border-bottom:1px solid #eeeeee;'>");
html.append("<td style='text-align:left;padding:4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'>").append(escapeHtml(name)).append("</td>"); html.append(
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(serviceVatRate).append("</td>"); "<td style='text-align:left;padding:4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'>")
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(netAmount).append("</td>"); .append(escapeHtml(name)).append("</td>");
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(serviceVatRate)
.append("</td>");
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(netAmount)
.append(" €</td>");
html.append("</tr>"); html.append("</tr>");
} }
} }
@@ -800,21 +822,27 @@ public class CustomerInvoiceService {
html.append("<tr>"); html.append("<tr>");
html.append("<td style='width:55%;padding:2px 0;'></td>"); html.append("<td style='width:55%;padding:2px 0;'></td>");
html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>Nettosumme:</td>"); html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>Nettosumme:</td>");
html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>").append(netTotal).append("</td>"); html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>")
.append(netTotal).append("</td>");
html.append("</tr>"); html.append("</tr>");
// Umsatzsteuer // Umsatzsteuer
html.append("<tr>"); html.append("<tr>");
html.append("<td style='width:55%;padding:2px 0;'></td>"); html.append("<td style='width:55%;padding:2px 0;'></td>");
html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>zzgl. ").append(vatPercent).append("% USt:</td>"); html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>zzgl. ")
html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>").append(vatTotal).append("</td>"); .append(vatPercent).append("% USt:</td>");
html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>")
.append(vatTotal).append("</td>");
html.append("</tr>"); html.append("</tr>");
// Gesamtsumme // Gesamtsumme
html.append("<tr>"); html.append("<tr>");
html.append("<td style='width:55%;padding:2px 0;'></td>"); html.append("<td style='width:55%;padding:2px 0;'></td>");
html.append("<td style='width:20%;text-align:left;padding:4px 8px;white-space:nowrap;font-weight:bold;font-size:1.05em;'>Gesamtsumme:</td>"); html.append(
html.append("<td style='width:25%;text-align:right;padding:4px 8px;white-space:nowrap;font-weight:bold;font-size:1.05em;'>").append(grossTotal).append("</td>"); "<td style='width:20%;text-align:left;padding:4px 8px;white-space:nowrap;font-weight:bold;font-size:1.05em;'>Gesamtsumme:</td>");
html.append(
"<td style='width:25%;text-align:right;padding:4px 8px;white-space:nowrap;font-weight:bold;font-size:1.05em;'>")
.append(grossTotal).append("</td>");
html.append("</tr>"); html.append("</tr>");
html.append("</table>"); html.append("</table>");
@@ -831,10 +859,7 @@ public class CustomerInvoiceService {
if (input == null) { if (input == null) {
return ""; return "";
} }
return input.replace("&", "&amp;") return input.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#x27;"); .replace("'", "&#x27;");
} }
} }