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

@@ -9,8 +9,8 @@ const COOKIE_MAX_AGE_DAYS = 365; // Cookie gültig für 1 Jahr
* @param languageCode - The language code (e.g., 'de', 'en', 'fr', 'es')
*/
export function setLanguageCookie(languageCode: string): void {
const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60; // Convert days to seconds
document.cookie = `${LANGUAGE_COOKIE_NAME}=${languageCode};path=/;max-age=${maxAge};SameSite=Lax`;
const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60; // Convert days to seconds
document.cookie = `${LANGUAGE_COOKIE_NAME}=${languageCode};path=/;max-age=${maxAge};SameSite=Lax`;
}
/**
@@ -18,39 +18,39 @@ export function setLanguageCookie(languageCode: string): void {
* @returns The language code or null if not found
*/
export function getLanguageCookie(): string | null {
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === LANGUAGE_COOKIE_NAME) {
return decodeURIComponent(value);
}
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === LANGUAGE_COOKIE_NAME) {
return decodeURIComponent(value);
}
return null;
}
return null;
}
/**
* Clears the language cookie
*/
export function clearLanguageCookie(): void {
document.cookie = `${LANGUAGE_COOKIE_NAME}=;path=/;max-age=0;SameSite=Lax`;
document.cookie = `${LANGUAGE_COOKIE_NAME}=;path=/;max-age=0;SameSite=Lax`;
}
/**
* Maps Language enum values to locale strings
*/
export const languageToLocale: Record<string, string> = {
'DE': 'de',
'EN': 'en',
'FR': 'fr',
'ES': 'es'
DE: 'de',
EN: 'en',
FR: 'fr',
ES: 'es',
};
/**
* Maps locale strings to Language enum values
*/
export const localeToLanguage: Record<string, string> = {
'de': 'DE',
'en': 'EN',
'fr': 'FR',
'es': 'ES'
de: 'DE',
en: 'EN',
fr: 'FR',
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
* constructed. Registered via {@code UIInitListener} → {@code BeforeEnterListener},
* which fires prior to the router creating the layout component tree.
* constructed. Registered via {@code UIInitListener} →
* {@code BeforeEnterListener}, which fires prior to the router creating the
* layout component tree.
*
* For authenticated users: Uses the language preference from the user profile.
* For anonymous users: Uses the language from the 'votianlt.language' cookie
* or falls back to the browser's preferred locale.
* For anonymous users: Uses the language from the 'votianlt.language' cookie or
* falls back to the browser's preferred locale.
*/
@Component
@Slf4j
@@ -126,26 +127,26 @@ public class LocaleVaadinInitListener implements VaadinServiceInitListener {
// Check if the browser locale is supported
String language = locale.getLanguage().toLowerCase();
return switch (language) {
case "de" -> Locale.GERMAN;
case "en" -> Locale.ENGLISH;
case "fr" -> Locale.FRENCH;
case "es" -> Locale.of("es", "ES");
default -> null; // Return null to use Vaadin's default
case "de" -> Locale.GERMAN;
case "en" -> Locale.ENGLISH;
case "fr" -> Locale.FRENCH;
case "es" -> Locale.of("es", "ES");
default -> null; // Return null to use Vaadin's default
};
}
private Locale getLocaleFromLanguage(Language language) {
return switch (language) {
case DE -> Locale.GERMAN;
case EN -> Locale.ENGLISH;
case FR -> Locale.FRENCH;
case ES -> Locale.of("es", "ES");
case TR -> Locale.of("tr", "TR");
case PL -> Locale.of("pl", "PL");
case RU -> Locale.of("ru", "RU");
case EE -> Locale.of("et", "EE");
case LV -> Locale.of("lv", "LV");
case LT -> Locale.of("lt", "LT");
case DE -> Locale.GERMAN;
case EN -> Locale.ENGLISH;
case FR -> Locale.FRENCH;
case ES -> Locale.of("es", "ES");
case TR -> Locale.of("tr", "TR");
case PL -> Locale.of("pl", "PL");
case RU -> Locale.of("ru", "RU");
case EE -> Locale.of("et", "EE");
case LV -> Locale.of("lv", "LV");
case LT -> Locale.of("lt", "LT");
};
}
@@ -155,17 +156,17 @@ public class LocaleVaadinInitListener implements VaadinServiceInitListener {
}
return switch (languageCode.toUpperCase()) {
case "DE" -> Locale.GERMAN;
case "EN" -> Locale.ENGLISH;
case "FR" -> Locale.FRENCH;
case "ES" -> Locale.of("es", "ES");
case "TR" -> Locale.of("tr", "TR");
case "PL" -> Locale.of("pl", "PL");
case "RU" -> Locale.of("ru", "RU");
case "EE" -> Locale.of("et", "EE");
case "LV" -> Locale.of("lv", "LV");
case "LT" -> Locale.of("lt", "LT");
default -> null;
case "DE" -> Locale.GERMAN;
case "EN" -> Locale.ENGLISH;
case "FR" -> Locale.FRENCH;
case "ES" -> Locale.of("es", "ES");
case "TR" -> Locale.of("tr", "TR");
case "PL" -> Locale.of("pl", "PL");
case "RU" -> Locale.of("ru", "RU");
case "EE" -> Locale.of("et", "EE");
case "LV" -> Locale.of("lv", "LV");
case "LT" -> Locale.of("lt", "LT");
default -> null;
};
}
}

View File

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

View File

@@ -28,12 +28,12 @@ public enum TaskType {
// Fallback to German translations
return switch (this) {
case CONFIRMATION -> "Bestätigung";
case SIGNATURE -> "Unterschrift";
case TODOLIST -> "To-Do Liste";
case PHOTO -> "Foto";
case BARCODE -> "Barcode";
case COMMENT -> "Kommentar";
case CONFIRMATION -> "Bestätigung";
case SIGNATURE -> "Unterschrift";
case TODOLIST -> "To-Do Liste";
case PHOTO -> "Foto";
case BARCODE -> "Barcode";
case COMMENT -> "Kommentar";
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -148,19 +148,23 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
// Total jobs card
long totalJobs = jobRepository.count();
cards.add(createStatCard(getTranslation("admindashboard.stat.totaljobs"), String.valueOf(totalJobs), VaadinIcon.PACKAGE, "blue"));
cards.add(createStatCard(getTranslation("admindashboard.stat.totaljobs"), String.valueOf(totalJobs),
VaadinIcon.PACKAGE, "blue"));
// Total users card
long totalUsers = userRepository.count();
cards.add(createStatCard(getTranslation("admindashboard.stat.users"), String.valueOf(totalUsers), VaadinIcon.USERS, "green"));
cards.add(createStatCard(getTranslation("admindashboard.stat.users"), String.valueOf(totalUsers),
VaadinIcon.USERS, "green"));
// Total app users card
long totalAppUsers = appUserRepository.count();
cards.add(createStatCard(getTranslation("admindashboard.stat.appusers"), String.valueOf(totalAppUsers), VaadinIcon.MOBILE, "purple"));
cards.add(createStatCard(getTranslation("admindashboard.stat.appusers"), String.valueOf(totalAppUsers),
VaadinIcon.MOBILE, "purple"));
// Current time
String currentTime = DateTimeFormatUtil.formatDateTime(LocalDateTime.now());
cards.add(createStatCard(getTranslation("admindashboard.stat.lastupdated"), currentTime, VaadinIcon.CLOCK, "gray"));
cards.add(createStatCard(getTranslation("admindashboard.stat.lastupdated"), currentTime, VaadinIcon.CLOCK,
"gray"));
section.add(title, cards);
return section;
@@ -185,17 +189,22 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
long inProgressJobs = jobRepository.countByStatus(JobStatus.IN_PROGRESS);
long completedJobs = jobRepository.countByStatus(JobStatus.COMPLETED);
cards.add(createStatCard(getTranslation("admindashboard.stat.openjobs"), String.valueOf(openJobs), VaadinIcon.HOURGLASS_START, "orange"));
cards.add(createStatCard(getTranslation("admindashboard.stat.inprogress"), String.valueOf(inProgressJobs), VaadinIcon.PLAY, "blue"));
cards.add(createStatCard(getTranslation("admindashboard.stat.completed"), String.valueOf(completedJobs), VaadinIcon.CHECK_CIRCLE, "green"));
cards.add(createStatCard(getTranslation("admindashboard.stat.openjobs"), String.valueOf(openJobs),
VaadinIcon.HOURGLASS_START, "orange"));
cards.add(createStatCard(getTranslation("admindashboard.stat.inprogress"), String.valueOf(inProgressJobs),
VaadinIcon.PLAY, "blue"));
cards.add(createStatCard(getTranslation("admindashboard.stat.completed"), String.valueOf(completedJobs),
VaadinIcon.CHECK_CIRCLE, "green"));
// Total cargo items
long totalCargoItems = cargoItemRepository.count();
cards.add(createStatCard(getTranslation("admindashboard.stat.cargo"), String.valueOf(totalCargoItems), VaadinIcon.CUBE, "purple"));
cards.add(createStatCard(getTranslation("admindashboard.stat.cargo"), String.valueOf(totalCargoItems),
VaadinIcon.CUBE, "purple"));
} catch (Exception e) {
log.warn("Could not load job statistics by status", e);
cards.add(createStatCard(getTranslation("admindashboard.stat.status.info"), getTranslation("admindashboard.stat.status.unavailable"), VaadinIcon.WARNING, "red"));
cards.add(createStatCard(getTranslation("admindashboard.stat.status.info"),
getTranslation("admindashboard.stat.status.unavailable"), VaadinIcon.WARNING, "red"));
}
section.add(title, cards);
@@ -217,20 +226,23 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
// Total tasks
long totalTasks = taskRepository.count();
cards.add(createStatCard(getTranslation("admindashboard.stat.totaltasks"), String.valueOf(totalTasks), VaadinIcon.TASKS, "blue"));
cards.add(createStatCard(getTranslation("admindashboard.stat.totaltasks"), String.valueOf(totalTasks),
VaadinIcon.TASKS, "blue"));
// Completed tasks
long completedTasks = taskRepository.countByCompleted(true);
cards.add(createStatCard(getTranslation("admindashboard.stat.completedtasks"), String.valueOf(completedTasks), VaadinIcon.CHECK, "green"));
cards.add(createStatCard(getTranslation("admindashboard.stat.completedtasks"), String.valueOf(completedTasks),
VaadinIcon.CHECK, "green"));
// Pending tasks
long pendingTasks = totalTasks - completedTasks;
cards.add(createStatCard(getTranslation("admindashboard.stat.pendingtasks"), String.valueOf(pendingTasks), VaadinIcon.CLOCK, "orange"));
cards.add(createStatCard(getTranslation("admindashboard.stat.pendingtasks"), String.valueOf(pendingTasks),
VaadinIcon.CLOCK, "orange"));
// Completion rate
double completionRate = totalTasks > 0 ? (completedTasks * 100.0 / totalTasks) : 0;
cards.add(createStatCard(getTranslation("admindashboard.stat.successrate"), String.format("%.1f%%", completionRate), VaadinIcon.TRENDING_UP,
"purple"));
cards.add(createStatCard(getTranslation("admindashboard.stat.successrate"),
String.format("%.1f%%", completionRate), VaadinIcon.TRENDING_UP, "purple"));
section.add(title, cards);
return section;
@@ -251,16 +263,20 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
// Content statistics
long totalPhotos = photoRepository.count();
cards.add(createStatCard(getTranslation("admindashboard.stat.photos"), String.valueOf(totalPhotos), VaadinIcon.CAMERA, "blue"));
cards.add(createStatCard(getTranslation("admindashboard.stat.photos"), String.valueOf(totalPhotos),
VaadinIcon.CAMERA, "blue"));
long totalBarcodes = barcodeRepository.count();
cards.add(createStatCard(getTranslation("admindashboard.stat.barcodes"), String.valueOf(totalBarcodes), VaadinIcon.BARCODE, "green"));
cards.add(createStatCard(getTranslation("admindashboard.stat.barcodes"), String.valueOf(totalBarcodes),
VaadinIcon.BARCODE, "green"));
long totalSignatures = signatureRepository.count();
cards.add(createStatCard(getTranslation("admindashboard.stat.signatures"), String.valueOf(totalSignatures), VaadinIcon.EDIT, "purple"));
cards.add(createStatCard(getTranslation("admindashboard.stat.signatures"), String.valueOf(totalSignatures),
VaadinIcon.EDIT, "purple"));
long totalComments = commentRepository.count();
cards.add(createStatCard(getTranslation("admindashboard.stat.comments"), String.valueOf(totalComments), VaadinIcon.COMMENT, "orange"));
cards.add(createStatCard(getTranslation("admindashboard.stat.comments"), String.valueOf(totalComments),
VaadinIcon.COMMENT, "orange"));
section.add(title, cards);
return section;
@@ -282,16 +298,20 @@ public class AdminDashboardView extends Main implements HasDynamicTitle {
// Database connection status
try {
userRepository.count(); // Test database connection
cards.add(createStatCard(getTranslation("admindashboard.stat.database"), getTranslation("admindashboard.stat.database.connected"), VaadinIcon.DATABASE, "green"));
cards.add(createStatCard(getTranslation("admindashboard.stat.database"),
getTranslation("admindashboard.stat.database.connected"), VaadinIcon.DATABASE, "green"));
} catch (Exception e) {
cards.add(createStatCard(getTranslation("admindashboard.stat.database"), getTranslation("admindashboard.stat.database.error"), VaadinIcon.DATABASE, "red"));
cards.add(createStatCard(getTranslation("admindashboard.stat.database"),
getTranslation("admindashboard.stat.database.error"), VaadinIcon.DATABASE, "red"));
}
// Messaging status
cards.add(createStatCard(getTranslation("admindashboard.stat.websocket"), getTranslation("admindashboard.stat.websocket.active"), VaadinIcon.CONNECT, "green"));
cards.add(createStatCard(getTranslation("admindashboard.stat.websocket"),
getTranslation("admindashboard.stat.websocket.active"), VaadinIcon.CONNECT, "green"));
// System uptime (placeholder)
cards.add(createStatCard(getTranslation("admindashboard.stat.app"), getTranslation("admindashboard.stat.app.running"), VaadinIcon.HEART, "green"));
cards.add(createStatCard(getTranslation("admindashboard.stat.app"),
getTranslation("admindashboard.stat.app.running"), VaadinIcon.HEART, "green"));
// Memory usage (placeholder)
Runtime runtime = Runtime.getRuntime();

View File

@@ -75,9 +75,11 @@ public class AdminPricetableView extends VerticalLayout implements HasDynamicTit
priceTable.setRevenueParticipation(revenueParticipation.getValue());
priceTableRepository.save(priceTable);
Notification.show(getTranslation("adminpricetable.notification.saved"), 3000, Notification.Position.BOTTOM_CENTER);
Notification.show(getTranslation("adminpricetable.notification.saved"), 3000,
Notification.Position.BOTTOM_CENTER);
} catch (Exception ex) {
Notification.show(getTranslation("adminpricetable.notification.save.error", ex.getMessage()), 5000, Notification.Position.BOTTOM_CENTER);
Notification.show(getTranslation("adminpricetable.notification.save.error", ex.getMessage()), 5000,
Notification.Position.BOTTOM_CENTER);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -107,7 +107,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
}
private Button createLanguageSelector() {
// Aktuelle Sprache aus der UI-Locale ermitteln (wird vom LocaleVaadinInitListener gesetzt)
// Aktuelle Sprache aus der UI-Locale ermitteln (wird vom
// LocaleVaadinInitListener gesetzt)
String currentLang = getCurrentLanguageFromLocale();
String flag = getFlagForLanguage(currentLang);
@@ -134,7 +135,8 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
}
private String getCurrentLanguageFromLocale() {
// Aktuelle UI-Locale verwenden (wird vom LocaleVaadinInitListener aus Cookie/Browser gesetzt)
// Aktuelle UI-Locale verwenden (wird vom LocaleVaadinInitListener aus
// Cookie/Browser gesetzt)
Locale locale = UI.getCurrent().getLocale();
if (locale == null) {
return "DE";
@@ -142,16 +144,16 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
String language = locale.getLanguage();
return switch (language) {
case "en" -> "EN";
case "fr" -> "FR";
case "es" -> "ES";
case "tr" -> "TR";
case "pl" -> "PL";
case "ru" -> "RU";
case "et" -> "EE"; // Estnisch
case "lv" -> "LV"; // Lettisch
case "lt" -> "LT"; // Litauisch
default -> "DE"; // German as default
case "en" -> "EN";
case "fr" -> "FR";
case "es" -> "ES";
case "tr" -> "TR";
case "pl" -> "PL";
case "ru" -> "RU";
case "et" -> "EE"; // Estnisch
case "lv" -> "LV"; // Lettisch
case "lt" -> "LT"; // Litauisch
default -> "DE"; // German as default
};
}
@@ -160,26 +162,24 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
return "🇩🇪";
}
return switch (languageCode.toUpperCase()) {
case "DE" -> "🇩🇪";
case "EN" -> "🇬🇧";
case "FR" -> "🇫🇷";
case "ES" -> "🇪🇸";
case "TR" -> "🇹🇷";
case "PL" -> "🇵🇱";
case "RU" -> "🇷🇺";
case "EE" -> "🇪🇪";
case "LV" -> "🇱🇻";
case "LT" -> "🇱🇹";
default -> "🇩🇪";
case "DE" -> "🇩🇪";
case "EN" -> "🇬🇧";
case "FR" -> "🇫🇷";
case "ES" -> "🇪🇸";
case "TR" -> "🇹🇷";
case "PL" -> "🇵🇱";
case "RU" -> "🇷🇺";
case "EE" -> "🇪🇪";
case "LV" -> "🇱🇻";
case "LT" -> "🇱🇹";
default -> "🇩🇪";
};
}
private void setLanguageCookie(String languageCode) {
UI.getCurrent().getPage().executeJs(
"const maxAge = 365 * 24 * 60 * 60;" +
"document.cookie = 'votianlt.language=' + $0 + ';path=/;max-age=' + maxAge + ';SameSite=Lax';",
languageCode
);
UI.getCurrent().getPage().executeJs("const maxAge = 365 * 24 * 60 * 60;"
+ "document.cookie = 'votianlt.language=' + $0 + ';path=/;max-age=' + maxAge + ';SameSite=Lax';",
languageCode);
}
private Component createAuthenticatedNavigation() {
@@ -307,8 +307,9 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
featuresGrid.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.STRETCH);
// Feature Cards
featuresGrid.add(createFeatureCard(VaadinIcon.COG, getTranslation("start.feature.setup.title"),
getTranslation("start.feature.setup.desc")),
featuresGrid.add(
createFeatureCard(VaadinIcon.COG, getTranslation("start.feature.setup.title"),
getTranslation("start.feature.setup.desc")),
createFeatureCard(VaadinIcon.USERS, getTranslation("start.feature.customers.title"),
getTranslation("start.feature.customers.desc")),
createFeatureCard(VaadinIcon.CLIPBOARD_TEXT, getTranslation("start.feature.jobs.title"),
@@ -389,8 +390,10 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha
companyInfo.setPadding(false);
companyInfo.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
companyInfo.add(new Paragraph(getTranslation("start.imprint.company")), new Paragraph(getTranslation("start.imprint.address")),
new Paragraph(getTranslation("start.imprint.phone")), new Paragraph(getTranslation("start.imprint.email")));
companyInfo.add(new Paragraph(getTranslation("start.imprint.company")),
new Paragraph(getTranslation("start.imprint.address")),
new Paragraph(getTranslation("start.imprint.phone")),
new Paragraph(getTranslation("start.imprint.email")));
// Call to Action
Paragraph ctaText = new Paragraph(getTranslation("start.cta.text"));

View File

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

View File

@@ -459,11 +459,8 @@ public class CustomerInvoiceService {
StringBuilder html = new StringBuilder();
// Sample data for preview (will be replaced with actual job data later)
String[][] sampleData = {
{"Umzugsleistung inkl. Verpackung", "19%", "450,00 €"},
{"Entsorgung Möbel", "19%", "85,00 €"},
{"Montage/De-Montage", "19%", "120,00 €"}
};
String[][] sampleData = { { "Umzugsleistung inkl. Verpackung", "19%", "450,00 €" },
{ "Entsorgung Möbel", "19%", "85,00 €" }, { "Montage/De-Montage", "19%", "120,00 €" } };
// Calculate totals
double netTotal = 655.00;
@@ -478,18 +475,25 @@ public class CustomerInvoiceService {
// Header row
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("<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(
"<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: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>");
// Data rows
for (int i = 0; i < sampleData.length; i++) {
String bgColor = (i % 2 == 1) ? "background-color:rgba(0,0,0,0.02);" : "";
html.append("<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("<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(
"<td style='text-align:left;padding:4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'>")
.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>");
}
@@ -505,21 +509,26 @@ public class CustomerInvoiceService {
html.append("<tr>");
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: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>");
// Umsatzsteuer - label in col 2, value in col 3
html.append("<tr>");
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: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>");
// Gesamtsumme - label in col 2, value in col 3
html.append("<tr>");
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("<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(
"<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("</table>");
@@ -530,8 +539,9 @@ public class CustomerInvoiceService {
}
/**
* Generate a PDF preview from canvas template data with pre-built variables map.
* This version accepts a pre-built variables map instead of building it from the User object.
* Generate a PDF preview from canvas template data with pre-built variables
* map. This version accepts a pre-built variables map instead of building it
* from the User object.
*/
public byte[] generatePdfFromCanvasTemplateWithData(String jsonTemplateData,
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
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
// The template data is stored as a JSON string, so we need to parse it as a string first
// The template data is stored as a JSON string, so we need to parse it as a
// string first
String jsonContent = mapper.readValue(jsonTemplateData, String.class);
com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonContent);
@@ -597,9 +608,11 @@ public class CustomerInvoiceService {
String variable = element.has("variable") ? element.get("variable").asText(null) : null;
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;
if (element.has("xPercent")) {
xPercent = element.get("xPercent").asDouble(0);
@@ -671,7 +684,8 @@ public class CustomerInvoiceService {
System.out.println("DEBUG: Replaced variable " + variable + " with: " + text);
} else if (variable != null) {
// Variable exists but no value provided - use placeholder
text = "[" + variable.replace("masterdata.", "").replace("customer.", "").replace("invoice.", "").replace("job.", "") + "]";
text = "[" + variable.replace("masterdata.", "").replace("customer.", "").replace("invoice.", "")
.replace("job.", "") + "]";
System.out.println("DEBUG: No value for variable " + variable + ", using placeholder");
} else {
System.out.println("DEBUG: Using static text: " + text);
@@ -744,8 +758,8 @@ public class CustomerInvoiceService {
if (servicesJson != null && !servicesJson.isEmpty() && !servicesJson.equals("[]")) {
try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
com.fasterxml.jackson.core.type.TypeReference<java.util.List<java.util.Map<String, String>>> typeRef =
new com.fasterxml.jackson.core.type.TypeReference<>() {};
com.fasterxml.jackson.core.type.TypeReference<java.util.List<java.util.Map<String, String>>> typeRef = new com.fasterxml.jackson.core.type.TypeReference<>() {
};
servicesData = mapper.readValue(servicesJson, typeRef);
} catch (Exception e) {
System.err.println("DEBUG: Failed to parse services JSON: " + e.getMessage());
@@ -760,16 +774,20 @@ public class CustomerInvoiceService {
// Header row
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("<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(
"<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: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>");
// Data rows - use actual service data from the job
if (servicesData.isEmpty()) {
// Fallback: show a single row with no data
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>");
} else {
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);" : "";
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("<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(
"<td style='text-align:left;padding:4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'>")
.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>");
}
}
@@ -800,21 +822,27 @@ public class CustomerInvoiceService {
html.append("<tr>");
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: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>");
// Umsatzsteuer
html.append("<tr>");
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:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>").append(vatTotal).append("</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:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>")
.append(vatTotal).append("</td>");
html.append("</tr>");
// Gesamtsumme
html.append("<tr>");
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("<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(
"<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("</table>");
@@ -831,10 +859,7 @@ public class CustomerInvoiceService {
if (input == null) {
return "";
}
return input.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#x27;");
return input.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
.replace("'", "&#x27;");
}
}

View File

@@ -33,20 +33,20 @@ public class LanguageService {
try {
Locale locale;
switch (language) {
case DE:
locale = Locale.GERMAN;
break;
case EN:
locale = Locale.ENGLISH;
break;
case FR:
locale = Locale.FRENCH;
break;
case DE:
locale = Locale.GERMAN;
break;
case EN:
locale = Locale.ENGLISH;
break;
case FR:
locale = Locale.FRENCH;
break;
case ES:
locale = Locale.of("es", "ES");
break;
default:
locale = Locale.GERMAN;
default:
locale = Locale.GERMAN;
}
ResourceBundle bundle = ResourceBundle.getBundle("messages", locale);
@@ -74,11 +74,11 @@ public class LanguageService {
private Locale getLocaleFromLanguage(Language language) {
return switch (language) {
case DE -> Locale.GERMAN;
case EN -> Locale.ENGLISH;
case FR -> Locale.FRENCH;
case ES -> Locale.of("es", "ES");
default -> Locale.GERMAN;
case DE -> Locale.GERMAN;
case EN -> Locale.ENGLISH;
case FR -> Locale.FRENCH;
case ES -> Locale.of("es", "ES");
default -> Locale.GERMAN;
};
}