feat: Kundenauswahl vereinheitlicht und Job manuell beenden mit Leistungs-/Routenerfassung
- Kunden-Repository liefert auch Legacy-Dokumente ohne internal-Flag ($ne: true) - Auftraggeber- und Abholadress-Labels über neuen CustomerAddressLabelHelper, zeigen nur Firmenname bzw. Vor-/Nachname ohne Adresszusatz - Pickup-Dialog: E-Mail ist kein Pflichtfeld mehr - JobManualCompleteView erhält Route-/Leistungen-/Zusammenfassung-/Bemerkung-Block mit Vorbelegung aus dem Auftrag; bei fehlenden Routendaten manuelle Eingabe von Entfernung und Dauer, die in die Preisermittlung einfliessen Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<String, Customer> 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<String, Customer> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String> companyField, List<Customer> 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<String> 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<String> 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) {
|
||||
|
||||
@@ -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<String, Customer> 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<String> companyField, List<Customer> customers,
|
||||
TranslationHelper translationHelper) {
|
||||
List<String> 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<Customer> 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());
|
||||
|
||||
@@ -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<String> companyField, List<Customer> customers) {
|
||||
List<String> 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;
|
||||
CustomerAddressLabelHelper.putUnique(companyCustomerMap, customer,
|
||||
translationHelper.getTranslation("addjob.customer.unnamed"));
|
||||
}
|
||||
companyCustomerMap.put(companyName, customer);
|
||||
}
|
||||
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<Customer> 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<String, Customer> 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<String, Customer> 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
|
||||
// ============================================
|
||||
|
||||
@@ -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<Customer, ObjectId> {
|
||||
@@ -14,5 +15,8 @@ public interface CustomerRepository extends MongoRepository<Customer, ObjectId>
|
||||
|
||||
List<Customer> 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<Customer> findByOwnerAndInternalFalse(ObjectId owner);
|
||||
}
|
||||
|
||||
@@ -43,4 +43,8 @@ public class CustomerService {
|
||||
return todoRepository.findById(id).orElse(null);
|
||||
}
|
||||
|
||||
public void deleteById(ObjectId id) {
|
||||
todoRepository.deleteById(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Customer> 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<Customer> allCustomers = customerService.findAllForCurrentOwner();
|
||||
|
||||
// Extract unique company names (filter out null/empty values)
|
||||
List<String> companyNames = allCustomers.stream().map(Customer::getCompanyName)
|
||||
.filter(name -> name != null && !name.trim().isEmpty()).distinct().sorted().toList();
|
||||
Map<String, Customer> 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<Customer> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<String>, 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<ServiceRow> serviceRows = new ArrayList<>();
|
||||
private Grid<ServiceRow> 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<Strin
|
||||
@Override
|
||||
public void setParameter(BeforeEvent event, String parameter) {
|
||||
content.removeAll();
|
||||
serviceRows.clear();
|
||||
job = null;
|
||||
|
||||
if (parameter == null || parameter.isBlank()) {
|
||||
content.add(new Span(getTranslation("jobhistory.error.no.id")));
|
||||
@@ -80,16 +125,32 @@ public class JobManualCompleteView extends Main implements HasUrlParameter<Strin
|
||||
return;
|
||||
}
|
||||
|
||||
Job job = jobRepository.findById(jobId).orElse(null);
|
||||
if (job == null) {
|
||||
Job loaded = jobRepository.findById(jobId).orElse(null);
|
||||
if (loaded == null) {
|
||||
content.add(new Span(getTranslation("jobhistory.error.not.found", parameter)));
|
||||
return;
|
||||
}
|
||||
|
||||
render(job);
|
||||
job = loaded;
|
||||
loadVatRate();
|
||||
render();
|
||||
}
|
||||
|
||||
private void render(Job job) {
|
||||
private void loadVatRate() {
|
||||
try {
|
||||
User user = securityService.getCurrentDatabaseUser();
|
||||
if (user != null && user.getVatRate() != null) {
|
||||
vatRate = user.getVatRate();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not load user VAT rate, falling back to default: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void render() {
|
||||
manualDistanceKm = job.getRouteDistanceKm();
|
||||
manualDurationSeconds = job.getRouteDurationSeconds();
|
||||
|
||||
Span warningText = new Span(getTranslation("jobsummary.dialog.manualcomplete.text", job.getJobNumber()));
|
||||
warningText.getStyle().set("color", "var(--lumo-error-text-color)");
|
||||
|
||||
@@ -100,9 +161,16 @@ public class JobManualCompleteView extends Main implements HasUrlParameter<Strin
|
||||
|
||||
content.add(warningText, reasonField);
|
||||
|
||||
boolean hasRouteData = manualDistanceKm != null && manualDistanceKm > 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,7 +178,467 @@ public class JobManualCompleteView extends Main implements HasUrlParameter<Strin
|
||||
|
||||
Button confirmButton = new Button(getTranslation("jobsummary.dialog.manualcomplete.confirm"));
|
||||
confirmButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
|
||||
confirmButton.addClickListener(e -> {
|
||||
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<Service> availableServices = currentUser != null
|
||||
? serviceRepository.findByUserId(currentUser.getId().toString())
|
||||
: List.of();
|
||||
|
||||
ComboBox<Service> 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<Integer> stationOrders = availableDeliveryStationOrders();
|
||||
ComboBox<Integer> 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<Integer> availableDeliveryStationOrders() {
|
||||
List<Integer> 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);
|
||||
@@ -120,6 +648,29 @@ public class JobManualCompleteView extends Main implements HasUrlParameter<Strin
|
||||
|
||||
try {
|
||||
JobStatus oldStatus = job.getStatus();
|
||||
|
||||
List<JobServiceSelection> 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);
|
||||
@@ -127,10 +678,9 @@ public class JobManualCompleteView extends Main implements HasUrlParameter<Strin
|
||||
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"),
|
||||
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
|
||||
@@ -144,9 +694,5 @@ public class JobManualCompleteView extends Main implements HasUrlParameter<Strin
|
||||
Notification.Position.BOTTOM_END)
|
||||
.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
buttonBar.add(cancelButton, confirmButton);
|
||||
content.add(buttonBar);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=Заказы
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user