diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/CustomerAddressLabelHelper.java b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/CustomerAddressLabelHelper.java new file mode 100644 index 0000000..741de42 --- /dev/null +++ b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/CustomerAddressLabelHelper.java @@ -0,0 +1,56 @@ +package de.assecutor.votianlt.pages.base.ui.component; + +import de.assecutor.votianlt.model.Customer; +import java.util.Map; + +public final class CustomerAddressLabelHelper { + + private CustomerAddressLabelHelper() { + } + + public static void putUnique(Map target, Customer customer, String fallbackLabel) { + String label = build(customer, fallbackLabel); + String uniqueLabel = label; + int counter = 2; + while (target.containsKey(uniqueLabel)) { + uniqueLabel = label + " (" + counter++ + ")"; + } + target.put(uniqueLabel, customer); + } + + public static String build(Customer customer, String fallbackLabel) { + if (customer == null) { + return fallbackLabel; + } + + String companyName = trimToNull(customer.getCompanyName()); + if (companyName != null) { + return companyName; + } + + String fullName = trimToNull(join(" ", customer.getFirstname(), customer.getLastName())); + return fullName != null ? fullName : fallbackLabel; + } + + public static String resolveCompanyValue(Map addressOptions, String comboValue) { + if (addressOptions.containsKey(comboValue)) { + Customer customer = addressOptions.get(comboValue); + return customer != null ? customer.getCompanyName() : null; + } + return comboValue; + } + + private static String join(String separator, String first, String second) { + String left = first != null ? first.trim() : ""; + String right = second != null ? second.trim() : ""; + return (left + separator + right).trim(); + } + + private static String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } +} diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java index bf7b703..40e66e0 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java @@ -254,9 +254,9 @@ public class DeliveryStationDialog extends Dialog { formLayout.setSpacing(true); formLayout.setWidthFull(); - // Company with autocomplete - company = new ComboBox<>(translationHelper.getTranslation("profile.company")); - company.setPlaceholder(translationHelper.getTranslation("addjob.address.company.placeholder")); + // Delivery address with autocomplete + company = new ComboBox<>(translationHelper.getTranslation("addjob.address.delivery.label")); + company.setPlaceholder(translationHelper.getTranslation("addjob.address.delivery.placeholder")); company.setAllowCustomValue(true); company.setWidthFull(); setupCompanyAutocomplete(company, customers); @@ -390,7 +390,7 @@ public class DeliveryStationDialog extends Dialog { addressTabError = createTabErrorIndicator(); tasksTabError = createTabErrorIndicator(); - Tab addressTab = tabSheet.add(translationHelper.getTranslation("addjob.tab.addresses"), formLayout); + Tab addressTab = tabSheet.add(translationHelper.getTranslation("addjob.tab.delivery.address"), formLayout); addressTab.add(addressTabError); Tab tasksTab = tabSheet.add(translationHelper.getTranslation("addjob.tab.tasks"), createTasksTab(templates, templateSaveCallback)); @@ -687,17 +687,8 @@ public class DeliveryStationDialog extends Dialog { private void setupCompanyAutocomplete(ComboBox companyField, List customers) { companyAddressOptions.clear(); for (Customer customer : customers) { - String label = buildCompanyAddressLabel(customer); - if (label == null) { - continue; - } - - String uniqueLabel = label; - int counter = 2; - while (companyAddressOptions.containsKey(uniqueLabel)) { - uniqueLabel = label + " (" + counter++ + ")"; - } - companyAddressOptions.put(uniqueLabel, customer); + CustomerAddressLabelHelper.putUnique(companyAddressOptions, customer, + translationHelper.getTranslation("addjob.customer.unnamed")); } companyField.setItems(new ArrayList<>(companyAddressOptions.keySet())); @@ -769,51 +760,8 @@ public class DeliveryStationDialog extends Dialog { mail.setRequiredIndicatorVisible(false); } - private String buildCompanyAddressLabel(Customer customer) { - if (customer == null) { - return null; - } - - List leftParts = new ArrayList<>(); - if (customer.getCompanyName() != null && !customer.getCompanyName().isBlank()) { - leftParts.add(customer.getCompanyName().trim()); - } - - String fullName = ((customer.getFirstname() != null ? customer.getFirstname() : "") + " " - + (customer.getLastName() != null ? customer.getLastName() : "")).trim(); - if (!fullName.isBlank()) { - leftParts.add(fullName); - } - - List rightParts = new ArrayList<>(); - String streetLine = ((customer.getStreet() != null ? customer.getStreet() : "") + " " - + (customer.getHouseNumber() != null ? customer.getHouseNumber() : "")).trim(); - if (!streetLine.isBlank()) { - rightParts.add(streetLine); - } - - String cityLine = ((customer.getZip() != null ? customer.getZip() : "") + " " - + (customer.getCity() != null ? customer.getCity() : "")).trim(); - if (!cityLine.isBlank()) { - rightParts.add(cityLine); - } - - String left = String.join(" | ", leftParts); - String right = String.join(", ", rightParts); - String label = left; - if (!right.isBlank()) { - label = label.isBlank() ? right : left + " | " + right; - } - - return label.isBlank() ? null : label; - } - private String resolveCompanyValue(String comboValue) { - Customer customer = companyAddressOptions.get(comboValue); - if (customer != null && customer.getCompanyName() != null && !customer.getCompanyName().isBlank()) { - return customer.getCompanyName(); - } - return comboValue; + return CustomerAddressLabelHelper.resolveCompanyValue(companyAddressOptions, comboValue); } private String findCompanyOptionLabel(DeliveryData data) { diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationTile.java b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationTile.java index cd147a1..52fe7d3 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationTile.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationTile.java @@ -16,8 +16,10 @@ import com.vaadin.flow.component.textfield.TextField; import de.assecutor.votianlt.model.Customer; import de.assecutor.votianlt.model.DeliveryStation; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; -import java.util.Optional; +import java.util.Map; /** * A self-contained tile for one delivery station in the AddJob form. Contains @@ -51,6 +53,7 @@ public class DeliveryStationTile extends VerticalLayout { private final TextField city; private final Checkbox saveAddress; private final H3 title; + private final Map companyAddressOptions = new LinkedHashMap<>(); private ChangeListener changeListener; private DeleteListener deleteListener; @@ -100,9 +103,9 @@ public class DeliveryStationTile extends VerticalLayout { add(titleLayout); - // Company with autocomplete - company = new ComboBox<>(translationHelper.getTranslation("profile.company")); - company.setPlaceholder(translationHelper.getTranslation("addjob.address.company.placeholder")); + // Delivery address with autocomplete + company = new ComboBox<>(translationHelper.getTranslation("addjob.address.delivery.label")); + company.setPlaceholder(translationHelper.getTranslation("addjob.address.delivery.placeholder")); company.setAllowCustomValue(true); company.setWidthFull(); setupCompanyAutocomplete(company, customers, translationHelper); @@ -224,22 +227,22 @@ public class DeliveryStationTile extends VerticalLayout { private void setupCompanyAutocomplete(ComboBox companyField, List customers, TranslationHelper translationHelper) { - List companyNames = customers.stream().map(Customer::getCompanyName) - .filter(name -> name != null && !name.trim().isEmpty()).distinct().sorted().toList(); + companyAddressOptions.clear(); + for (Customer customer : customers) { + CustomerAddressLabelHelper.putUnique(companyAddressOptions, customer, + translationHelper.getTranslation("addjob.customer.unnamed")); + } - companyField.setItems(companyNames); + companyField.setItems(new ArrayList<>(companyAddressOptions.keySet())); companyField.addValueChangeListener(event -> { - String selectedCompany = event.getValue(); - if (selectedCompany == null || selectedCompany.trim().isEmpty()) { + String selectedAddress = event.getValue(); + if (selectedAddress == null || selectedAddress.trim().isEmpty()) { return; } - Optional matchingCustomer = customers.stream() - .filter(c -> selectedCompany.equals(c.getCompanyName())).findFirst(); - - if (matchingCustomer.isPresent()) { - Customer customer = matchingCustomer.get(); + Customer customer = companyAddressOptions.get(selectedAddress); + if (customer != null) { if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle()) || "Frau".equalsIgnoreCase(customer.getTitle()) || "Divers".equalsIgnoreCase(customer.getTitle()))) { @@ -282,7 +285,7 @@ public class DeliveryStationTile extends VerticalLayout { */ public DeliveryStation getDeliveryStation() { DeliveryStation station = new DeliveryStation(); - station.setCompany(company.getValue()); + station.setCompany(CustomerAddressLabelHelper.resolveCompanyValue(companyAddressOptions, company.getValue())); station.setSalutation(salutation.getValue()); station.setFirstName(firstName.getValue()); station.setLastName(lastName.getValue()); diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java index ef41bd3..e15565c 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java @@ -35,7 +35,6 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.CompletableFuture; /** @@ -303,7 +302,7 @@ public class PickupStationDialog extends Dialog { formLayout.setSpacing(true); formLayout.setWidthFull(); - // Customer selection + // Principal selection customerComboBox = new ComboBox<>(translationHelper.getTranslation("addjob.customer.label")); customerComboBox.setPlaceholder(translationHelper.getTranslation("addjob.customer.placeholder")); customerComboBox.setRequiredIndicatorVisible(true); @@ -311,27 +310,14 @@ public class PickupStationDialog extends Dialog { customerLabelMap.clear(); for (Customer c : customers) { - String label = (c.getCompanyName() != null && !c.getCompanyName().isBlank()) - ? c.getCompanyName() + " | " - + ((c.getFirstname() != null ? c.getFirstname() : "") + " " - + (c.getLastName() != null ? c.getLastName() : "")).trim() - : ((c.getFirstname() != null ? c.getFirstname() : "") + " " - + (c.getLastName() != null ? c.getLastName() : "")).trim(); - if (label.isBlank()) { - label = translationHelper.getTranslation("addjob.customer.unnamed"); - } - String uniqueLabel = label; - int counter = 2; - while (customerLabelMap.containsKey(uniqueLabel)) { - uniqueLabel = label + " (" + counter++ + ")"; - } - customerLabelMap.put(uniqueLabel, c); + CustomerAddressLabelHelper.putUnique(customerLabelMap, c, + translationHelper.getTranslation("addjob.customer.unnamed")); } customerComboBox.setItems(new ArrayList<>(customerLabelMap.keySet())); - // Company with autocomplete - company = new ComboBox<>(translationHelper.getTranslation("profile.company")); - company.setPlaceholder(translationHelper.getTranslation("addjob.address.company.placeholder")); + // Pickup address with autocomplete + company = new ComboBox<>(translationHelper.getTranslation("addjob.address.pickup.label")); + company.setPlaceholder(translationHelper.getTranslation("addjob.address.pickup.placeholder")); company.setAllowCustomValue(true); company.setWidthFull(); setupCompanyAutocomplete(company, customers); @@ -462,10 +448,7 @@ public class PickupStationDialog extends Dialog { return; } selectedCustomerId = c.getId(); - if (c.getCompanyName() != null) - company.setValue(c.getCompanyName()); - else - company.clear(); + setCompanySelection(c); if (c.getTitle() != null && ("Herr".equalsIgnoreCase(c.getTitle()) || "Frau".equalsIgnoreCase(c.getTitle()) || "Divers".equalsIgnoreCase(c.getTitle()))) salutation.setValue(c.getTitle()); @@ -511,7 +494,12 @@ public class PickupStationDialog extends Dialog { updateSaveAddressState(); }); - formLayout.add(customerComboBox, company, salutation, firstName, lastName, phone, mail, streetLayout, + Div addressDivider = new Div(); + addressDivider.setWidthFull(); + addressDivider.getStyle().set("border-top", "1px solid var(--lumo-contrast-20pct)"); + addressDivider.getStyle().set("margin", "var(--lumo-space-m) 0 var(--lumo-space-s)"); + + formLayout.add(customerComboBox, addressDivider, company, salutation, firstName, lastName, phone, mail, streetLayout, addressAddition, zipCityLayout, saveAddress); // TabSheet with address, appointments, and cargo tabs @@ -523,7 +511,7 @@ public class PickupStationDialog extends Dialog { appointmentsTabError = createTabErrorIndicator(); cargoTabError = createTabErrorIndicator(); - Tab addressTab = tabSheet.add(translationHelper.getTranslation("addjob.tab.addresses"), formLayout); + Tab addressTab = tabSheet.add(translationHelper.getTranslation("addjob.tab.pickup.address"), formLayout); addressTab.add(addressTabError); Tab appointmentsTab = tabSheet.add(translationHelper.getTranslation("addjob.tab.appointments"), createAppointmentsTab(availableAppUsers)); @@ -637,13 +625,23 @@ public class PickupStationDialog extends Dialog { public void setData(PickupData data) { if (data == null) return; - if (data.getCustomerSelection() != null) { - customerComboBox.setValue(data.getCustomerSelection()); + String customerSelection = normalizeValue(data.getCustomerSelection()); + if (!customerSelection.isEmpty()) { + if (!customerLabelMap.containsKey(customerSelection)) { + customerLabelMap.put(customerSelection, null); + customerComboBox.setItems(new ArrayList<>(customerLabelMap.keySet())); + } + customerComboBox.setValue(customerSelection); } else { customerComboBox.clear(); } - if (data.getCompany() != null) + String companyOption = findCompanyOptionLabel(data); + boolean customerSelectedFromOptions = companyOption != null; + if (companyOption != null) { + company.setValue(companyOption); + } else if (data.getCompany() != null) { company.setValue(data.getCompany()); + } if (data.getSalutation() != null) salutation.setValue(data.getSalutation()); if (data.getFirstName() != null) @@ -689,19 +687,16 @@ public class PickupStationDialog extends Dialog { if (data.getCustomerId() != null) { selectedCustomerId = data.getCustomerId(); } else { - Customer matched = customerLabelMap.get(data.getCustomerSelection()); - if (matched == null) { - matched = companyCustomerMap.get(normalizeValue(data.getCompany())); - } + Customer matched = companyOption != null ? companyCustomerMap.get(companyOption) : null; selectedCustomerId = matched != null ? matched.getId() : null; } - saveAddress.setValue(data.isSaveAddress()); + saveAddress.setValue(customerSelectedFromOptions ? false : data.isSaveAddress()); updateSaveAddressState(); } private PickupData collectData() { PickupData data = new PickupData(); - data.setCompany(company.getValue()); + data.setCompany(resolveCompanyValue(company.getValue())); data.setSalutation(salutation.getValue()); data.setFirstName(firstName.getValue()); data.setLastName(lastName.getValue()); @@ -781,12 +776,9 @@ public class PickupStationDialog extends Dialog { private boolean validateMailField() { String value = mail.getValue(); String normalizedValue = value == null ? "" : value.trim(); - boolean empty = normalizedValue.isEmpty(); - boolean required = Boolean.TRUE.equals(saveAddress.getValue()); - boolean invalid = !empty && !normalizedValue.contains("@"); - boolean hasError = invalid || (required && empty); - applyErrorStyling(mail, hasError); - return !hasError; + boolean invalid = !normalizedValue.isEmpty() && !normalizedValue.contains("@"); + applyErrorStyling(mail, invalid); + return !invalid; } private boolean validateCargoItems() { @@ -838,56 +830,24 @@ public class PickupStationDialog extends Dialog { } private void setupCompanyAutocomplete(ComboBox companyField, List customers) { - List companyNames = customers.stream().map(Customer::getCompanyName) - .filter(name -> name != null && !name.trim().isEmpty()).distinct().sorted().toList(); - companyCustomerMap.clear(); for (Customer customer : customers) { - String companyName = normalizeValue(customer.getCompanyName()); - if (companyName.isEmpty() || companyCustomerMap.containsKey(companyName)) { - continue; - } - companyCustomerMap.put(companyName, customer); + CustomerAddressLabelHelper.putUnique(companyCustomerMap, customer, + translationHelper.getTranslation("addjob.customer.unnamed")); } - companyField.setItems(companyNames); + companyField.setItems(new ArrayList<>(companyCustomerMap.keySet())); companyField.addValueChangeListener(event -> { - String selectedCompany = event.getValue(); - if (selectedCompany == null || selectedCompany.trim().isEmpty()) { + String selectedAddress = event.getValue(); + if (selectedAddress == null || selectedAddress.trim().isEmpty()) { selectedCustomerId = null; updateSaveAddressState(); return; } - Optional matchingCustomer = customers.stream() - .filter(c -> sameValue(selectedCompany, c.getCompanyName())).findFirst(); - - if (matchingCustomer.isPresent()) { - Customer customer = matchingCustomer.get(); - selectedCustomerId = customer.getId(); - if (customer.getTitle() != null - && ("Herr".equalsIgnoreCase(customer.getTitle()) || "Frau".equalsIgnoreCase(customer.getTitle()) - || "Divers".equalsIgnoreCase(customer.getTitle()))) { - salutation.setValue(customer.getTitle()); - } - if (customer.getFirstname() != null) - firstName.setValue(customer.getFirstname()); - if (customer.getLastName() != null) - lastName.setValue(customer.getLastName()); - if (customer.getTelephone() != null) - phone.setValue(customer.getTelephone()); - if (customer.getMail() != null) - mail.setValue(customer.getMail()); - if (customer.getStreet() != null) - street.setValue(customer.getStreet()); - if (customer.getHouseNumber() != null) - houseNumber.setValue(customer.getHouseNumber()); - if (customer.getAddressAddition() != null) - addressAddition.setValue(customer.getAddressAddition()); - if (customer.getZip() != null) - zip.setValue(customer.getZip()); - if (customer.getCity() != null) - city.setValue(customer.getCity()); + Customer customer = companyCustomerMap.get(selectedAddress); + if (customer != null) { + applyCustomerAddress(customer); } updateSaveAddressState(); @@ -902,7 +862,7 @@ public class PickupStationDialog extends Dialog { private void updateSaveAddressState() { Customer selectedCustomer = customerLabelMap.get(customerComboBox.getValue()); - Customer selectedCompanyCustomer = companyCustomerMap.get(normalizeValue(company.getValue())); + Customer selectedCompanyCustomer = companyCustomerMap.get(company.getValue()); boolean customerDataMatches = selectedCustomer != null && matchesCustomer(selectedCustomer); boolean companyDataMatches = selectedCompanyCustomer != null && matchesCustomer(selectedCompanyCustomer); @@ -924,11 +884,11 @@ public class PickupStationDialog extends Dialog { } private void updateMailRequirement() { - mail.setRequiredIndicatorVisible(Boolean.TRUE.equals(saveAddress.getValue())); + mail.setRequiredIndicatorVisible(false); } private boolean matchesCustomer(Customer customer) { - return sameValue(company.getValue(), customer.getCompanyName()) + return sameValue(resolveCompanyValue(company.getValue()), customer.getCompanyName()) && sameValue(salutation.getValue(), customer.getTitle()) && sameValue(firstName.getValue(), customer.getFirstname()) && sameValue(lastName.getValue(), customer.getLastName()) @@ -950,7 +910,7 @@ public class PickupStationDialog extends Dialog { } private boolean computeAddressDiffers() { - boolean hasAnyValue = !normalizeValue(company.getValue()).isEmpty() + boolean hasAnyValue = !normalizeValue(resolveCompanyValue(company.getValue())).isEmpty() || !normalizeValue(firstName.getValue()).isEmpty() || !normalizeValue(lastName.getValue()).isEmpty() || !normalizeValue(phone.getValue()).isEmpty() || !normalizeValue(mail.getValue()).isEmpty() || !normalizeValue(street.getValue()).isEmpty() || !normalizeValue(houseNumber.getValue()).isEmpty() @@ -983,6 +943,108 @@ public class PickupStationDialog extends Dialog { return null; } + private void applyCustomerAddress(Customer customer) { + selectedCustomerId = customer.getId(); + if (customer.getTitle() != null + && ("Herr".equalsIgnoreCase(customer.getTitle()) || "Frau".equalsIgnoreCase(customer.getTitle()) + || "Divers".equalsIgnoreCase(customer.getTitle()))) { + salutation.setValue(customer.getTitle()); + } else { + salutation.clear(); + } + if (customer.getFirstname() != null) + firstName.setValue(customer.getFirstname()); + else + firstName.clear(); + if (customer.getLastName() != null) + lastName.setValue(customer.getLastName()); + else + lastName.clear(); + if (customer.getTelephone() != null) + phone.setValue(customer.getTelephone()); + else + phone.clear(); + if (customer.getMail() != null) + mail.setValue(customer.getMail()); + else + mail.clear(); + if (customer.getStreet() != null) + street.setValue(customer.getStreet()); + else + street.clear(); + if (customer.getHouseNumber() != null) + houseNumber.setValue(customer.getHouseNumber()); + else + houseNumber.clear(); + if (customer.getAddressAddition() != null) + addressAddition.setValue(customer.getAddressAddition()); + else + addressAddition.clear(); + if (customer.getZip() != null) + zip.setValue(customer.getZip()); + else + zip.clear(); + if (customer.getCity() != null) + city.setValue(customer.getCity()); + else + city.clear(); + } + + private void setCompanySelection(Customer customer) { + String label = findCompanyOptionLabel(customer); + if (label != null) { + company.setValue(label); + } else if (customer.getCompanyName() != null && !customer.getCompanyName().isBlank()) { + company.setValue(customer.getCompanyName()); + } else { + company.clear(); + } + } + + private String resolveCompanyValue(String comboValue) { + return CustomerAddressLabelHelper.resolveCompanyValue(companyCustomerMap, comboValue); + } + + private String findCompanyOptionLabel(Customer customer) { + if (customer == null || customer.getId() == null) { + return null; + } + for (Map.Entry entry : companyCustomerMap.entrySet()) { + Customer option = entry.getValue(); + if (option != null && customer.getId().equals(option.getId())) { + return entry.getKey(); + } + } + return null; + } + + private String findCompanyOptionLabel(PickupData data) { + for (Map.Entry entry : companyCustomerMap.entrySet()) { + Customer customer = entry.getValue(); + if (data.getCustomerId() != null && customer.getId() != null && data.getCustomerId().equals(customer.getId())) { + return entry.getKey(); + } + if (matchesCustomer(customer, data)) { + return entry.getKey(); + } + } + return null; + } + + private boolean matchesCustomer(Customer customer, PickupData data) { + return sameValue(customer.getCompanyName(), data.getCompany()) + && sameValue(customer.getTitle(), data.getSalutation()) + && sameValue(customer.getFirstname(), data.getFirstName()) + && sameValue(customer.getLastName(), data.getLastName()) + && sameValue(customer.getTelephone(), data.getPhone()) + && sameValue(customer.getMail(), data.getMail()) + && sameValue(customer.getStreet(), data.getStreet()) + && sameValue(customer.getHouseNumber(), data.getHouseNumber()) + && sameValue(customer.getAddressAddition(), data.getAddressAddition()) + && sameValue(customer.getZip(), data.getZip()) + && sameValue(customer.getCity(), data.getCity()); + } + // ============================================ // Appointments & Processing Tab // ============================================ diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/domain/CustomerRepository.java b/backend/src/main/java/de/assecutor/votianlt/pages/domain/CustomerRepository.java index 6615015..3d20e0c 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/domain/CustomerRepository.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/domain/CustomerRepository.java @@ -5,6 +5,7 @@ import org.bson.types.ObjectId; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; import java.util.List; public interface CustomerRepository extends MongoRepository { @@ -14,5 +15,8 @@ public interface CustomerRepository extends MongoRepository List findByOwner(ObjectId owner); + // $ne: true matches documents where internal is false, null, or the field is missing + // (legacy data without the internal field still shows up in customer dropdowns). + @Query("{ 'owner' : ?0, 'internal' : { $ne: true } }") List findByOwnerAndInternalFalse(ObjectId owner); } diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/service/CustomerService.java b/backend/src/main/java/de/assecutor/votianlt/pages/service/CustomerService.java index bf1ac5b..7af9c33 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/service/CustomerService.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/service/CustomerService.java @@ -43,4 +43,8 @@ public class CustomerService { return todoRepository.findById(id).orElse(null); } + public void deleteById(ObjectId id) { + todoRepository.deleteById(id); + } + } diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java index 8ebd2be..570e27c 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java @@ -46,11 +46,9 @@ public class AddCustomerView extends Main implements HasDynamicTitle { public AddCustomerView(AddCustomerService todoService, Clock clock) { this.addCustomerService = todoService; - // Firma (Pflichtfeld) + // Firma (optional; auch Privatpersonen können im Adressbuch stehen) companyName = new TextField(getTranslation("profile.company")); - companyName.setRequiredIndicatorVisible(true); companyName.setWidthFull(); - companyName.addBlurListener(e -> validateField(companyName)); // Anrede (Dropdown) title = new ComboBox<>(getTranslation("addjob.address.salutation")); @@ -162,8 +160,7 @@ 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).bind(Customer::getCompanyName, Customer::setCompanyName); binder.forField(title).bind(Customer::getTitle, Customer::setTitle); @@ -257,7 +254,6 @@ public class AddCustomerView extends Main implements HasDynamicTitle { } private boolean validateAllFields() { - validateField(companyName); validateField(firstName); validateField(lastName); validateField(telephone); @@ -267,9 +263,8 @@ public class AddCustomerView extends Main implements HasDynamicTitle { validateField(city); validateEmail(); - return !companyName.isInvalid() && !firstName.isInvalid() && !lastName.isInvalid() && !telephone.isInvalid() - && !mail.isInvalid() && !street.isInvalid() && !houseNumber.isInvalid() && !zip.isInvalid() - && !city.isInvalid(); + return !firstName.isInvalid() && !lastName.isInvalid() && !telephone.isInvalid() && !mail.isInvalid() + && !street.isInvalid() && !houseNumber.isInvalid() && !zip.isInvalid() && !city.isInvalid(); } @Override diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java index 64cec4d..b6fc0e2 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -63,6 +63,7 @@ import de.assecutor.votianlt.model.AddressValidationResult; import de.assecutor.votianlt.model.RouteCalculationResult; import de.assecutor.votianlt.pages.base.ui.component.DeliveryStationTile; import de.assecutor.votianlt.pages.base.ui.component.StationTile; +import de.assecutor.votianlt.pages.base.ui.component.CustomerAddressLabelHelper; import de.assecutor.votianlt.pages.base.ui.component.PickupStationDialog; import de.assecutor.votianlt.pages.base.ui.component.DeliveryStationDialog; import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper; @@ -236,32 +237,19 @@ public class AddJobView extends Main implements HasDynamicTitle { customerSelection.setPlaceholder(getTranslation("addjob.customer.placeholder")); customerSelection.setWidthFull(); customerSelection.setRequiredIndicatorVisible(true); + customerSelection.setAllowCustomValue(true); + customerSelection.addCustomValueSetListener(event -> setCustomerSelectionValue(event.getDetail())); // Mit Kunden des angemeldeten Benutzers befüllen und Mapping aufbauen List ownerCustomers = customerService.findAllForCurrentOwner(); customerLabelToEntity.clear(); for (Customer c : ownerCustomers) { - String label = (c.getCompanyName() != null && !c.getCompanyName().isBlank()) - ? c.getCompanyName() + " | " - + ((c.getFirstname() != null ? c.getFirstname() : "") + " " - + (c.getLastName() != null ? c.getLastName() : "")).trim() - : ((c.getFirstname() != null ? c.getFirstname() : "") + " " - + (c.getLastName() != null ? c.getLastName() : "")).trim(); - if (label.isBlank()) { - label = getTranslation("addjob.customer.unnamed"); - } - // Bei Duplikaten Label einzigartig machen - String uniqueLabel = label; - int counter = 2; - while (customerLabelToEntity.containsKey(uniqueLabel)) { - uniqueLabel = label + " (" + counter++ + ")"; - } - customerLabelToEntity.put(uniqueLabel, c); + CustomerAddressLabelHelper.putUnique(customerLabelToEntity, c, getTranslation("addjob.customer.unnamed")); } customerSelection.setItems(new ArrayList<>(customerLabelToEntity.keySet())); // Pickup address - pickupCompany = new ComboBox<>(getTranslation("profile.company")); - pickupCompany.setPlaceholder(getTranslation("addjob.address.company.placeholder")); + pickupCompany = new ComboBox<>(getTranslation("addjob.address.pickup.label")); + pickupCompany.setPlaceholder(getTranslation("addjob.address.pickup.placeholder")); pickupCompany.setAllowCustomValue(true); setupCompanyAutocomplete(pickupCompany, true); // true für Pickup pickupSalutation = new ComboBox<>(getTranslation("addjob.address.salutation")); @@ -857,7 +845,7 @@ public class AddJobView extends Main implements HasDynamicTitle { translationHelper, data -> { // Update customer selection from dialog if (data.getCustomerSelection() != null) { - customerSelection.setValue(data.getCustomerSelection()); + setCustomerSelectionValue(data.getCustomerSelection()); } else { customerSelection.clear(); } @@ -1116,6 +1104,19 @@ public class AddJobView extends Main implements HasDynamicTitle { return trimmed.isEmpty() ? null : trimmed; } + private void setCustomerSelectionValue(String value) { + String normalizedValue = trimToNull(value); + if (normalizedValue == null) { + customerSelection.clear(); + return; + } + if (!customerLabelToEntity.containsKey(normalizedValue)) { + customerLabelToEntity.put(normalizedValue, null); + customerSelection.setItems(new ArrayList<>(customerLabelToEntity.keySet())); + } + customerSelection.setValue(normalizedValue); + } + private void openDeliveryDialog(StationTile tile, int stationIndex) { // Ensure index is valid (could have changed due to deletions) int actualIndex = deliveryStationTilesList.indexOf(tile); @@ -1412,30 +1413,29 @@ public class AddJobView extends Main implements HasDynamicTitle { // Get all customers for the current owner List allCustomers = customerService.findAllForCurrentOwner(); - // Extract unique company names (filter out null/empty values) - List companyNames = allCustomers.stream().map(Customer::getCompanyName) - .filter(name -> name != null && !name.trim().isEmpty()).distinct().sorted().toList(); + Map addressOptions = new LinkedHashMap<>(); + for (Customer customer : allCustomers) { + CustomerAddressLabelHelper.putUnique(addressOptions, customer, getTranslation("addjob.customer.unnamed")); + } // Set items for autocomplete - companyField.setItems(companyNames); + companyField.setItems(new ArrayList<>(addressOptions.keySet())); // Add selection listener to auto-fill pickup address fields when company is // selected companyField.addValueChangeListener(event -> { - String selectedCompany = event.getValue(); - if (selectedCompany == null || selectedCompany.trim().isEmpty()) { + String selectedAddress = event.getValue(); + if (selectedAddress == null || selectedAddress.trim().isEmpty()) { return; } // Streckeninformationen zurücksetzen, da sich die Adresse ändert resetRouteInformation(); - // Find the first customer with this company name - Optional matchingCustomer = allCustomers.stream() - .filter(c -> selectedCompany.equals(c.getCompanyName())).findFirst(); + Customer customer = addressOptions.get(selectedAddress); - if (matchingCustomer.isPresent()) { - Customer customer = matchingCustomer.get(); + if (customer != null) { + pickupCustomerId = customer.getId(); // Fill pickup address fields if (customer.getTitle() != null @@ -1476,6 +1476,7 @@ public class AddJobView extends Main implements HasDynamicTitle { // Reactivate save checkbox for custom values savePickupAddress.setValue(true); + pickupCustomerId = null; pickupMail = null; }); } @@ -1987,7 +1988,7 @@ public class AddJobView extends Main implements HasDynamicTitle { */ private void loadJobIntoForm(Job job) { if (job.getCustomerSelection() != null) { - customerSelection.setValue(job.getCustomerSelection()); + setCustomerSelectionValue(job.getCustomerSelection()); } } diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java index 5728475..f07f0c0 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/EditCustomerView.java @@ -191,6 +191,7 @@ public class EditCustomerView extends VerticalLayout implements HasUrlParameter< Button confirmDeleteButton = new Button(getTranslation("editcustomer.dialog.delete.confirm"), e -> { if (customer != null && customer.getId() != null) { + customerService.deleteById(customer.getId()); Notification.show(getTranslation("editcustomer.notification.deleted"), 3000, Notification.Position.MIDDLE); confirmDialog.close(); diff --git a/backend/src/main/java/de/assecutor/votianlt/pages/view/JobManualCompleteView.java b/backend/src/main/java/de/assecutor/votianlt/pages/view/JobManualCompleteView.java index 7049e4b..84efd32 100644 --- a/backend/src/main/java/de/assecutor/votianlt/pages/view/JobManualCompleteView.java +++ b/backend/src/main/java/de/assecutor/votianlt/pages/view/JobManualCompleteView.java @@ -2,22 +2,38 @@ package de.assecutor.votianlt.pages.view; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.html.Main; import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.component.textfield.NumberField; import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.router.BeforeEvent; import com.vaadin.flow.router.HasDynamicTitle; import com.vaadin.flow.router.HasUrlParameter; import com.vaadin.flow.router.Route; import com.vaadin.flow.theme.lumo.LumoUtility; +import de.assecutor.votianlt.model.DeliveryStation; import de.assecutor.votianlt.model.Job; import de.assecutor.votianlt.model.JobHistoryType; +import de.assecutor.votianlt.model.JobServiceSelection; import de.assecutor.votianlt.model.JobStatus; +import de.assecutor.votianlt.model.Service; +import de.assecutor.votianlt.model.User; +import de.assecutor.votianlt.pages.base.ui.component.DialogStylingHelper; import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; +import de.assecutor.votianlt.repository.ServiceRepository; import de.assecutor.votianlt.repository.JobRepository; import de.assecutor.votianlt.security.SecurityService; import de.assecutor.votianlt.service.JobHistoryService; @@ -25,23 +41,50 @@ import jakarta.annotation.security.RolesAllowed; import lombok.extern.slf4j.Slf4j; import org.bson.types.ObjectId; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; @Route(value = "job_manual_complete", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @RolesAllowed("USER") @Slf4j public class JobManualCompleteView extends Main implements HasUrlParameter, HasDynamicTitle { + private static final class ServiceRow { + private final Service service; + private final JobServiceSelection selection; + + private ServiceRow(Service service, JobServiceSelection selection) { + this.service = service; + this.selection = selection; + } + } + private final JobRepository jobRepository; private final JobHistoryService jobHistoryService; private final SecurityService securityService; + private final ServiceRepository serviceRepository; private final VerticalLayout content; + private Job job; + private final List serviceRows = new ArrayList<>(); + private Grid servicesGrid; + private Span netTotalLabel; + private Span grossTotalLabel; + private TextArea remarkArea; + private BigDecimal vatRate = Service.FIXED_VAT_RATE; + private Double manualDistanceKm; + private Integer manualDurationSeconds; + public JobManualCompleteView(JobRepository jobRepository, JobHistoryService jobHistoryService, - SecurityService securityService) { + SecurityService securityService, ServiceRepository serviceRepository) { this.jobRepository = jobRepository; this.jobHistoryService = jobHistoryService; this.securityService = securityService; + this.serviceRepository = serviceRepository; setSizeFull(); addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, @@ -66,6 +109,8 @@ public class JobManualCompleteView extends Main implements HasUrlParameter 0; + content.add(hasRouteData ? createRouteSection() : createManualRouteSection()); + + content.add(createServicesSection()); + content.add(createSummarySection()); + content.add(createRemarkSection()); + HorizontalLayout buttonBar = new HorizontalLayout(); buttonBar.setWidthFull(); - buttonBar.setJustifyContentMode(HorizontalLayout.JustifyContentMode.END); + buttonBar.setJustifyContentMode(FlexComponent.JustifyContentMode.END); buttonBar.setSpacing(true); Button cancelButton = new Button(getTranslation("jobsummary.dialog.manualcomplete.cancel"), @@ -110,43 +178,521 @@ public class JobManualCompleteView extends Main implements HasUrlParameter { - String reason = reasonField.getValue(); - if (reason == null || reason.trim().isEmpty()) { - reasonField.setInvalid(true); - reasonField.setErrorMessage(getTranslation("jobsummary.dialog.manualcomplete.reason.required")); - return; - } - - try { - JobStatus oldStatus = job.getStatus(); - job.setStatus(JobStatus.COMPLETED); - job.setUpdatedAt(LocalDateTime.now()); - jobRepository.save(job); - - String currentUser = securityService.getCurrentUsername(); - jobHistoryService.logStatusChange(job, oldStatus, JobStatus.COMPLETED, currentUser); - - String description = String.format("Auftrag manuell beendet von %s. Begründung: %s", - currentUser, reason.trim()); - jobHistoryService.logCustomEvent(job.getId(), - getTranslation("jobsummary.history.manualcomplete.reason"), - description, currentUser, JobHistoryType.STATUS_CHANGE); - - Notification - .show(getTranslation("jobsummary.notification.completed", job.getJobNumber()), 3000, - Notification.Position.BOTTOM_END) - .addThemeVariants(NotificationVariant.LUMO_SUCCESS); - getUI().ifPresent(ui -> ui.navigate("job_summary/" + job.getId().toHexString())); - } catch (Exception ex) { - Notification - .show(getTranslation("jobsummary.notification.complete.error", ex.getMessage()), 5000, - Notification.Position.BOTTOM_END) - .addThemeVariants(NotificationVariant.LUMO_ERROR); - } - }); + confirmButton.addClickListener(e -> confirm(reasonField)); buttonBar.add(cancelButton, confirmButton); content.add(buttonBar); + + loadSelectedServicesFromJob(); + } + + private VerticalLayout createRouteSection() { + VerticalLayout routeBox = new VerticalLayout(); + routeBox.setPadding(true); + routeBox.setSpacing(true); + routeBox.setWidthFull(); + routeBox.getStyle().set("border", "1px solid var(--lumo-primary-color-50pct)"); + routeBox.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); + routeBox.getStyle().set("background-color", "var(--lumo-primary-color-10pct)"); + routeBox.addClassName("route-card"); + + H3 routeTitle = new H3(getTranslation("addjob.route.title")); + routeTitle.getStyle().set("margin", "0"); + routeTitle.getStyle().set("color", "var(--lumo-primary-text-color)"); + + HorizontalLayout distanceRow = new HorizontalLayout(); + distanceRow.setWidthFull(); + distanceRow.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); + distanceRow.setAlignItems(FlexComponent.Alignment.CENTER); + Span distanceLabel = new Span(getTranslation("addjob.route.distance") + ":"); + Span distanceValue = new Span(formatDistance(job.getRouteDistanceKm())); + distanceValue.getStyle().set("font-weight", "bold"); + distanceValue.getStyle().set("font-size", "var(--lumo-font-size-l)"); + distanceValue.getStyle().set("color", "var(--lumo-primary-text-color)"); + distanceRow.add(distanceLabel, distanceValue); + + HorizontalLayout durationRow = new HorizontalLayout(); + durationRow.setWidthFull(); + durationRow.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); + durationRow.setAlignItems(FlexComponent.Alignment.CENTER); + Span durationLabel = new Span(getTranslation("addjob.route.duration") + ":"); + Span durationValue = new Span(formatDuration(job.getRouteDurationSeconds())); + durationValue.getStyle().set("font-weight", "bold"); + durationValue.getStyle().set("color", "var(--lumo-secondary-text-color)"); + durationRow.add(durationLabel, durationValue); + + routeBox.add(routeTitle, distanceRow, durationRow); + return routeBox; + } + + private VerticalLayout createManualRouteSection() { + VerticalLayout box = new VerticalLayout(); + box.setPadding(true); + box.setSpacing(true); + box.setWidthFull(); + box.getStyle().set("border", "1px solid var(--lumo-primary-color-50pct)"); + box.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); + box.getStyle().set("background-color", "var(--lumo-primary-color-10pct)"); + box.addClassName("route-card"); + + H3 title = new H3(getTranslation("addjob.route.title")); + title.getStyle().set("margin", "0"); + title.getStyle().set("color", "var(--lumo-primary-text-color)"); + + NumberField distanceField = new NumberField(getTranslation("addjob.route.distance.km")); + distanceField.setMin(0); + distanceField.setStep(0.1); + distanceField.setPlaceholder(getTranslation("addjob.route.distance.placeholder")); + distanceField.setWidthFull(); + + IntegerField hoursField = new IntegerField(getTranslation("jobmanualcomplete.route.hours")); + hoursField.setMin(0); + hoursField.setStepButtonsVisible(true); + hoursField.setWidthFull(); + + IntegerField minutesField = new IntegerField(getTranslation("jobmanualcomplete.route.minutes")); + minutesField.setMin(0); + minutesField.setMax(59); + minutesField.setStepButtonsVisible(true); + minutesField.setWidthFull(); + + if (manualDurationSeconds != null && manualDurationSeconds > 0) { + hoursField.setValue(manualDurationSeconds / 3600); + minutesField.setValue((manualDurationSeconds % 3600) / 60); + } + + distanceField.addValueChangeListener(e -> { + manualDistanceKm = e.getValue(); + servicesGrid.getDataProvider().refreshAll(); + updatePriceSummary(); + }); + + Runnable recalcDuration = () -> { + Integer h = hoursField.getValue(); + Integer m = minutesField.getValue(); + int hours = h != null ? Math.max(0, h) : 0; + int minutes = m != null ? Math.max(0, Math.min(59, m)) : 0; + int total = hours * 3600 + minutes * 60; + manualDurationSeconds = total > 0 ? total : null; + servicesGrid.getDataProvider().refreshAll(); + updatePriceSummary(); + }; + hoursField.addValueChangeListener(e -> recalcDuration.run()); + minutesField.addValueChangeListener(e -> recalcDuration.run()); + + HorizontalLayout durationRow = new HorizontalLayout(hoursField, minutesField); + durationRow.setWidthFull(); + durationRow.setSpacing(true); + hoursField.getStyle().set("flex", "1"); + minutesField.getStyle().set("flex", "1"); + + Span hint = new Span(getTranslation("jobmanualcomplete.route.manual.hint")); + hint.getStyle().set("font-size", "var(--lumo-font-size-s)"); + hint.getStyle().set("color", "var(--lumo-secondary-text-color)"); + hint.getStyle().set("font-style", "italic"); + + box.add(title, distanceField, durationRow, hint); + return box; + } + + private VerticalLayout createServicesSection() { + VerticalLayout section = new VerticalLayout(); + section.setPadding(false); + section.setSpacing(true); + section.setWidthFull(); + + H3 title = new H3(getTranslation("addjob.services.title")); + title.getStyle().set("margin", "0"); + + servicesGrid = new Grid<>(); + servicesGrid.setWidthFull(); + servicesGrid.setHeight("250px"); + servicesGrid.setItems(serviceRows); + servicesGrid.addClassName("data-grid"); + + servicesGrid.addColumn(row -> row.service != null ? row.service.getName() : "") + .setHeader(getTranslation("common.service")).setSortable(true); + servicesGrid.addColumn(row -> formatDeliveryStationLabel( + row.selection != null ? row.selection.getDeliveryStationOrder() : null)) + .setHeader(getTranslation("addjob.services.deliverystation")).setSortable(false); + servicesGrid.addColumn(row -> formatCalculationBasis(row.service)) + .setHeader(getTranslation("addjob.services.calculation")).setSortable(true); + servicesGrid.addColumn(this::formatPrice).setHeader(getTranslation("common.price")).setSortable(false); + servicesGrid.addComponentColumn(row -> { + if (row.service != null && row.service.isMandatory()) { + return new Span(""); + } + Button removeButton = new Button(new Icon(VaadinIcon.TRASH)); + removeButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY, + ButtonVariant.LUMO_SMALL); + removeButton.addClickListener(e -> { + serviceRows.remove(row); + servicesGrid.getDataProvider().refreshAll(); + updatePriceSummary(); + }); + return removeButton; + }).setHeader(getTranslation("common.actions")).setAutoWidth(true).setFlexGrow(0); + + Div gridPanel = new Div(servicesGrid); + gridPanel.addClassNames("surface-panel", "data-grid-panel"); + gridPanel.setWidthFull(); + + Button addButton = new Button(getTranslation("addjob.services.add"), new Icon(VaadinIcon.PLUS)); + addButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + addButton.addClickListener(e -> openAddServiceDialog()); + + section.add(title, gridPanel, addButton); + return section; + } + + private VerticalLayout createSummarySection() { + VerticalLayout summary = new VerticalLayout(); + summary.setPadding(true); + summary.setSpacing(true); + summary.setWidthFull(); + summary.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); + summary.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); + summary.getStyle().set("background-color", "var(--lumo-contrast-5pct)"); + summary.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); + summary.addClassName("summary-card"); + + H3 title = new H3(getTranslation("addjob.summary.title")); + title.getStyle().set("margin", "0"); + summary.add(title); + + Div priceTable = new Div(); + priceTable.getStyle().set("width", "100%"); + + Div netRow = new Div(); + netRow.getStyle().set("display", "flex").set("justify-content", "space-between").set("padding", "4px 0"); + Span netLabel = new Span(getTranslation("addjob.summary.net") + ":"); + netLabel.getStyle().set("padding-right", "8px"); + netTotalLabel = new Span("0,00 €"); + netTotalLabel.getStyle().set("font-weight", "bold").set("white-space", "nowrap"); + netRow.add(netLabel, netTotalLabel); + + Div grossRow = new Div(); + grossRow.getStyle().set("display", "flex").set("justify-content", "space-between").set("padding", "4px 0"); + Span grossLabel = new Span(getTranslation("addjob.summary.gross") + ":"); + grossLabel.getStyle().set("padding-right", "8px").set("font-weight", "bold"); + grossTotalLabel = new Span("0,00 €"); + grossTotalLabel.getStyle().set("font-size", "var(--lumo-font-size-l)"); + grossTotalLabel.getStyle().set("font-weight", "bold"); + grossTotalLabel.getStyle().set("color", "var(--lumo-primary-text-color)").set("white-space", "nowrap"); + grossRow.add(grossLabel, grossTotalLabel); + + priceTable.add(netRow, grossRow); + summary.add(priceTable); + return summary; + } + + private VerticalLayout createRemarkSection() { + VerticalLayout section = new VerticalLayout(); + section.setPadding(false); + section.setSpacing(true); + section.setWidthFull(); + + H3 title = new H3(getTranslation("addjob.tasks.remark")); + title.getStyle().set("margin", "0"); + + remarkArea = new TextArea(); + remarkArea.setPlaceholder(getTranslation("addjob.tasks.remark.placeholder")); + remarkArea.setWidthFull(); + remarkArea.setMinHeight("120px"); + if (job.getRemark() != null) { + remarkArea.setValue(job.getRemark()); + } + + section.add(title, remarkArea); + return section; + } + + private void loadSelectedServicesFromJob() { + serviceRows.clear(); + + if (job.getSelectedServices() != null && !job.getSelectedServices().isEmpty()) { + for (JobServiceSelection selection : job.getSelectedServices()) { + if (selection.getServiceId() == null) { + continue; + } + serviceRepository.findById(selection.getServiceId()) + .ifPresent(service -> serviceRows.add(new ServiceRow(service, selection))); + } + } else if (job.getServiceIds() != null && !job.getServiceIds().isEmpty()) { + for (String serviceId : job.getServiceIds()) { + serviceRepository.findById(serviceId) + .ifPresent(service -> serviceRows.add(new ServiceRow(service, null))); + } + } + + servicesGrid.getDataProvider().refreshAll(); + updatePriceSummary(); + } + + private void openAddServiceDialog() { + Dialog dialog = DialogStylingHelper.createStyledDialog(getTranslation("addjob.services.dialog.title"), "720px"); + dialog.setCloseOnOutsideClick(false); + + VerticalLayout dialogContent = DialogStylingHelper.createContentLayout("620px"); + + User currentUser = securityService.getCurrentDatabaseUser(); + List availableServices = currentUser != null + ? serviceRepository.findByUserId(currentUser.getId().toString()) + : List.of(); + + ComboBox serviceCombo = new ComboBox<>(getTranslation("common.service")); + serviceCombo.setWidthFull(); + serviceCombo.setItems(availableServices); + serviceCombo.setItemLabelGenerator(service -> { + if (service.getCalculationBasis() == Service.CalculationBasis.FLAT_RATE + && service.getEffectivePrice() != null) { + return service.getName() + " (" + service.getEffectivePrice().setScale(2, RoundingMode.HALF_UP) + " €)"; + } + return service.getName(); + }); + serviceCombo.setPlaceholder(getTranslation("addjob.services.dialog.placeholder")); + serviceCombo.setRequired(true); + + List stationOrders = availableDeliveryStationOrders(); + ComboBox stationCombo = new ComboBox<>(getTranslation("addjob.services.deliverystation")); + stationCombo.setWidthFull(); + stationCombo.setRequired(true); + stationCombo.setRequiredIndicatorVisible(true); + stationCombo.setItems(stationOrders); + stationCombo.setItemLabelGenerator(this::buildDeliveryStationSelectionLabel); + stationCombo.setPlaceholder(getTranslation("addjob.services.dialog.station.placeholder")); + if (!stationOrders.isEmpty()) { + stationCombo.setValue(0); + } + + dialogContent.add(serviceCombo, stationCombo); + + Button cancel = new Button(getTranslation("button.cancel"), e -> dialog.close()); + cancel.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + Button add = new Button(getTranslation("addjob.services.dialog.add"), e -> { + Service service = serviceCombo.getValue(); + Integer stationOrder = stationCombo.getValue(); + if (service == null || stationOrder == null) { + return; + } + JobServiceSelection selection = new JobServiceSelection(); + selection.setServiceId(service.getId()); + selection.setDeliveryStationOrder(stationOrder); + selection.setRouteDistanceKm(manualDistanceKm); + selection.setRouteDurationSeconds(manualDurationSeconds); + serviceRows.add(new ServiceRow(service, selection)); + servicesGrid.getDataProvider().refreshAll(); + updatePriceSummary(); + dialog.close(); + }); + add.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + dialog.add(DialogStylingHelper.wrapContent(dialogContent)); + dialog.getFooter().add(cancel, add); + dialog.open(); + } + + private List availableDeliveryStationOrders() { + List orders = new ArrayList<>(); + if (job.getDeliveryStations() != null) { + for (int i = 0; i < job.getDeliveryStations().size(); i++) { + orders.add(i); + } + } + return orders; + } + + private String buildDeliveryStationSelectionLabel(Integer order) { + if (order == null || job.getDeliveryStations() == null || order < 0 + || order >= job.getDeliveryStations().size()) { + return "-"; + } + DeliveryStation station = job.getDeliveryStations().get(order); + StringBuilder label = new StringBuilder(getTranslation("addjob.station.delivery", order + 1)); + if (station.getCity() != null && !station.getCity().isBlank()) { + label.append(" - ").append(station.getCity()); + } else if (station.getCompany() != null && !station.getCompany().isBlank()) { + label.append(" - ").append(station.getCompany()); + } + return label.toString(); + } + + private String formatDeliveryStationLabel(Integer order) { + if (order == null || order < 0) { + return "-"; + } + return getTranslation("addjob.station.delivery", order + 1); + } + + private String formatCalculationBasis(Service service) { + if (service == null || service.getCalculationBasis() == null) { + return ""; + } + return switch (service.getCalculationBasis()) { + case DISTANCE -> getTranslation("addjob.services.basis.distance"); + case TIME -> getTranslation("addjob.services.basis.time"); + case FLAT_RATE -> getTranslation("addjob.services.basis.flatrate"); + }; + } + + private String formatPrice(ServiceRow row) { + Service service = row.service; + if (service == null || service.getCalculationBasis() == null) { + return ""; + } + BigDecimal price = calculateServicePrice(row); + if (price != null && price.compareTo(BigDecimal.ZERO) > 0) { + return price.setScale(2, RoundingMode.HALF_UP) + " €"; + } + if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE && service.getPricePerKilometer() != null + && routeDistanceFor(row.selection) == null) { + return service.getPricePerKilometer().setScale(2, RoundingMode.HALF_UP) + " €/km (" + + getTranslation("addjob.services.route.missing") + ")"; + } + if (service.getCalculationBasis() == Service.CalculationBasis.TIME && service.getPricePer15Minutes() != null + && routeDurationFor(row.selection) == null) { + return service.getPricePer15Minutes().setScale(2, RoundingMode.HALF_UP) + " €/15 Min. (" + + getTranslation("addjob.services.route.missing") + ")"; + } + return service.getEffectivePrice() != null + ? service.getEffectivePrice().setScale(2, RoundingMode.HALF_UP) + " €" + : ""; + } + + private BigDecimal calculateServicePrice(ServiceRow row) { + Service service = row.service; + if (service == null || service.getCalculationBasis() == null) { + return BigDecimal.ZERO; + } + switch (service.getCalculationBasis()) { + case FLAT_RATE: + return service.getPrice() != null ? service.getPrice() : BigDecimal.ZERO; + case DISTANCE: { + Double km = routeDistanceFor(row.selection); + if (service.getPricePerKilometer() != null && km != null && km > 0) { + return service.getPricePerKilometer().multiply(BigDecimal.valueOf(km)); + } + return BigDecimal.ZERO; + } + case TIME: { + Integer seconds = routeDurationFor(row.selection); + if (service.getPricePer15Minutes() != null && seconds != null && seconds > 0) { + int units = seconds / 900; + if (seconds % 900 > 0) { + units++; + } + return service.getPricePer15Minutes().multiply(BigDecimal.valueOf(units)); + } + return BigDecimal.ZERO; + } + default: + return BigDecimal.ZERO; + } + } + + private Double routeDistanceFor(JobServiceSelection selection) { + if (selection != null && selection.getRouteDistanceKm() != null) { + return selection.getRouteDistanceKm(); + } + return manualDistanceKm; + } + + private Integer routeDurationFor(JobServiceSelection selection) { + if (selection != null && selection.getRouteDurationSeconds() != null) { + return selection.getRouteDurationSeconds(); + } + return manualDurationSeconds; + } + + private void updatePriceSummary() { + BigDecimal net = BigDecimal.ZERO; + for (ServiceRow row : serviceRows) { + net = net.add(calculateServicePrice(row)); + } + BigDecimal gross = net.add(net.multiply(vatRate)); + netTotalLabel.setText(formatAmount(net)); + grossTotalLabel.setText(formatAmount(gross)); + } + + private String formatAmount(BigDecimal amount) { + return amount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €"; + } + + private String formatDistance(Double km) { + if (km == null) { + return "-"; + } + return String.format(Locale.GERMANY, "%.1f km", km); + } + + private String formatDuration(Integer seconds) { + if (seconds == null || seconds <= 0) { + return "-"; + } + int hours = seconds / 3600; + int minutes = (seconds % 3600) / 60; + if (hours > 0) { + return String.format("%d Std. %d Min.", hours, minutes); + } + return String.format("%d Min.", minutes); + } + + private void confirm(TextArea reasonField) { + String reason = reasonField.getValue(); + if (reason == null || reason.trim().isEmpty()) { + reasonField.setInvalid(true); + reasonField.setErrorMessage(getTranslation("jobsummary.dialog.manualcomplete.reason.required")); + return; + } + + try { + JobStatus oldStatus = job.getStatus(); + + List selections = new ArrayList<>(); + for (ServiceRow row : serviceRows) { + if (row.service == null) { + continue; + } + JobServiceSelection selection = row.selection != null ? row.selection : new JobServiceSelection(); + selection.setServiceId(row.service.getId()); + if (selection.getDeliveryStationOrder() == null && row.selection != null) { + selection.setDeliveryStationOrder(row.selection.getDeliveryStationOrder()); + } + selections.add(selection); + } + job.setSelectedServices(selections); + + String remark = remarkArea.getValue(); + job.setRemark(remark != null && !remark.isBlank() ? remark.trim() : null); + + if (job.getRouteDistanceKm() == null || job.getRouteDistanceKm() <= 0) { + job.setRouteDistanceKm(manualDistanceKm); + job.setRouteDurationSeconds(manualDurationSeconds); + } + + job.setStatus(JobStatus.COMPLETED); + job.setUpdatedAt(LocalDateTime.now()); + jobRepository.save(job); + + String currentUser = securityService.getCurrentUsername(); + jobHistoryService.logStatusChange(job, oldStatus, JobStatus.COMPLETED, currentUser); + + String description = String.format("Auftrag manuell beendet von %s. Begründung: %s", currentUser, + reason.trim()); + jobHistoryService.logCustomEvent(job.getId(), getTranslation("jobsummary.history.manualcomplete.reason"), + description, currentUser, JobHistoryType.STATUS_CHANGE); + + Notification + .show(getTranslation("jobsummary.notification.completed", job.getJobNumber()), 3000, + Notification.Position.BOTTOM_END) + .addThemeVariants(NotificationVariant.LUMO_SUCCESS); + getUI().ifPresent(ui -> ui.navigate("job_summary/" + job.getId().toHexString())); + } catch (Exception ex) { + Notification + .show(getTranslation("jobsummary.notification.complete.error", ex.getMessage()), 5000, + Notification.Position.BOTTOM_END) + .addThemeVariants(NotificationVariant.LUMO_ERROR); + } } } diff --git a/backend/src/main/resources/messages_de.properties b/backend/src/main/resources/messages_de.properties index 6a81022..a33a0b5 100644 --- a/backend/src/main/resources/messages_de.properties +++ b/backend/src/main/resources/messages_de.properties @@ -242,7 +242,7 @@ page.title.appuser.create=Neuen App-Nutzer anlegen page.title.messages=Nachrichten page.title.register=Bei VotianLT registrieren page.title.customers=Kunden -page.title.customer.edit=Kunde bearbeiten +page.title.customer.edit=Adresse bearbeiten page.title.verwaltung=Verwaltung page.title.company.create=Neue Firma anlegen page.title.imprint=Impressum @@ -339,13 +339,13 @@ customers.column.street=Straße customers.column.city=Ort # Edit Customer -editcustomer.title=Kunde bearbeiten -editcustomer.notification.notfound=Kunde nicht gefunden -editcustomer.notification.invalid.id=Ungültige Kunden-ID -editcustomer.notification.saved=Kunde erfolgreich gespeichert +editcustomer.title=Adresse bearbeiten +editcustomer.notification.notfound=Adresse nicht gefunden +editcustomer.notification.invalid.id=Ungültige Adress-ID +editcustomer.notification.saved=Adresse erfolgreich gespeichert editcustomer.notification.check=Bitte überprüfen Sie Ihre Eingaben -editcustomer.notification.deleted=Kunde erfolgreich gelöscht -editcustomer.dialog.delete.text=Möchten Sie diesen Kunden wirklich löschen? +editcustomer.notification.deleted=Adresse erfolgreich gelöscht +editcustomer.dialog.delete.text=Möchten Sie diese Adresse wirklich löschen? editcustomer.dialog.delete.confirm=Löschen # Add Customer @@ -429,9 +429,9 @@ messages.sender.unknown=Unbekannter Absender # Add Job addjob.title=Neuen Auftrag anlegen -addjob.customer.label=Kunde -addjob.customer.placeholder=Kunde auswählen -addjob.customer.unnamed=Unbenannter Kunde +addjob.customer.label=Auftraggeber +addjob.customer.placeholder=Auftraggeber auswählen +addjob.customer.unnamed=Unbenannter Auftraggeber addjob.button.clearfields=Felder leeren addjob.button.submit=Auftrag anlegen addjob.address.salutation=Anrede @@ -440,6 +440,10 @@ addjob.salutation.mr=Herr addjob.salutation.ms=Frau addjob.salutation.other=Divers addjob.address.company.placeholder=Firma eingeben +addjob.address.pickup.label=Abholadresse +addjob.address.pickup.placeholder=Abholadresse auswählen oder eingeben +addjob.address.delivery.label=Lieferadresse +addjob.address.delivery.placeholder=Lieferadresse auswählen oder eingeben addjob.address.street.placeholder=Straße eingeben addjob.address.housenumber=Hausnummer addjob.address.addition.placeholder=Adresszusatz @@ -460,6 +464,8 @@ addjob.station.max.reached=Maximale Anzahl von 25 Lieferstationen erreicht addjob.station.unused=Nicht genutzt addjob.appointment.delivery.info=Liefertermine werden direkt in den Lieferstationen festgelegt. addjob.tab.addresses=Auftraggeber & Adressen +addjob.tab.pickup.address=Auftraggeber & Abholadresse +addjob.tab.delivery.address=Lieferadresse addjob.tab.appointments=Termine & Verarbeitung addjob.tab.cargo=Fracht addjob.tab.tasks=Aufgaben @@ -621,6 +627,9 @@ jobsummary.dialog.manualcomplete.reason.required=Bitte geben Sie eine Begründun jobsummary.dialog.manualcomplete.cancel=Abbrechen jobsummary.dialog.manualcomplete.confirm=Akzeptiert jobsummary.history.manualcomplete.reason=Manuell beendet +jobmanualcomplete.route.hours=Stunden +jobmanualcomplete.route.minutes=Minuten +jobmanualcomplete.route.manual.hint=Keine Routendaten vorhanden – bitte Entfernung und Dauer manuell erfassen. # Jobs jobs.title=Aufträge diff --git a/backend/src/main/resources/messages_ee.properties b/backend/src/main/resources/messages_ee.properties index 8331d56..70c421e 100644 --- a/backend/src/main/resources/messages_ee.properties +++ b/backend/src/main/resources/messages_ee.properties @@ -377,9 +377,9 @@ messages.preview.image=Pilt messages.preview.empty=Eelvaade puudub messages.sender.unknown=Tundmatu saatja addjob.title=Uue tellimuse loomine -addjob.customer.label=Klient -addjob.customer.placeholder=Valige klient -addjob.customer.unnamed=Nimetu klient +addjob.customer.label=Tellija +addjob.customer.placeholder=Vali tellija +addjob.customer.unnamed=Nimetu tellija addjob.button.clearfields=T\u00fchjenda v\u00e4ljad addjob.button.submit=Loo tellimus addjob.address.salutation=P\u00f6\u00f6rdumine @@ -388,6 +388,10 @@ addjob.salutation.mr=Hr addjob.salutation.ms=Pr addjob.salutation.other=Muu addjob.address.company.placeholder=Sisestage ettev\u00f5te +addjob.address.pickup.label=Pealekorje aadress +addjob.address.pickup.placeholder=Vali v\u00f5i sisesta pealekorje aadress +addjob.address.delivery.label=Kohaletoimetamise aadress +addjob.address.delivery.placeholder=Vali v\u00f5i sisesta kohaletoimetamise aadress addjob.address.street.placeholder=Sisestage t\u00e4nav addjob.address.housenumber=Majanumber addjob.address.addition.placeholder=Aadressi t\u00e4iend @@ -408,6 +412,8 @@ addjob.station.max.reached=Maksimaalne arv 25 kohaletoimetamise jaama on saavuta addjob.station.unused=Kasutamata addjob.appointment.delivery.info=Kohaletoimetamise ajad m\u00e4\u00e4ratakse otse kohaletoimetamise jaamades. addjob.tab.addresses=Tellija ja aadressid +addjob.tab.pickup.address=Tellija ja pealekorje aadress +addjob.tab.delivery.address=Kohaletoimetamise aadress addjob.tab.appointments=Ajad ja t\u00f6\u00f6tlemine addjob.tab.cargo=Veosed addjob.tab.tasks=\u00dclesanded @@ -567,6 +573,9 @@ jobsummary.dialog.manualcomplete.reason.required=Palun sisestage põhjendus jobsummary.dialog.manualcomplete.cancel=Tühista jobsummary.dialog.manualcomplete.confirm=Nõustu jobsummary.history.manualcomplete.reason=Käsitsi lõpetatud +jobmanualcomplete.route.hours=Tunnid +jobmanualcomplete.route.minutes=Minutid +jobmanualcomplete.route.manual.hint=Marsruudiandmed puuduvad – palun sisestage vahemaa ja kestus käsitsi. jobs.title=Tellimused jobs.filter.search=Otsi jobs.filter.search.placeholder=Otsi tellimuse numbri j\u00e4rgi... diff --git a/backend/src/main/resources/messages_en.properties b/backend/src/main/resources/messages_en.properties index 98ddafa..8fa75f9 100644 --- a/backend/src/main/resources/messages_en.properties +++ b/backend/src/main/resources/messages_en.properties @@ -429,9 +429,9 @@ messages.sender.unknown=Unknown Sender # Add Job addjob.title=Create New Job -addjob.customer.label=Customer -addjob.customer.placeholder=Select Customer -addjob.customer.unnamed=Unnamed Customer +addjob.customer.label=Principal +addjob.customer.placeholder=Select principal +addjob.customer.unnamed=Unnamed principal addjob.button.clearfields=Clear Fields addjob.button.submit=Create Job addjob.address.salutation=Salutation @@ -440,6 +440,10 @@ addjob.salutation.mr=Mr addjob.salutation.ms=Ms addjob.salutation.other=Other addjob.address.company.placeholder=Enter company +addjob.address.pickup.label=Pickup address +addjob.address.pickup.placeholder=Select or enter pickup address +addjob.address.delivery.label=Delivery address +addjob.address.delivery.placeholder=Select or enter delivery address addjob.address.street.placeholder=Enter street addjob.address.housenumber=House Number addjob.address.addition.placeholder=Address suffix @@ -460,6 +464,8 @@ addjob.station.max.reached=Maximum number of 25 delivery stations reached addjob.station.unused=Not used addjob.appointment.delivery.info=Delivery dates are set directly in the delivery stations. addjob.tab.addresses=Client & Addresses +addjob.tab.pickup.address=Principal & Pickup Address +addjob.tab.delivery.address=Delivery Address addjob.tab.appointments=Appointments & Processing addjob.tab.cargo=Cargo addjob.tab.tasks=Tasks @@ -621,6 +627,9 @@ jobsummary.dialog.manualcomplete.reason.required=Please enter a reason jobsummary.dialog.manualcomplete.cancel=Cancel jobsummary.dialog.manualcomplete.confirm=Accept jobsummary.history.manualcomplete.reason=Manually completed +jobmanualcomplete.route.hours=Hours +jobmanualcomplete.route.minutes=Minutes +jobmanualcomplete.route.manual.hint=No route data available – please enter distance and duration manually. # Jobs jobs.title=Jobs diff --git a/backend/src/main/resources/messages_es.properties b/backend/src/main/resources/messages_es.properties index b51e8e0..2bdcb1b 100644 --- a/backend/src/main/resources/messages_es.properties +++ b/backend/src/main/resources/messages_es.properties @@ -428,9 +428,9 @@ messages.sender.unknown=Remitente desconocido # Add Job addjob.title=Crear nuevo pedido -addjob.customer.label=Cliente -addjob.customer.placeholder=Seleccionar cliente -addjob.customer.unnamed=Cliente sin nombre +addjob.customer.label=Ordenante +addjob.customer.placeholder=Seleccionar ordenante +addjob.customer.unnamed=Ordenante sin nombre addjob.button.clearfields=Vaciar campos addjob.button.submit=Crear pedido addjob.address.salutation=Tratamiento @@ -439,6 +439,10 @@ addjob.salutation.mr=Sr. addjob.salutation.ms=Sra. addjob.salutation.other=Otro addjob.address.company.placeholder=Introducir empresa +addjob.address.pickup.label=Direcci\u00f3n de recogida +addjob.address.pickup.placeholder=Seleccionar o introducir direcci\u00f3n de recogida +addjob.address.delivery.label=Direcci\u00f3n de entrega +addjob.address.delivery.placeholder=Seleccionar o introducir direcci\u00f3n de entrega addjob.address.street.placeholder=Introducir calle addjob.address.housenumber=N\u00famero de casa addjob.address.addition.placeholder=Complemento de direcci\u00f3n @@ -459,6 +463,8 @@ addjob.station.max.reached=Se ha alcanzado el n\u00famero m\u00e1ximo de 25 esta addjob.station.unused=No utilizada addjob.appointment.delivery.info=Las fechas de entrega se establecen directamente en las estaciones de entrega. addjob.tab.addresses=Cliente y direcciones +addjob.tab.pickup.address=Ordenante y direcci\u00f3n de recogida +addjob.tab.delivery.address=Direcci\u00f3n de entrega addjob.tab.appointments=Citas y procesamiento addjob.tab.cargo=Carga addjob.tab.tasks=Tareas @@ -620,6 +626,9 @@ jobsummary.dialog.manualcomplete.reason.required=Por favor, introduzca un motivo jobsummary.dialog.manualcomplete.cancel=Cancelar jobsummary.dialog.manualcomplete.confirm=Aceptar jobsummary.history.manualcomplete.reason=Finalizado manualmente +jobmanualcomplete.route.hours=Horas +jobmanualcomplete.route.minutes=Minutos +jobmanualcomplete.route.manual.hint=No hay datos de ruta disponibles – introduzca la distancia y la duración manualmente. # Jobs jobs.title=Pedidos diff --git a/backend/src/main/resources/messages_fr.properties b/backend/src/main/resources/messages_fr.properties index d958081..2e159d5 100644 --- a/backend/src/main/resources/messages_fr.properties +++ b/backend/src/main/resources/messages_fr.properties @@ -428,9 +428,9 @@ messages.sender.unknown=Exp\u00e9diteur inconnu # Add Job addjob.title=Cr\u00e9er une nouvelle mission -addjob.customer.label=Client -addjob.customer.placeholder=S\u00e9lectionner un client -addjob.customer.unnamed=Client sans nom +addjob.customer.label=Donneur d'ordre +addjob.customer.placeholder=S\u00e9lectionner le donneur d'ordre +addjob.customer.unnamed=Donneur d'ordre sans nom addjob.button.clearfields=Vider les champs addjob.button.submit=Cr\u00e9er la mission addjob.address.salutation=Civilit\u00e9 @@ -439,6 +439,10 @@ addjob.salutation.mr=Monsieur addjob.salutation.ms=Madame addjob.salutation.other=Autre addjob.address.company.placeholder=Saisir l'entreprise +addjob.address.pickup.label=Adresse d'enl\u00e8vement +addjob.address.pickup.placeholder=S\u00e9lectionner ou saisir l'adresse d'enl\u00e8vement +addjob.address.delivery.label=Adresse de livraison +addjob.address.delivery.placeholder=S\u00e9lectionner ou saisir l'adresse de livraison addjob.address.street.placeholder=Saisir la rue addjob.address.housenumber=Num\u00e9ro addjob.address.addition.placeholder=Compl\u00e9ment d'adresse @@ -459,6 +463,8 @@ addjob.station.max.reached=Nombre maximum de 25 stations de livraison atteint addjob.station.unused=Non utilis\u00e9e addjob.appointment.delivery.info=Les dates de livraison sont d\u00e9finies directement dans les stations de livraison. addjob.tab.addresses=Donneur d'ordre & adresses +addjob.tab.pickup.address=Donneur d'ordre & adresse d'enl\u00e8vement +addjob.tab.delivery.address=Adresse de livraison addjob.tab.appointments=Rendez-vous & traitement addjob.tab.cargo=Fret addjob.tab.tasks=T\u00e2ches @@ -620,6 +626,9 @@ jobsummary.dialog.manualcomplete.reason.required=Veuillez saisir un motif jobsummary.dialog.manualcomplete.cancel=Annuler jobsummary.dialog.manualcomplete.confirm=Accepter jobsummary.history.manualcomplete.reason=Termin\u00e9 manuellement +jobmanualcomplete.route.hours=Heures +jobmanualcomplete.route.minutes=Minutes +jobmanualcomplete.route.manual.hint=Aucune donn\u00e9e d'itin\u00e9raire disponible \u2013 veuillez saisir la distance et la dur\u00e9e manuellement. # Jobs jobs.title=Missions diff --git a/backend/src/main/resources/messages_lt.properties b/backend/src/main/resources/messages_lt.properties index 8485195..3259833 100644 --- a/backend/src/main/resources/messages_lt.properties +++ b/backend/src/main/resources/messages_lt.properties @@ -428,9 +428,9 @@ messages.sender.unknown=Nežinomas siuntėjas # Add Job addjob.title=Sukurti naują užsakymą -addjob.customer.label=Klientas -addjob.customer.placeholder=Pasirinkite klientą -addjob.customer.unnamed=Klientas be pavadinimo +addjob.customer.label=Užsakovas +addjob.customer.placeholder=Pasirinkite užsakovą +addjob.customer.unnamed=Neįvardytas užsakovas addjob.button.clearfields=Išvalyti laukus addjob.button.submit=Sukurti užsakymą addjob.address.salutation=Kreipinys @@ -439,6 +439,10 @@ addjob.salutation.mr=Ponas addjob.salutation.ms=Ponia addjob.salutation.other=Kita addjob.address.company.placeholder=Įveskite įmonę +addjob.address.pickup.label=Atsiėmimo adresas +addjob.address.pickup.placeholder=Pasirinkti arba įvesti atsiėmimo adresą +addjob.address.delivery.label=Pristatymo adresas +addjob.address.delivery.placeholder=Pasirinkti arba įvesti pristatymo adresą addjob.address.street.placeholder=Įveskite gatvę addjob.address.housenumber=Namo numeris addjob.address.addition.placeholder=Adreso priedas @@ -459,6 +463,8 @@ addjob.station.max.reached=Pasiektas maksimalus 25 pristatymo stočių skaičius addjob.station.unused=Nenaudojama addjob.appointment.delivery.info=Pristatymo terminai nustatomi tiesiogiai pristatymo stotyse. addjob.tab.addresses=Užsakovas ir adresai +addjob.tab.pickup.address=Užsakovas ir atsiėmimo adresas +addjob.tab.delivery.address=Pristatymo adresas addjob.tab.appointments=Terminai ir apdorojimas addjob.tab.cargo=Krovinys addjob.tab.tasks=Užduotys @@ -620,6 +626,9 @@ jobsummary.dialog.manualcomplete.reason.required=Prašome įvesti priežastį jobsummary.dialog.manualcomplete.cancel=Atšaukti jobsummary.dialog.manualcomplete.confirm=Priimti jobsummary.history.manualcomplete.reason=Užbaigta rankiniu būdu +jobmanualcomplete.route.hours=Valandos +jobmanualcomplete.route.minutes=Minutės +jobmanualcomplete.route.manual.hint=Maršruto duomenų nėra – prašome įvesti atstumą ir trukmę rankiniu būdu. # Jobs jobs.title=Užsakymai diff --git a/backend/src/main/resources/messages_lv.properties b/backend/src/main/resources/messages_lv.properties index ee4aab7..2440fdf 100644 --- a/backend/src/main/resources/messages_lv.properties +++ b/backend/src/main/resources/messages_lv.properties @@ -428,9 +428,9 @@ messages.sender.unknown=Nezināms sūtītājs # Add Job addjob.title=Izveidot jaunu uzdevumu -addjob.customer.label=Klients -addjob.customer.placeholder=Izvēlēties klientu -addjob.customer.unnamed=Nenosaukts klients +addjob.customer.label=Pasūtītājs +addjob.customer.placeholder=Izvēlēties pasūtītāju +addjob.customer.unnamed=Nenosaukts pasūtītājs addjob.button.clearfields=Notīrīt laukus addjob.button.submit=Izveidot uzdevumu addjob.address.salutation=Uzruna @@ -439,6 +439,10 @@ addjob.salutation.mr=Kungs addjob.salutation.ms=Kundze addjob.salutation.other=Cits addjob.address.company.placeholder=Ievadiet uzņēmumu +addjob.address.pickup.label=Saņemšanas adrese +addjob.address.pickup.placeholder=Izvēlēties vai ievadīt saņemšanas adresi +addjob.address.delivery.label=Piegādes adrese +addjob.address.delivery.placeholder=Izvēlēties vai ievadīt piegādes adresi addjob.address.street.placeholder=Ievadiet ielu addjob.address.housenumber=Mājas numurs addjob.address.addition.placeholder=Adreses papildinājums @@ -459,6 +463,8 @@ addjob.station.max.reached=Sasniegts maksimālais piegādes staciju skaits - 25 addjob.station.unused=Netiek izmantots addjob.appointment.delivery.info=Piegādes termiņi tiek noteikti tieši piegādes stacijās. addjob.tab.addresses=Pasūtītājs un adreses +addjob.tab.pickup.address=Pasūtītājs un saņemšanas adrese +addjob.tab.delivery.address=Piegādes adrese addjob.tab.appointments=Termiņi un apstrāde addjob.tab.cargo=Krava addjob.tab.tasks=Uzdevuma darbības @@ -620,6 +626,9 @@ jobsummary.dialog.manualcomplete.reason.required=Lūdzu, ievadiet pamatojumu jobsummary.dialog.manualcomplete.cancel=Atcelt jobsummary.dialog.manualcomplete.confirm=Apstiprināt jobsummary.history.manualcomplete.reason=Pabeigts manuāli +jobmanualcomplete.route.hours=Stundas +jobmanualcomplete.route.minutes=Minūtes +jobmanualcomplete.route.manual.hint=Maršruta dati nav pieejami – lūdzu, manuāli ievadiet attālumu un ilgumu. # Jobs jobs.title=Uzdevumi diff --git a/backend/src/main/resources/messages_pl.properties b/backend/src/main/resources/messages_pl.properties index 1d4066b..c649f09 100644 --- a/backend/src/main/resources/messages_pl.properties +++ b/backend/src/main/resources/messages_pl.properties @@ -428,9 +428,9 @@ messages.sender.unknown=Nieznany nadawca # Add Job addjob.title=Dodaj nowe zlecenie -addjob.customer.label=Klient -addjob.customer.placeholder=Wybierz klienta -addjob.customer.unnamed=Klient bez nazwy +addjob.customer.label=Zleceniodawca +addjob.customer.placeholder=Wybierz zleceniodawcę +addjob.customer.unnamed=Nienazwany zleceniodawca addjob.button.clearfields=Wyczy\u015b\u0107 pola addjob.button.submit=Utw\u00f3rz zlecenie addjob.address.salutation=Zwrot grzeczno\u015bciowy @@ -439,6 +439,10 @@ addjob.salutation.mr=Pan addjob.salutation.ms=Pani addjob.salutation.other=Inna addjob.address.company.placeholder=Wprowad\u017a firm\u0119 +addjob.address.pickup.label=Adres odbioru +addjob.address.pickup.placeholder=Wybierz lub wprowad\u017a adres odbioru +addjob.address.delivery.label=Adres dostawy +addjob.address.delivery.placeholder=Wybierz lub wprowad\u017a adres dostawy addjob.address.street.placeholder=Wprowad\u017a ulic\u0119 addjob.address.housenumber=Numer domu addjob.address.addition.placeholder=Dodatek do adresu @@ -459,6 +463,8 @@ addjob.station.max.reached=Osi\u0105gni\u0119to maksymaln\u0105 liczb\u0119 25 s addjob.station.unused=Nieu\u017cywana addjob.appointment.delivery.info=Terminy dostaw s\u0105 ustalane bezpo\u015brednio w stacjach dostawy. addjob.tab.addresses=Zleceniodawca i adresy +addjob.tab.pickup.address=Zleceniodawca i adres odbioru +addjob.tab.delivery.address=Adres dostawy addjob.tab.appointments=Terminy i przetwarzanie addjob.tab.cargo=\u0141adunek addjob.tab.tasks=Zadania @@ -620,6 +626,9 @@ jobsummary.dialog.manualcomplete.reason.required=Prosz\u0119 poda\u0107 uzasadni jobsummary.dialog.manualcomplete.cancel=Anuluj jobsummary.dialog.manualcomplete.confirm=Akceptuj jobsummary.history.manualcomplete.reason=Zako\u0144czono r\u0119cznie +jobmanualcomplete.route.hours=Godziny +jobmanualcomplete.route.minutes=Minuty +jobmanualcomplete.route.manual.hint=Brak danych trasy \u2013 prosz\u0119 r\u0119cznie poda\u0107 odleg\u0142o\u015b\u0107 i czas trwania. # Jobs jobs.title=Zlecenia diff --git a/backend/src/main/resources/messages_ru.properties b/backend/src/main/resources/messages_ru.properties index 333953c..d26b1b6 100644 --- a/backend/src/main/resources/messages_ru.properties +++ b/backend/src/main/resources/messages_ru.properties @@ -428,9 +428,9 @@ messages.sender.unknown=Неизвестный отправитель # Add Job addjob.title=Создать новый заказ -addjob.customer.label=Клиент -addjob.customer.placeholder=Выберите клиента -addjob.customer.unnamed=Безымянный клиент +addjob.customer.label=Заказчик +addjob.customer.placeholder=Выберите заказчика +addjob.customer.unnamed=Безымянный заказчик addjob.button.clearfields=Очистить поля addjob.button.submit=Создать заказ addjob.address.salutation=Обращение @@ -439,6 +439,10 @@ addjob.salutation.mr=Господин addjob.salutation.ms=Госпожа addjob.salutation.other=Другое addjob.address.company.placeholder=Введите компанию +addjob.address.pickup.label=Адрес забора +addjob.address.pickup.placeholder=Выберите или введите адрес забора +addjob.address.delivery.label=Адрес доставки +addjob.address.delivery.placeholder=Выберите или введите адрес доставки addjob.address.street.placeholder=Введите улицу addjob.address.housenumber=Номер дома addjob.address.addition.placeholder=Дополнение к адресу @@ -459,6 +463,8 @@ addjob.station.max.reached=Достигнуто максимальное кол addjob.station.unused=Не используется addjob.appointment.delivery.info=Сроки доставки устанавливаются непосредственно в станциях доставки. addjob.tab.addresses=Заказчик и адреса +addjob.tab.pickup.address=Заказчик и адрес забора +addjob.tab.delivery.address=Адрес доставки addjob.tab.appointments=Сроки и обработка addjob.tab.cargo=Груз addjob.tab.tasks=Задачи @@ -620,6 +626,9 @@ jobsummary.dialog.manualcomplete.reason.required=Пожалуйста, укаж jobsummary.dialog.manualcomplete.cancel=Отмена jobsummary.dialog.manualcomplete.confirm=Принять jobsummary.history.manualcomplete.reason=Завершено вручную +jobmanualcomplete.route.hours=Часы +jobmanualcomplete.route.minutes=Минуты +jobmanualcomplete.route.manual.hint=Данные маршрута отсутствуют — пожалуйста, введите расстояние и продолжительность вручную. # Jobs jobs.title=Заказы diff --git a/backend/src/main/resources/messages_tr.properties b/backend/src/main/resources/messages_tr.properties index 6ffd930..bae9798 100644 --- a/backend/src/main/resources/messages_tr.properties +++ b/backend/src/main/resources/messages_tr.properties @@ -428,9 +428,9 @@ messages.sender.unknown=Bilinmeyen G\u00f6nderici # Add Job addjob.title=Yeni \u0130\u015f Olu\u015ftur -addjob.customer.label=M\u00fc\u015fteri -addjob.customer.placeholder=M\u00fc\u015fteri Se\u00e7in -addjob.customer.unnamed=\u0130simsiz M\u00fc\u015fteri +addjob.customer.label=Sipari\u015f veren +addjob.customer.placeholder=Sipari\u015f vereni se\u00e7 +addjob.customer.unnamed=\u0130simsiz sipari\u015f veren addjob.button.clearfields=Alanlar\u0131 Temizle addjob.button.submit=\u0130\u015f Olu\u015ftur addjob.address.salutation=Hitap @@ -439,6 +439,10 @@ addjob.salutation.mr=Bay addjob.salutation.ms=Bayan addjob.salutation.other=Di\u011fer addjob.address.company.placeholder=\u015eirketi girin +addjob.address.pickup.label=Al\u0131m adresi +addjob.address.pickup.placeholder=Al\u0131m adresi se\u00e7in veya girin +addjob.address.delivery.label=Teslimat adresi +addjob.address.delivery.placeholder=Teslimat adresi se\u00e7in veya girin addjob.address.street.placeholder=Soka\u011f\u0131 girin addjob.address.housenumber=Kap\u0131 Numaras\u0131 addjob.address.addition.placeholder=Adres eki @@ -459,6 +463,8 @@ addjob.station.max.reached=Maksimum 25 teslimat istasyonu s\u0131n\u0131r\u0131n addjob.station.unused=Kullan\u0131lm\u0131yor addjob.appointment.delivery.info=Teslimat tarihleri do\u011frudan teslimat istasyonlar\u0131nda belirlenir. addjob.tab.addresses=M\u00fc\u015fteri & Adresler +addjob.tab.pickup.address=Sipari\u015f veren ve al\u0131m adresi +addjob.tab.delivery.address=Teslimat adresi addjob.tab.appointments=Randevular & \u0130\u015fleme addjob.tab.cargo=Kargo addjob.tab.tasks=G\u00f6revler @@ -620,6 +626,9 @@ jobsummary.dialog.manualcomplete.reason.required=Lütfen bir gerekçe girin jobsummary.dialog.manualcomplete.cancel=İptal jobsummary.dialog.manualcomplete.confirm=Kabul et jobsummary.history.manualcomplete.reason=Manuel olarak tamamlandı +jobmanualcomplete.route.hours=Saat +jobmanualcomplete.route.minutes=Dakika +jobmanualcomplete.route.manual.hint=Rota verisi mevcut değil – lütfen mesafeyi ve süreyi elle girin. # Jobs jobs.title=\u0130\u015fler