@@ -84,16 +84,12 @@ class MessagingPublisherImpl implements MessagingPublisher {
|
||||
return convertToTranslatedJson(dto, translations);
|
||||
}
|
||||
|
||||
if (payload instanceof List<?> list && !list.isEmpty()
|
||||
&& list.get(0) instanceof JobWithRelatedDataDTO) {
|
||||
if (payload instanceof List<?> list && !list.isEmpty() && list.get(0) instanceof JobWithRelatedDataDTO) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<JobWithRelatedDataDTO> dtoList = (List<JobWithRelatedDataDTO>) list;
|
||||
|
||||
// Collect all texts from all DTOs and translate in one batch
|
||||
List<String> allTexts = dtoList.stream()
|
||||
.flatMap(d -> collectTexts(d).stream())
|
||||
.distinct()
|
||||
.toList();
|
||||
List<String> allTexts = dtoList.stream().flatMap(d -> collectTexts(d).stream()).distinct().toList();
|
||||
Map<String, List<TranslationService.Translation>> translations = translationService
|
||||
.translateBatch(allTexts);
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
|
||||
/**
|
||||
* Embedded delivery station within a Job. Each job can have up to 25 delivery
|
||||
* stations. This is NOT a standalone MongoDB document - it is stored as part of
|
||||
* the Job document.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DeliveryStation {
|
||||
|
||||
@Field("station_order")
|
||||
private int stationOrder;
|
||||
|
||||
@Field("company")
|
||||
private String company;
|
||||
|
||||
@Field("salutation")
|
||||
private String salutation;
|
||||
|
||||
@Field("first_name")
|
||||
private String firstName;
|
||||
|
||||
@Field("last_name")
|
||||
private String lastName;
|
||||
|
||||
@Field("phone")
|
||||
private String phone;
|
||||
|
||||
@Field("street")
|
||||
private String street;
|
||||
|
||||
@Field("house_number")
|
||||
private String houseNumber;
|
||||
|
||||
@Field("address_addition")
|
||||
private String addressAddition;
|
||||
|
||||
@Field("zip")
|
||||
private String zip;
|
||||
|
||||
@Field("city")
|
||||
private String city;
|
||||
|
||||
@Field("delivery_date")
|
||||
private LocalDate deliveryDate;
|
||||
|
||||
@Field("delivery_time")
|
||||
private LocalTime deliveryTime;
|
||||
}
|
||||
@@ -12,7 +12,9 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Data
|
||||
@Document(collection = "jobs")
|
||||
@@ -158,6 +160,10 @@ public class Job {
|
||||
@Field("invoice_id")
|
||||
private String invoiceId;
|
||||
|
||||
// Lieferstationen (bis zu 25)
|
||||
@Field("delivery_stations")
|
||||
private List<DeliveryStation> deliveryStations = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Returns the ObjectId as string for JSON serialization. This ensures that the
|
||||
* job id is returned as a string when jobs are retrieved via API.
|
||||
@@ -166,4 +172,59 @@ public class Job {
|
||||
public String getIdAsString() {
|
||||
return id != null ? id.toString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first delivery station's city. Falls back to the flat
|
||||
* deliveryCity field for backward compatibility with old jobs.
|
||||
*/
|
||||
public String getFirstDeliveryCity() {
|
||||
if (deliveryStations != null && !deliveryStations.isEmpty()) {
|
||||
return deliveryStations.get(0).getCity();
|
||||
}
|
||||
return deliveryCity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last delivery station's city for route display.
|
||||
*/
|
||||
public String getLastDeliveryCity() {
|
||||
if (deliveryStations != null && !deliveryStations.isEmpty()) {
|
||||
return deliveryStations.get(deliveryStations.size() - 1).getCity();
|
||||
}
|
||||
return deliveryCity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all delivery cities joined with arrows for display (e.g. "Berlin →
|
||||
* Dresden → München").
|
||||
*/
|
||||
public String getDeliveryCitiesDisplay() {
|
||||
if (deliveryStations != null && !deliveryStations.isEmpty()) {
|
||||
return deliveryStations.stream().map(DeliveryStation::getCity).filter(c -> c != null && !c.isBlank())
|
||||
.collect(Collectors.joining(" \u2192 "));
|
||||
}
|
||||
return deliveryCity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the flat delivery fields from the first delivery station for
|
||||
* backward compatibility. Call this before saving when using delivery stations.
|
||||
*/
|
||||
public void syncFlatDeliveryFieldsFromStations() {
|
||||
if (deliveryStations != null && !deliveryStations.isEmpty()) {
|
||||
DeliveryStation first = deliveryStations.get(0);
|
||||
this.deliveryCompany = first.getCompany();
|
||||
this.deliverySalutation = first.getSalutation();
|
||||
this.deliveryFirstName = first.getFirstName();
|
||||
this.deliveryLastName = first.getLastName();
|
||||
this.deliveryPhone = first.getPhone();
|
||||
this.deliveryStreet = first.getStreet();
|
||||
this.deliveryHouseNumber = first.getHouseNumber();
|
||||
this.deliveryAddressAddition = first.getAddressAddition();
|
||||
this.deliveryZip = first.getZip();
|
||||
this.deliveryCity = first.getCity();
|
||||
this.deliveryDate = first.getDeliveryDate();
|
||||
this.deliveryTime = first.getDeliveryTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MongoDB document for caching LLM translations. Stores the original text,
|
||||
* all translations keyed by language code, and the insertion timestamp.
|
||||
* MongoDB document for caching LLM translations. Stores the original text, all
|
||||
* translations keyed by language code, and the insertion timestamp.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
package de.assecutor.votianlt.pages.base.ui.component;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
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.TextField;
|
||||
import de.assecutor.votianlt.model.Customer;
|
||||
import de.assecutor.votianlt.model.DeliveryStation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A self-contained tile for one delivery station in the AddJob form. Contains
|
||||
* all address fields, delivery date/time, and a save-address checkbox.
|
||||
*/
|
||||
public class DeliveryStationTile extends VerticalLayout {
|
||||
|
||||
public interface ChangeListener {
|
||||
void onChanged();
|
||||
}
|
||||
|
||||
public interface DeleteListener {
|
||||
void onDelete(DeliveryStationTile tile);
|
||||
}
|
||||
|
||||
private final int stationNumber;
|
||||
|
||||
private final ComboBox<String> company;
|
||||
private final ComboBox<String> salutation;
|
||||
private final TextField firstName;
|
||||
private final TextField lastName;
|
||||
private final TextField phone;
|
||||
private final TextField street;
|
||||
private final TextField houseNumber;
|
||||
private final TextField addressAddition;
|
||||
private final TextField zip;
|
||||
private final TextField city;
|
||||
private final Checkbox saveAddress;
|
||||
private final H3 title;
|
||||
|
||||
private ChangeListener changeListener;
|
||||
private DeleteListener deleteListener;
|
||||
|
||||
public DeliveryStationTile(int stationNumber, boolean removable, List<Customer> customers,
|
||||
TranslationHelper translationHelper) {
|
||||
this.stationNumber = stationNumber;
|
||||
|
||||
setSpacing(true);
|
||||
setPadding(true);
|
||||
setWidth("40%");
|
||||
getStyle().set("min-width", "300px");
|
||||
getStyle().set("flex-shrink", "0");
|
||||
getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
|
||||
getStyle().set("border-radius", "var(--lumo-border-radius-m)");
|
||||
getStyle().set("background-color", "var(--lumo-base-color)");
|
||||
|
||||
// Header
|
||||
title = new H3(translationHelper.getTranslation("addjob.station.delivery", stationNumber));
|
||||
title.getStyle().set("margin", "0");
|
||||
|
||||
HorizontalLayout titleLayout = new HorizontalLayout();
|
||||
titleLayout.setWidthFull();
|
||||
titleLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
|
||||
titleLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
titleLayout.add(title);
|
||||
|
||||
Button deleteButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL));
|
||||
deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
|
||||
if (removable) {
|
||||
deleteButton.addClickListener(e -> {
|
||||
if (deleteListener != null) {
|
||||
deleteListener.onDelete(this);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
deleteButton.getStyle().set("visibility", "hidden");
|
||||
}
|
||||
titleLayout.add(deleteButton);
|
||||
|
||||
add(titleLayout);
|
||||
|
||||
// Company with autocomplete
|
||||
company = new ComboBox<>(translationHelper.getTranslation("profile.company"));
|
||||
company.setPlaceholder(translationHelper.getTranslation("addjob.address.company.placeholder"));
|
||||
company.setAllowCustomValue(true);
|
||||
company.setWidthFull();
|
||||
setupCompanyAutocomplete(company, customers, translationHelper);
|
||||
add(company);
|
||||
|
||||
// Salutation
|
||||
salutation = new ComboBox<>(translationHelper.getTranslation("addjob.address.salutation"));
|
||||
salutation.setItems(translationHelper.getTranslation("addjob.salutation.mr"),
|
||||
translationHelper.getTranslation("addjob.salutation.ms"),
|
||||
translationHelper.getTranslation("addjob.salutation.other"));
|
||||
salutation.setPlaceholder(translationHelper.getTranslation("addjob.address.salutation.placeholder"));
|
||||
salutation.setWidthFull();
|
||||
add(salutation);
|
||||
|
||||
// Name fields
|
||||
firstName = new TextField(translationHelper.getTranslation("profile.firstname"));
|
||||
firstName.setPlaceholder(translationHelper.getTranslation("profile.firstname"));
|
||||
firstName.setRequiredIndicatorVisible(true);
|
||||
firstName.setWidthFull();
|
||||
add(firstName);
|
||||
|
||||
lastName = new TextField(translationHelper.getTranslation("profile.lastname"));
|
||||
lastName.setPlaceholder(translationHelper.getTranslation("profile.lastname"));
|
||||
lastName.setRequiredIndicatorVisible(true);
|
||||
lastName.setWidthFull();
|
||||
add(lastName);
|
||||
|
||||
// Phone
|
||||
phone = new TextField(translationHelper.getTranslation("profile.phone"));
|
||||
phone.setPlaceholder(translationHelper.getTranslation("profile.phone"));
|
||||
phone.setWidthFull();
|
||||
add(phone);
|
||||
|
||||
// Street + house number
|
||||
street = new TextField(translationHelper.getTranslation("profile.street"));
|
||||
street.setPlaceholder(translationHelper.getTranslation("addjob.address.delivery.street.placeholder"));
|
||||
street.setRequiredIndicatorVisible(true);
|
||||
|
||||
houseNumber = new TextField(translationHelper.getTranslation("profile.housenr"));
|
||||
houseNumber.setPlaceholder(translationHelper.getTranslation("addjob.address.housenumber"));
|
||||
houseNumber.setRequiredIndicatorVisible(true);
|
||||
|
||||
HorizontalLayout streetLayout = new HorizontalLayout();
|
||||
streetLayout.setWidthFull();
|
||||
streetLayout.setSpacing(true);
|
||||
street.setWidth("70%");
|
||||
houseNumber.setWidth("30%");
|
||||
streetLayout.add(street, houseNumber);
|
||||
add(streetLayout);
|
||||
|
||||
// Address addition
|
||||
addressAddition = new TextField(translationHelper.getTranslation("profile.addressadd"));
|
||||
addressAddition
|
||||
.setPlaceholder(translationHelper.getTranslation("addjob.address.delivery.addition.placeholder"));
|
||||
addressAddition.setWidthFull();
|
||||
add(addressAddition);
|
||||
|
||||
// Zip + city
|
||||
zip = new TextField(translationHelper.getTranslation("profile.zip"));
|
||||
zip.setPlaceholder(translationHelper.getTranslation("profile.zip"));
|
||||
zip.setRequiredIndicatorVisible(true);
|
||||
|
||||
city = new TextField(translationHelper.getTranslation("addjob.address.city"));
|
||||
city.setPlaceholder(translationHelper.getTranslation("addjob.address.city.placeholder.delivery"));
|
||||
city.setRequiredIndicatorVisible(true);
|
||||
|
||||
HorizontalLayout zipCityLayout = new HorizontalLayout();
|
||||
zipCityLayout.setWidthFull();
|
||||
zipCityLayout.setSpacing(true);
|
||||
zip.setWidth("30%");
|
||||
city.setWidth("70%");
|
||||
zipCityLayout.add(zip, city);
|
||||
add(zipCityLayout);
|
||||
|
||||
// Save address checkbox
|
||||
saveAddress = new Checkbox(translationHelper.getTranslation("addjob.address.save"));
|
||||
saveAddress.setValue(true);
|
||||
saveAddress.setWidthFull();
|
||||
add(saveAddress);
|
||||
|
||||
// Register change listeners on all fields
|
||||
setupChangeListeners();
|
||||
}
|
||||
|
||||
private void setupChangeListeners() {
|
||||
TextField[] textFields = { firstName, lastName, street, houseNumber, zip, city, phone, addressAddition };
|
||||
for (TextField field : textFields) {
|
||||
field.addValueChangeListener(e -> {
|
||||
updateFieldStyling(field);
|
||||
fireChanged();
|
||||
});
|
||||
}
|
||||
|
||||
company.addValueChangeListener(e -> fireChanged());
|
||||
salutation.addValueChangeListener(e -> fireChanged());
|
||||
}
|
||||
|
||||
private void fireChanged() {
|
||||
if (changeListener != null) {
|
||||
changeListener.onChanged();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
companyField.setItems(companyNames);
|
||||
|
||||
companyField.addValueChangeListener(event -> {
|
||||
String selectedCompany = event.getValue();
|
||||
if (selectedCompany == null || selectedCompany.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<Customer> matchingCustomer = customers.stream()
|
||||
.filter(c -> selectedCompany.equals(c.getCompanyName())).findFirst();
|
||||
|
||||
if (matchingCustomer.isPresent()) {
|
||||
Customer customer = matchingCustomer.get();
|
||||
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.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());
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
companyField.addCustomValueSetListener(event -> {
|
||||
companyField.setValue(event.getDetail());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the station number displayed in the title.
|
||||
*/
|
||||
public void updateStationNumber(int newNumber) {
|
||||
title.setText(getTranslation("addjob.station.delivery", newNumber));
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all field values into a DeliveryStation object.
|
||||
*/
|
||||
public DeliveryStation getDeliveryStation() {
|
||||
DeliveryStation station = new DeliveryStation();
|
||||
station.setCompany(company.getValue());
|
||||
station.setSalutation(salutation.getValue());
|
||||
station.setFirstName(firstName.getValue());
|
||||
station.setLastName(lastName.getValue());
|
||||
station.setPhone(phone.getValue());
|
||||
station.setStreet(street.getValue());
|
||||
station.setHouseNumber(houseNumber.getValue());
|
||||
station.setAddressAddition(addressAddition.getValue());
|
||||
station.setZip(zip.getValue());
|
||||
station.setCity(city.getValue());
|
||||
return station;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the tile fields from an existing DeliveryStation.
|
||||
*/
|
||||
public void setDeliveryStation(DeliveryStation station) {
|
||||
if (station == null)
|
||||
return;
|
||||
if (station.getCompany() != null)
|
||||
company.setValue(station.getCompany());
|
||||
if (station.getSalutation() != null)
|
||||
salutation.setValue(station.getSalutation());
|
||||
if (station.getFirstName() != null)
|
||||
firstName.setValue(station.getFirstName());
|
||||
if (station.getLastName() != null)
|
||||
lastName.setValue(station.getLastName());
|
||||
if (station.getPhone() != null)
|
||||
phone.setValue(station.getPhone());
|
||||
if (station.getStreet() != null)
|
||||
street.setValue(station.getStreet());
|
||||
if (station.getHouseNumber() != null)
|
||||
houseNumber.setValue(station.getHouseNumber());
|
||||
if (station.getAddressAddition() != null)
|
||||
addressAddition.setValue(station.getAddressAddition());
|
||||
if (station.getZip() != null)
|
||||
zip.setValue(station.getZip());
|
||||
if (station.getCity() != null)
|
||||
city.setValue(station.getCity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all required address fields are filled.
|
||||
*/
|
||||
public boolean hasValidationErrors() {
|
||||
return isFieldEmpty(firstName) || isFieldEmpty(lastName) || isFieldEmpty(street) || isFieldEmpty(houseNumber)
|
||||
|| isFieldEmpty(zip) || isFieldEmpty(city);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies error styling to empty required fields.
|
||||
*/
|
||||
public void highlightErrors() {
|
||||
TextField[] required = { firstName, lastName, street, houseNumber, zip, city };
|
||||
for (TextField field : required) {
|
||||
updateFieldStyling(field);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all fields in this tile.
|
||||
*/
|
||||
public void clearFields() {
|
||||
company.clear();
|
||||
salutation.clear();
|
||||
firstName.clear();
|
||||
lastName.clear();
|
||||
phone.clear();
|
||||
street.clear();
|
||||
houseNumber.clear();
|
||||
addressAddition.clear();
|
||||
zip.clear();
|
||||
city.clear();
|
||||
saveAddress.setValue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the street value for address validation.
|
||||
*/
|
||||
public String getStreetValue() {
|
||||
return getValueOrEmpty(street);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the house number value for address validation.
|
||||
*/
|
||||
public String getHouseNumberValue() {
|
||||
return getValueOrEmpty(houseNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the zip value for address validation.
|
||||
*/
|
||||
public String getZipValue() {
|
||||
return getValueOrEmpty(zip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the city value for address validation.
|
||||
*/
|
||||
public String getCityValue() {
|
||||
return getValueOrEmpty(city);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the delivery address has enough data for validation.
|
||||
*/
|
||||
public boolean hasAddressForValidation() {
|
||||
return !getStreetValue().isEmpty() && !getZipValue().isEmpty() && !getCityValue().isEmpty();
|
||||
}
|
||||
|
||||
public void setChangeListener(ChangeListener listener) {
|
||||
this.changeListener = listener;
|
||||
}
|
||||
|
||||
public void setDeleteListener(DeleteListener listener) {
|
||||
this.deleteListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the user wants to save this address as a customer.
|
||||
*/
|
||||
public boolean isSaveAddressChecked() {
|
||||
return saveAddress.getValue();
|
||||
}
|
||||
|
||||
public int getStationNumber() {
|
||||
return stationNumber;
|
||||
}
|
||||
|
||||
private boolean isFieldEmpty(TextField field) {
|
||||
String value = field.getValue();
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
|
||||
private String getValueOrEmpty(TextField field) {
|
||||
return field.getValue() != null ? field.getValue().trim() : "";
|
||||
}
|
||||
|
||||
private void updateFieldStyling(TextField field) {
|
||||
boolean isEmpty = isFieldEmpty(field);
|
||||
if (isEmpty && field.isRequiredIndicatorVisible()) {
|
||||
field.getStyle().set("--vaadin-input-field-background", "rgba(255, 0, 0, 0.1)");
|
||||
field.getStyle().set("--vaadin-input-field-border-color", "rgba(255, 0, 0, 0.3)");
|
||||
} else {
|
||||
field.getStyle().remove("--vaadin-input-field-background");
|
||||
field.getStyle().remove("--vaadin-input-field-border-color");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional interface for accessing translations from the parent view.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TranslationHelper {
|
||||
String getTranslation(String key, Object... params);
|
||||
}
|
||||
}
|
||||
@@ -61,8 +61,10 @@ import java.math.RoundingMode;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import de.assecutor.votianlt.model.CargoItem;
|
||||
import de.assecutor.votianlt.model.DeliveryStation;
|
||||
import de.assecutor.votianlt.model.AddressValidationResult;
|
||||
import de.assecutor.votianlt.model.RouteCalculationResult;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.DeliveryStationTile;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.Objects;
|
||||
@@ -104,18 +106,11 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
private TextField pickupCity;
|
||||
private Checkbox savePickupAddress;
|
||||
|
||||
// Delivery address fields
|
||||
private ComboBox<String> deliveryCompany;
|
||||
private ComboBox<String> deliverySalutation;
|
||||
private TextField deliveryFirstName;
|
||||
private TextField deliveryLastName;
|
||||
private TextField deliveryPhone;
|
||||
private TextField deliveryStreet;
|
||||
private TextField deliveryHouseNumber;
|
||||
private TextField deliveryAddressAddition;
|
||||
private TextField deliveryZip;
|
||||
private TextField deliveryCity;
|
||||
private Checkbox saveDeliveryAddress;
|
||||
// Delivery station tiles (up to 25)
|
||||
private final List<DeliveryStationTile> deliveryStationTiles = new ArrayList<>();
|
||||
private Div stationsScrollContainer;
|
||||
private Div addStationButton;
|
||||
private static final int MAX_DELIVERY_STATIONS = 25;
|
||||
|
||||
// Digital processing
|
||||
private Checkbox digitalProcessing;
|
||||
@@ -140,11 +135,9 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
|
||||
// Date picker fields for appointments
|
||||
private DatePicker pickupDate;
|
||||
private DatePicker deliveryDate;
|
||||
|
||||
// Time picker fields for appointments
|
||||
private TimePicker pickupTime;
|
||||
private TimePicker deliveryTime;
|
||||
|
||||
private com.vaadin.flow.component.tabs.Tab addressesTab;
|
||||
private com.vaadin.flow.component.tabs.Tab appointmentsTab;
|
||||
@@ -169,7 +162,6 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
private ComboBox<TaskTemplate> templateComboBox;
|
||||
private TextArea remarkArea;
|
||||
private VerticalLayout pickupSection;
|
||||
private VerticalLayout deliverySection;
|
||||
|
||||
private final Binder<Job> binder = new Binder<>(Job.class);
|
||||
|
||||
@@ -349,39 +341,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
savePickupAddress = new Checkbox(getTranslation("addjob.address.save"));
|
||||
savePickupAddress.setValue(true);
|
||||
|
||||
// Delivery address
|
||||
deliveryCompany = new ComboBox<>(getTranslation("profile.company"));
|
||||
deliveryCompany.setPlaceholder(getTranslation("addjob.address.company.placeholder"));
|
||||
deliveryCompany.setAllowCustomValue(true);
|
||||
setupCompanyAutocomplete(deliveryCompany, false); // false für Delivery
|
||||
deliverySalutation = new ComboBox<>(getTranslation("addjob.address.salutation"));
|
||||
deliverySalutation.setItems(getTranslation("addjob.salutation.mr"), getTranslation("addjob.salutation.ms"),
|
||||
getTranslation("addjob.salutation.other"));
|
||||
deliverySalutation.setPlaceholder(getTranslation("addjob.address.salutation.placeholder"));
|
||||
deliveryFirstName = new TextField(getTranslation("profile.firstname"));
|
||||
deliveryFirstName.setPlaceholder(getTranslation("profile.firstname"));
|
||||
deliveryFirstName.setRequiredIndicatorVisible(true);
|
||||
deliveryLastName = new TextField(getTranslation("profile.lastname"));
|
||||
deliveryLastName.setPlaceholder(getTranslation("profile.lastname"));
|
||||
deliveryLastName.setRequiredIndicatorVisible(true);
|
||||
deliveryPhone = new TextField(getTranslation("profile.phone"));
|
||||
deliveryPhone.setPlaceholder(getTranslation("profile.phone"));
|
||||
deliveryStreet = new TextField(getTranslation("profile.street"));
|
||||
deliveryStreet.setPlaceholder(getTranslation("addjob.address.delivery.street.placeholder"));
|
||||
deliveryStreet.setRequiredIndicatorVisible(true);
|
||||
deliveryHouseNumber = new TextField(getTranslation("profile.housenr"));
|
||||
deliveryHouseNumber.setPlaceholder(getTranslation("addjob.address.housenumber"));
|
||||
deliveryHouseNumber.setRequiredIndicatorVisible(true);
|
||||
deliveryAddressAddition = new TextField(getTranslation("profile.addressadd"));
|
||||
deliveryAddressAddition.setPlaceholder(getTranslation("addjob.address.delivery.addition.placeholder"));
|
||||
deliveryZip = new TextField(getTranslation("profile.zip"));
|
||||
deliveryZip.setPlaceholder(getTranslation("profile.zip"));
|
||||
deliveryZip.setRequiredIndicatorVisible(true);
|
||||
deliveryCity = new TextField(getTranslation("addjob.address.city"));
|
||||
deliveryCity.setPlaceholder(getTranslation("addjob.address.city.placeholder.delivery"));
|
||||
deliveryCity.setRequiredIndicatorVisible(true);
|
||||
saveDeliveryAddress = new Checkbox(getTranslation("addjob.address.save"));
|
||||
saveDeliveryAddress.setValue(true);
|
||||
// Delivery station tiles will be created in createCustomerAndAddressesTab()
|
||||
|
||||
// Digital processing - set value based on user's profile setting
|
||||
digitalProcessing = new Checkbox(getTranslation("profile.settings.digitalprocess"));
|
||||
@@ -414,17 +374,6 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
"Freitag", "Samstag"))
|
||||
.setWeekdaysShort(java.util.Arrays.asList("So", "Mo", "Di", "Mi", "Do", "Fr", "Sa")));
|
||||
|
||||
deliveryDate = new DatePicker(getTranslation("addjob.appointment.date"));
|
||||
deliveryDate.setRequiredIndicatorVisible(true);
|
||||
deliveryDate.setMin(LocalDate.now());
|
||||
deliveryDate.setLocale(java.util.Locale.GERMANY); // Monday as first day of week
|
||||
deliveryDate.setI18n(new DatePicker.DatePickerI18n().setFirstDayOfWeek(1) // 1 = Monday
|
||||
.setMonthNames(java.util.Arrays.asList("Januar", "Februar", "März", "April", "Mai", "Juni", "Juli",
|
||||
"August", "September", "Oktober", "November", "Dezember"))
|
||||
.setWeekdays(java.util.Arrays.asList("Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag",
|
||||
"Freitag", "Samstag"))
|
||||
.setWeekdaysShort(java.util.Arrays.asList("So", "Mo", "Di", "Mi", "Do", "Fr", "Sa")));
|
||||
|
||||
// Submit button - initially disabled until all required fields are valid
|
||||
submitButton = new Button(getTranslation("addjob.button.submit"), event -> submit());
|
||||
submitButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
@@ -493,6 +442,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
tabContent.setSizeFull();
|
||||
tabContent.setPadding(true);
|
||||
tabContent.setSpacing(true);
|
||||
tabContent.getStyle().set("overflow-y", "auto");
|
||||
|
||||
// Customer selection section
|
||||
HorizontalLayout customerLayout = new HorizontalLayout();
|
||||
@@ -504,29 +454,129 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
|
||||
tabContent.add(customerLayout);
|
||||
|
||||
// Main content layout with two equal columns (50% each)
|
||||
HorizontalLayout mainLayout = new HorizontalLayout();
|
||||
mainLayout.setWidthFull();
|
||||
mainLayout.setSpacing(true);
|
||||
mainLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.START);
|
||||
// Horizontal scrolling container for pickup + delivery station tiles
|
||||
stationsScrollContainer = new Div();
|
||||
stationsScrollContainer.getStyle().set("display", "flex");
|
||||
stationsScrollContainer.getStyle().set("overflow-x", "auto");
|
||||
stationsScrollContainer.getStyle().set("gap", "var(--lumo-space-m)");
|
||||
stationsScrollContainer.getStyle().set("padding", "var(--lumo-space-s)");
|
||||
stationsScrollContainer.getStyle().set("padding-bottom", "20px");
|
||||
stationsScrollContainer.getStyle().set("align-items", "flex-start");
|
||||
stationsScrollContainer.getStyle().set("flex-shrink", "0");
|
||||
stationsScrollContainer.setWidthFull();
|
||||
|
||||
// Left column (50%) - Pickup address section
|
||||
// Pickup section tile (always present)
|
||||
pickupSection = createPickupSection();
|
||||
pickupSection.setWidth("50%");
|
||||
pickupSection.setWidth("40%");
|
||||
pickupSection.getStyle().set("min-width", "300px");
|
||||
pickupSection.getStyle().set("flex-shrink", "0");
|
||||
stationsScrollContainer.add(pickupSection);
|
||||
|
||||
// Right column (50%) - Delivery address section
|
||||
deliverySection = createDeliverySection();
|
||||
deliverySection.setWidth("50%");
|
||||
// "+" add station button tile
|
||||
addStationButton = createAddStationButton();
|
||||
stationsScrollContainer.add(addStationButton);
|
||||
|
||||
// Setup focus listeners for input fields
|
||||
// Add first delivery station tile
|
||||
addDeliveryStationTile();
|
||||
|
||||
// Setup focus listeners for pickup input fields
|
||||
setupInputFieldFocusListeners();
|
||||
|
||||
mainLayout.add(pickupSection, deliverySection);
|
||||
tabContent.add(mainLayout);
|
||||
tabContent.add(stationsScrollContainer);
|
||||
|
||||
return tabContent;
|
||||
}
|
||||
|
||||
private Div createAddStationButton() {
|
||||
Div button = new Div();
|
||||
button.getStyle().set("min-width", "300px");
|
||||
button.getStyle().set("width", "40%");
|
||||
button.getStyle().set("min-height", "200px");
|
||||
button.getStyle().set("flex-shrink", "0");
|
||||
button.getStyle().set("border", "2px dashed var(--lumo-contrast-30pct)");
|
||||
button.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
|
||||
button.getStyle().set("display", "flex");
|
||||
button.getStyle().set("align-items", "center");
|
||||
button.getStyle().set("justify-content", "center");
|
||||
button.getStyle().set("cursor", "pointer");
|
||||
button.getStyle().set("background-color", "var(--lumo-contrast-5pct)");
|
||||
button.getStyle().set("transition", "background-color 0.2s");
|
||||
|
||||
Icon plusIcon = new Icon(VaadinIcon.PLUS);
|
||||
plusIcon.setSize("64px");
|
||||
plusIcon.getStyle().set("color", "var(--lumo-contrast-40pct)");
|
||||
button.add(plusIcon);
|
||||
|
||||
button.addClickListener(e -> {
|
||||
if (deliveryStationTiles.size() >= MAX_DELIVERY_STATIONS) {
|
||||
Notification.show(getTranslation("addjob.station.max.reached"), 3000,
|
||||
Notification.Position.BOTTOM_CENTER);
|
||||
return;
|
||||
}
|
||||
addDeliveryStationTile();
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private void addDeliveryStationTile() {
|
||||
int stationNumber = deliveryStationTiles.size() + 1;
|
||||
boolean removable = deliveryStationTiles.size() > 0; // First station is not removable
|
||||
|
||||
List<Customer> customers = customerService.findAllForCurrentOwner();
|
||||
DeliveryStationTile.TranslationHelper translationHelper = this::getTranslation;
|
||||
|
||||
DeliveryStationTile tile = new DeliveryStationTile(stationNumber, removable, customers, translationHelper);
|
||||
tile.setChangeListener(() -> {
|
||||
resetRouteInformation();
|
||||
triggerValidation();
|
||||
updateTabLabels();
|
||||
});
|
||||
tile.setDeleteListener(this::removeDeliveryStationTile);
|
||||
|
||||
deliveryStationTiles.add(tile);
|
||||
|
||||
// Insert tile before the "+" button
|
||||
stationsScrollContainer.remove(addStationButton);
|
||||
stationsScrollContainer.add(tile);
|
||||
|
||||
// Hide "+" button if max reached
|
||||
if (deliveryStationTiles.size() < MAX_DELIVERY_STATIONS) {
|
||||
stationsScrollContainer.add(addStationButton);
|
||||
}
|
||||
|
||||
triggerValidation();
|
||||
updateTabLabels();
|
||||
}
|
||||
|
||||
private void removeDeliveryStationTile(DeliveryStationTile tile) {
|
||||
ConfirmDialog dialog = new ConfirmDialog();
|
||||
int idx = deliveryStationTiles.indexOf(tile) + 1;
|
||||
dialog.setHeader(getTranslation("addjob.station.remove.confirm", idx));
|
||||
dialog.setCancelable(true);
|
||||
dialog.setCancelText(getTranslation("dialog.cancel"));
|
||||
dialog.setConfirmText(getTranslation("dialog.confirm"));
|
||||
dialog.addConfirmListener(e -> {
|
||||
deliveryStationTiles.remove(tile);
|
||||
stationsScrollContainer.remove(tile);
|
||||
|
||||
// Renumber remaining tiles
|
||||
for (int i = 0; i < deliveryStationTiles.size(); i++) {
|
||||
deliveryStationTiles.get(i).updateStationNumber(i + 1);
|
||||
}
|
||||
|
||||
// Ensure "+" button is visible if under max
|
||||
if (deliveryStationTiles.size() < MAX_DELIVERY_STATIONS && addStationButton.getParent().isEmpty()) {
|
||||
stationsScrollContainer.add(addStationButton);
|
||||
}
|
||||
|
||||
resetRouteInformation();
|
||||
triggerValidation();
|
||||
updateTabLabels();
|
||||
});
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
private Component createAppointmentsAndProcessingTab() {
|
||||
VerticalLayout tabContent = new VerticalLayout();
|
||||
tabContent.setSizeFull();
|
||||
@@ -565,17 +615,12 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
pickupTime.setWidth("50%");
|
||||
content.add(pickupApptTitle, pickupApptRow);
|
||||
|
||||
// Appointment (Delivery)
|
||||
H3 deliveryApptTitle = new H3(getTranslation("addjob.appointment.delivery"));
|
||||
deliveryApptTitle.getStyle().set("margin", "0");
|
||||
deliveryTime = new TimePicker(getTranslation("addjob.appointment.time"));
|
||||
deliveryTime.setLocale(java.util.Locale.GERMANY);
|
||||
HorizontalLayout deliveryApptRow = new HorizontalLayout(deliveryDate, deliveryTime);
|
||||
deliveryApptRow.setWidthFull();
|
||||
deliveryApptRow.setSpacing(true);
|
||||
deliveryDate.setWidth("50%");
|
||||
deliveryTime.setWidth("50%");
|
||||
content.add(deliveryApptTitle, deliveryApptRow);
|
||||
// Info: Delivery dates are set per-station in the tiles
|
||||
Span deliveryInfoLabel = new Span(getTranslation("addjob.appointment.delivery.info"));
|
||||
deliveryInfoLabel.getStyle().set("color", "var(--lumo-secondary-text-color)");
|
||||
deliveryInfoLabel.getStyle().set("font-style", "italic");
|
||||
deliveryInfoLabel.getStyle().set("margin-top", "var(--lumo-space-m)");
|
||||
content.add(deliveryInfoLabel);
|
||||
|
||||
tabContent.add(content);
|
||||
return tabContent;
|
||||
@@ -974,10 +1019,16 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
|
||||
HorizontalLayout titleLayout = new HorizontalLayout();
|
||||
titleLayout.setWidthFull();
|
||||
titleLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.START);
|
||||
titleLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
|
||||
titleLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
titleLayout.add(title);
|
||||
|
||||
// Invisible placeholder button to match DeliveryStationTile header height
|
||||
Button placeholder = new Button(new Icon(VaadinIcon.CLOSE_SMALL));
|
||||
placeholder.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
placeholder.getStyle().set("visibility", "hidden");
|
||||
titleLayout.add(placeholder);
|
||||
|
||||
// Alle einzelnen Controls auf volle Breite setzen
|
||||
pickupCompany.setWidthFull();
|
||||
pickupSalutation.setWidthFull();
|
||||
@@ -1018,64 +1069,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
return section;
|
||||
}
|
||||
|
||||
private VerticalLayout createDeliverySection() {
|
||||
VerticalLayout section = new VerticalLayout();
|
||||
section.setSpacing(true);
|
||||
section.setPadding(true);
|
||||
section.setWidthFull();
|
||||
|
||||
// Hellgrauer Rahmen hinzufügen
|
||||
section.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
|
||||
section.getStyle().set("border-radius", "var(--lumo-border-radius-m)");
|
||||
section.getStyle().set("background-color", "var(--lumo-base-color)");
|
||||
|
||||
H3 title = new H3(getTranslation("addjob.section.delivery"));
|
||||
title.getStyle().set("margin", "0");
|
||||
|
||||
HorizontalLayout titleLayout = new HorizontalLayout();
|
||||
titleLayout.setWidthFull();
|
||||
titleLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.START);
|
||||
titleLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
titleLayout.add(title);
|
||||
|
||||
// Alle einzelnen Controls auf volle Breite setzen
|
||||
deliveryCompany.setWidthFull();
|
||||
deliverySalutation.setWidthFull();
|
||||
deliveryFirstName.setWidthFull();
|
||||
deliveryLastName.setWidthFull();
|
||||
deliveryPhone.setWidthFull();
|
||||
deliveryAddressAddition.setWidthFull();
|
||||
saveDeliveryAddress.setWidthFull();
|
||||
|
||||
section.add(titleLayout);
|
||||
section.add(deliveryCompany);
|
||||
section.add(deliverySalutation);
|
||||
section.add(deliveryFirstName);
|
||||
section.add(deliveryLastName);
|
||||
section.add(deliveryPhone);
|
||||
|
||||
HorizontalLayout streetLayout = new HorizontalLayout();
|
||||
streetLayout.setWidthFull();
|
||||
streetLayout.setSpacing(true);
|
||||
streetLayout.add(deliveryStreet, deliveryHouseNumber);
|
||||
deliveryStreet.setWidth("70%");
|
||||
deliveryHouseNumber.setWidth("30%");
|
||||
section.add(streetLayout);
|
||||
|
||||
section.add(deliveryAddressAddition);
|
||||
|
||||
HorizontalLayout zipCityLayout = new HorizontalLayout();
|
||||
zipCityLayout.setWidthFull();
|
||||
zipCityLayout.setSpacing(true);
|
||||
zipCityLayout.add(deliveryZip, deliveryCity);
|
||||
deliveryZip.setWidth("30%");
|
||||
deliveryCity.setWidth("70%");
|
||||
section.add(zipCityLayout);
|
||||
|
||||
section.add(saveDeliveryAddress);
|
||||
|
||||
return section;
|
||||
}
|
||||
// createDeliverySection() removed - delivery stations are now handled by
|
||||
// DeliveryStationTile
|
||||
|
||||
private void setupCompanyAutocomplete(ComboBox<String> companyField, boolean isPickup) {
|
||||
// Get all customers for the current owner
|
||||
@@ -1088,7 +1083,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
// Set items for autocomplete
|
||||
companyField.setItems(companyNames);
|
||||
|
||||
// Add selection listener to auto-fill address fields when company is selected
|
||||
// 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()) {
|
||||
@@ -1105,59 +1101,31 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
if (matchingCustomer.isPresent()) {
|
||||
Customer customer = matchingCustomer.get();
|
||||
|
||||
if (isPickup) {
|
||||
// Fill pickup address fields
|
||||
if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle())
|
||||
|| "Frau".equalsIgnoreCase(customer.getTitle())
|
||||
|| "Divers".equalsIgnoreCase(customer.getTitle()))) {
|
||||
pickupSalutation.setValue(customer.getTitle());
|
||||
}
|
||||
if (customer.getFirstname() != null)
|
||||
pickupFirstName.setValue(customer.getFirstname());
|
||||
if (customer.getLastName() != null)
|
||||
pickupLastName.setValue(customer.getLastName());
|
||||
if (customer.getTelephone() != null)
|
||||
pickupPhone.setValue(customer.getTelephone());
|
||||
if (customer.getStreet() != null)
|
||||
pickupStreet.setValue(customer.getStreet());
|
||||
if (customer.getHouseNumber() != null)
|
||||
pickupHouseNumber.setValue(customer.getHouseNumber());
|
||||
if (customer.getAddressAddition() != null)
|
||||
pickupAddressAddition.setValue(customer.getAddressAddition());
|
||||
if (customer.getZip() != null)
|
||||
pickupZip.setValue(customer.getZip());
|
||||
if (customer.getCity() != null)
|
||||
pickupCity.setValue(customer.getCity());
|
||||
|
||||
// Deactivate save checkbox since customer already exists
|
||||
savePickupAddress.setValue(false);
|
||||
} else {
|
||||
// Fill delivery address fields
|
||||
if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle())
|
||||
|| "Frau".equalsIgnoreCase(customer.getTitle())
|
||||
|| "Divers".equalsIgnoreCase(customer.getTitle()))) {
|
||||
deliverySalutation.setValue(customer.getTitle());
|
||||
}
|
||||
if (customer.getFirstname() != null)
|
||||
deliveryFirstName.setValue(customer.getFirstname());
|
||||
if (customer.getLastName() != null)
|
||||
deliveryLastName.setValue(customer.getLastName());
|
||||
if (customer.getTelephone() != null)
|
||||
deliveryPhone.setValue(customer.getTelephone());
|
||||
if (customer.getStreet() != null)
|
||||
deliveryStreet.setValue(customer.getStreet());
|
||||
if (customer.getHouseNumber() != null)
|
||||
deliveryHouseNumber.setValue(customer.getHouseNumber());
|
||||
if (customer.getAddressAddition() != null)
|
||||
deliveryAddressAddition.setValue(customer.getAddressAddition());
|
||||
if (customer.getZip() != null)
|
||||
deliveryZip.setValue(customer.getZip());
|
||||
if (customer.getCity() != null)
|
||||
deliveryCity.setValue(customer.getCity());
|
||||
|
||||
// Deactivate save checkbox since customer already exists
|
||||
saveDeliveryAddress.setValue(false);
|
||||
// Fill pickup address fields
|
||||
if (customer.getTitle() != null
|
||||
&& ("Herr".equalsIgnoreCase(customer.getTitle()) || "Frau".equalsIgnoreCase(customer.getTitle())
|
||||
|| "Divers".equalsIgnoreCase(customer.getTitle()))) {
|
||||
pickupSalutation.setValue(customer.getTitle());
|
||||
}
|
||||
if (customer.getFirstname() != null)
|
||||
pickupFirstName.setValue(customer.getFirstname());
|
||||
if (customer.getLastName() != null)
|
||||
pickupLastName.setValue(customer.getLastName());
|
||||
if (customer.getTelephone() != null)
|
||||
pickupPhone.setValue(customer.getTelephone());
|
||||
if (customer.getStreet() != null)
|
||||
pickupStreet.setValue(customer.getStreet());
|
||||
if (customer.getHouseNumber() != null)
|
||||
pickupHouseNumber.setValue(customer.getHouseNumber());
|
||||
if (customer.getAddressAddition() != null)
|
||||
pickupAddressAddition.setValue(customer.getAddressAddition());
|
||||
if (customer.getZip() != null)
|
||||
pickupZip.setValue(customer.getZip());
|
||||
if (customer.getCity() != null)
|
||||
pickupCity.setValue(customer.getCity());
|
||||
|
||||
// Deactivate save checkbox since customer already exists
|
||||
savePickupAddress.setValue(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1170,11 +1138,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
resetRouteInformation();
|
||||
|
||||
// Reactivate save checkbox for custom values
|
||||
if (isPickup) {
|
||||
savePickupAddress.setValue(true);
|
||||
} else {
|
||||
saveDeliveryAddress.setValue(true);
|
||||
}
|
||||
savePickupAddress.setValue(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1192,51 +1156,30 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
|
||||
binder.forField(pickupCity).asRequired("").bind(Job::getPickupCity, Job::setPickupCity);
|
||||
|
||||
// Bind delivery address fields with validation
|
||||
binder.forField(deliveryFirstName).asRequired("").bind(Job::getDeliveryFirstName, Job::setDeliveryFirstName);
|
||||
|
||||
binder.forField(deliveryLastName).asRequired("").bind(Job::getDeliveryLastName, Job::setDeliveryLastName);
|
||||
|
||||
binder.forField(deliveryStreet).asRequired("").bind(Job::getDeliveryStreet, Job::setDeliveryStreet);
|
||||
|
||||
binder.forField(deliveryHouseNumber).asRequired("").bind(Job::getDeliveryHouseNumber,
|
||||
Job::setDeliveryHouseNumber);
|
||||
|
||||
binder.forField(deliveryZip).asRequired("").bind(Job::getDeliveryZip, Job::setDeliveryZip);
|
||||
|
||||
binder.forField(deliveryCity).asRequired("").bind(Job::getDeliveryCity, Job::setDeliveryCity);
|
||||
// Delivery address fields are managed by DeliveryStationTile (not binder)
|
||||
|
||||
// Price wird manuell in submit() berechnet und gesetzt - kein Binder notwendig
|
||||
|
||||
// Bind date picker fields with validation
|
||||
// Bind pickup date field with validation
|
||||
binder.forField(pickupDate).asRequired("")
|
||||
.withValidator(date -> date == null || !date.isBefore(LocalDate.now()),
|
||||
getTranslation("addjob.validation.pickupdate.future"))
|
||||
.bind(Job::getPickupDate, Job::setPickupDate);
|
||||
|
||||
binder.forField(deliveryDate).asRequired("")
|
||||
.withValidator(date -> date == null || !date.isBefore(LocalDate.now()),
|
||||
getTranslation("addjob.validation.deliverydate.future"))
|
||||
.bind(Job::getDeliveryDate, Job::setDeliveryDate);
|
||||
// Delivery dates are now per-station (in DeliveryStationTile)
|
||||
|
||||
// Bind time picker fields (optional)
|
||||
binder.bind(pickupTime, Job::getPickupTime, Job::setPickupTime);
|
||||
binder.bind(deliveryTime, Job::getDeliveryTime, Job::setDeliveryTime);
|
||||
|
||||
// Bind customerSelection field with validation
|
||||
binder.forField(customerSelection).asRequired("").bind(Job::getCustomerSelection, Job::setCustomerSelection);
|
||||
|
||||
// Bind optional fields without validation
|
||||
// Bind optional pickup fields without validation
|
||||
binder.bind(pickupCompany, Job::getPickupCompany, Job::setPickupCompany);
|
||||
binder.bind(pickupSalutation, Job::getPickupSalutation, Job::setPickupSalutation);
|
||||
binder.bind(pickupPhone, Job::getPickupPhone, Job::setPickupPhone);
|
||||
binder.bind(pickupAddressAddition, Job::getPickupAddressAddition, Job::setPickupAddressAddition);
|
||||
|
||||
binder.bind(deliveryCompany, Job::getDeliveryCompany, Job::setDeliveryCompany);
|
||||
binder.bind(deliverySalutation, Job::getDeliverySalutation, Job::setDeliverySalutation);
|
||||
binder.bind(deliveryPhone, Job::getDeliveryPhone, Job::setDeliveryPhone);
|
||||
binder.bind(deliveryAddressAddition, Job::getDeliveryAddressAddition, Job::setDeliveryAddressAddition);
|
||||
|
||||
binder.forField(digitalProcessing).bind(Job::isDigitalProcessing,
|
||||
(job, value) -> job.setDigitalProcessing(Boolean.TRUE.equals(value)));
|
||||
|
||||
@@ -1309,11 +1252,10 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
private void setupValidationTriggers() {
|
||||
// List of all required fields
|
||||
TextField[] requiredTextFields = { pickupFirstName, pickupLastName, pickupStreet, pickupHouseNumber, pickupZip,
|
||||
pickupCity, deliveryFirstName, deliveryLastName, deliveryStreet, deliveryHouseNumber, deliveryZip,
|
||||
deliveryCity };
|
||||
pickupCity };
|
||||
|
||||
// List of required date fields
|
||||
DatePicker[] requiredDateFields = { pickupDate, deliveryDate };
|
||||
// List of required date fields (delivery dates are per-station in tiles)
|
||||
DatePicker[] requiredDateFields = { pickupDate };
|
||||
|
||||
// Add validation listener for customerSelection ComboBox
|
||||
customerSelection.addValueChangeListener(event -> {
|
||||
@@ -1426,18 +1368,20 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
|| isFieldEmpty(pickupStreet) || isFieldEmpty(pickupHouseNumber) || isFieldEmpty(pickupZip)
|
||||
|| isFieldEmpty(pickupCity);
|
||||
|
||||
// Check delivery address fields
|
||||
boolean deliveryErrors = isFieldEmpty(deliveryFirstName) || isFieldEmpty(deliveryLastName)
|
||||
|| isFieldEmpty(deliveryStreet) || isFieldEmpty(deliveryHouseNumber) || isFieldEmpty(deliveryZip)
|
||||
|| isFieldEmpty(deliveryCity);
|
||||
// Check all delivery station tiles for errors
|
||||
boolean deliveryErrors = deliveryStationTiles.isEmpty()
|
||||
|| deliveryStationTiles.stream().anyMatch(DeliveryStationTile::hasValidationErrors);
|
||||
|
||||
return customerSelectionEmpty || pickupErrors || deliveryErrors;
|
||||
}
|
||||
|
||||
private boolean hasAppointmentValidationErrors() {
|
||||
LocalDate today = LocalDate.now();
|
||||
return pickupDate.getValue() == null || deliveryDate.getValue() == null || pickupDate.getValue().isBefore(today)
|
||||
|| deliveryDate.getValue().isBefore(today);
|
||||
// Check pickup date
|
||||
if (pickupDate.getValue() == null || pickupDate.getValue().isBefore(today)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean hasCargoValidationErrors() {
|
||||
@@ -1508,11 +1452,22 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
// Zusätzliche Felder, die nicht über den Binder gebunden sind, manuell setzen
|
||||
job.setPickupDate(pickupDate.getValue());
|
||||
job.setPickupTime(pickupTime.getValue());
|
||||
job.setDeliveryDate(deliveryDate.getValue());
|
||||
job.setDeliveryTime(deliveryTime.getValue());
|
||||
if (remarkArea != null)
|
||||
job.setRemark(remarkArea.getValue());
|
||||
|
||||
// Collect delivery stations from tiles
|
||||
List<DeliveryStation> stations = new ArrayList<>();
|
||||
for (int i = 0; i < deliveryStationTiles.size(); i++) {
|
||||
DeliveryStationTile tile = deliveryStationTiles.get(i);
|
||||
DeliveryStation station = tile.getDeliveryStation();
|
||||
station.setStationOrder(i);
|
||||
stations.add(station);
|
||||
}
|
||||
job.setDeliveryStations(stations);
|
||||
|
||||
// Populate flat delivery fields from first station for backward compatibility
|
||||
job.syncFlatDeliveryFieldsFromStations();
|
||||
|
||||
// Store selected service IDs in job for invoice creation
|
||||
job.setServiceIds(selectedServices.stream().map(Service::getId).toList());
|
||||
|
||||
@@ -1584,19 +1539,23 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
pickupCustomer.setCity(pickupCity.getValue());
|
||||
addCustomerService.addCustomer(pickupCustomer);
|
||||
}
|
||||
if (saveDeliveryAddress.getValue()) {
|
||||
Customer deliveryCustomer = new Customer();
|
||||
deliveryCustomer.setCompanyName(deliveryCompany.getValue());
|
||||
deliveryCustomer.setTitle(deliverySalutation.getValue());
|
||||
deliveryCustomer.setFirstname(deliveryFirstName.getValue());
|
||||
deliveryCustomer.setLastName(deliveryLastName.getValue());
|
||||
deliveryCustomer.setTelephone(deliveryPhone.getValue());
|
||||
deliveryCustomer.setStreet(deliveryStreet.getValue());
|
||||
deliveryCustomer.setHouseNumber(deliveryHouseNumber.getValue());
|
||||
deliveryCustomer.setAddressAddition(deliveryAddressAddition.getValue());
|
||||
deliveryCustomer.setZip(deliveryZip.getValue());
|
||||
deliveryCustomer.setCity(deliveryCity.getValue());
|
||||
addCustomerService.addCustomer(deliveryCustomer);
|
||||
// Save delivery station addresses as customers if checkbox is checked
|
||||
for (DeliveryStationTile tile : deliveryStationTiles) {
|
||||
if (tile.isSaveAddressChecked()) {
|
||||
DeliveryStation ds = tile.getDeliveryStation();
|
||||
Customer deliveryCustomer = new Customer();
|
||||
deliveryCustomer.setCompanyName(ds.getCompany());
|
||||
deliveryCustomer.setTitle(ds.getSalutation());
|
||||
deliveryCustomer.setFirstname(ds.getFirstName());
|
||||
deliveryCustomer.setLastName(ds.getLastName());
|
||||
deliveryCustomer.setTelephone(ds.getPhone());
|
||||
deliveryCustomer.setStreet(ds.getStreet());
|
||||
deliveryCustomer.setHouseNumber(ds.getHouseNumber());
|
||||
deliveryCustomer.setAddressAddition(ds.getAddressAddition());
|
||||
deliveryCustomer.setZip(ds.getZip());
|
||||
deliveryCustomer.setCity(ds.getCity());
|
||||
addCustomerService.addCustomer(deliveryCustomer);
|
||||
}
|
||||
}
|
||||
|
||||
// All validations passed, save the job with cargo items and tasks
|
||||
@@ -1986,18 +1945,18 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
pickupCity.clear();
|
||||
savePickupAddress.setValue(false);
|
||||
|
||||
// Delivery address
|
||||
deliveryCompany.clear();
|
||||
deliverySalutation.clear();
|
||||
deliveryFirstName.clear();
|
||||
deliveryLastName.clear();
|
||||
deliveryPhone.clear();
|
||||
deliveryStreet.clear();
|
||||
deliveryHouseNumber.clear();
|
||||
deliveryAddressAddition.clear();
|
||||
deliveryZip.clear();
|
||||
deliveryCity.clear();
|
||||
saveDeliveryAddress.setValue(false);
|
||||
// Delivery stations - remove all but the first, clear the first
|
||||
while (deliveryStationTiles.size() > 1) {
|
||||
DeliveryStationTile tile = deliveryStationTiles.remove(deliveryStationTiles.size() - 1);
|
||||
stationsScrollContainer.remove(tile);
|
||||
}
|
||||
if (!deliveryStationTiles.isEmpty()) {
|
||||
deliveryStationTiles.get(0).clearFields();
|
||||
}
|
||||
// Ensure "+" button is visible
|
||||
if (addStationButton.getParent().isEmpty()) {
|
||||
stationsScrollContainer.add(addStationButton);
|
||||
}
|
||||
|
||||
// Digital processing
|
||||
digitalProcessing.setValue(true);
|
||||
@@ -2045,27 +2004,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
pickupCity.addFocusListener(e -> disableDragSources());
|
||||
pickupCity.addBlurListener(e -> enableDragSources());
|
||||
|
||||
// Delivery fields
|
||||
deliveryCompany.addFocusListener(e -> disableDragSources());
|
||||
deliveryCompany.addBlurListener(e -> enableDragSources());
|
||||
deliverySalutation.addFocusListener(e -> disableDragSources());
|
||||
deliverySalutation.addBlurListener(e -> enableDragSources());
|
||||
deliveryFirstName.addFocusListener(e -> disableDragSources());
|
||||
deliveryFirstName.addBlurListener(e -> enableDragSources());
|
||||
deliveryLastName.addFocusListener(e -> disableDragSources());
|
||||
deliveryLastName.addBlurListener(e -> enableDragSources());
|
||||
deliveryPhone.addFocusListener(e -> disableDragSources());
|
||||
deliveryPhone.addBlurListener(e -> enableDragSources());
|
||||
deliveryStreet.addFocusListener(e -> disableDragSources());
|
||||
deliveryStreet.addBlurListener(e -> enableDragSources());
|
||||
deliveryHouseNumber.addFocusListener(e -> disableDragSources());
|
||||
deliveryHouseNumber.addBlurListener(e -> enableDragSources());
|
||||
deliveryAddressAddition.addFocusListener(e -> disableDragSources());
|
||||
deliveryAddressAddition.addBlurListener(e -> enableDragSources());
|
||||
deliveryZip.addFocusListener(e -> disableDragSources());
|
||||
deliveryZip.addBlurListener(e -> enableDragSources());
|
||||
deliveryCity.addFocusListener(e -> disableDragSources());
|
||||
deliveryCity.addBlurListener(e -> enableDragSources());
|
||||
// Delivery fields are handled inside DeliveryStationTile
|
||||
|
||||
// Digital processing
|
||||
appUser.addFocusListener(e -> disableDragSources());
|
||||
@@ -2080,10 +2019,6 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
pickupSection.getStyle().set("pointer-events", "none");
|
||||
pickupSection.getElement().setAttribute("draggable", "false");
|
||||
}
|
||||
if (deliverySection != null) {
|
||||
deliverySection.getStyle().set("pointer-events", "none");
|
||||
deliverySection.getElement().setAttribute("draggable", "false");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2094,10 +2029,6 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
pickupSection.getStyle().remove("pointer-events");
|
||||
pickupSection.getElement().setAttribute("draggable", "true");
|
||||
}
|
||||
if (deliverySection != null) {
|
||||
deliverySection.getStyle().remove("pointer-events");
|
||||
deliverySection.getElement().setAttribute("draggable", "true");
|
||||
}
|
||||
}
|
||||
|
||||
private void createTaskRow() {
|
||||
@@ -2924,16 +2855,13 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob die Lieferadresse gültig ist (Pflichtfelder ausgefüllt).
|
||||
* Prüft, ob mindestens eine Lieferstation gültige Adressdaten hat.
|
||||
*/
|
||||
private boolean hasDeliveryAddressChanged() {
|
||||
String currentStreet = getValueOrEmpty(deliveryStreet);
|
||||
String currentZip = getValueOrEmpty(deliveryZip);
|
||||
String currentCity = getValueOrEmpty(deliveryCity);
|
||||
|
||||
// Nur true zurückgeben, wenn alle Pflichtfelder ausgefüllt sind und Validierung
|
||||
// nötig ist
|
||||
return addressesDirty && !currentStreet.isEmpty() && !currentZip.isEmpty() && !currentCity.isEmpty();
|
||||
if (!addressesDirty) {
|
||||
return false;
|
||||
}
|
||||
return deliveryStationTiles.stream().anyMatch(DeliveryStationTile::hasAddressForValidation);
|
||||
}
|
||||
|
||||
private String getValueOrEmpty(TextField field) {
|
||||
@@ -3023,16 +2951,19 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
final String pickupZipValue = getValueOrEmpty(pickupZip);
|
||||
final String pickupCityValue = getValueOrEmpty(pickupCity);
|
||||
final boolean pickupChanged = hasPickupAddressChanged();
|
||||
|
||||
final String deliveryStreetValue = getValueOrEmpty(deliveryStreet);
|
||||
final String deliveryHouseNumberValue = getValueOrEmpty(deliveryHouseNumber);
|
||||
final String deliveryZipValue = getValueOrEmpty(deliveryZip);
|
||||
final String deliveryCityValue = getValueOrEmpty(deliveryCity);
|
||||
final boolean deliveryChanged = hasDeliveryAddressChanged();
|
||||
|
||||
// Capture delivery station data for async validation
|
||||
final List<String[]> stationData = new ArrayList<>();
|
||||
for (DeliveryStationTile tile : deliveryStationTiles) {
|
||||
if (tile.hasAddressForValidation()) {
|
||||
stationData.add(new String[] { tile.getStreetValue(), tile.getHouseNumberValue(), tile.getZipValue(),
|
||||
tile.getCityValue() });
|
||||
}
|
||||
}
|
||||
|
||||
// Asynchrone Validierung im Hintergrund durchführen
|
||||
getUI().ifPresent(ui -> {
|
||||
// CompletableFuture für Hintergrund-Verarbeitung
|
||||
java.util.concurrent.CompletableFuture.runAsync(() -> {
|
||||
// Abholadresse validieren
|
||||
final AddressValidationResult[] pickupResultHolder = new AddressValidationResult[1];
|
||||
@@ -3041,45 +2972,67 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
pickupHouseNumberValue, pickupZipValue, pickupCityValue);
|
||||
}
|
||||
|
||||
// Lieferadresse validieren
|
||||
final AddressValidationResult[] deliveryResultHolder = new AddressValidationResult[1];
|
||||
// Alle Lieferstationen validieren
|
||||
final List<AddressValidationResult> deliveryResults = new ArrayList<>();
|
||||
if (deliveryChanged) {
|
||||
deliveryResultHolder[0] = addressValidationService.validateAddress("delivery", deliveryStreetValue,
|
||||
deliveryHouseNumberValue, deliveryZipValue, deliveryCityValue);
|
||||
for (int i = 0; i < stationData.size(); i++) {
|
||||
String[] data = stationData.get(i);
|
||||
AddressValidationResult result = addressValidationService.validateAddress("delivery_" + i,
|
||||
data[0], data[1], data[2], data[3]);
|
||||
deliveryResults.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
// UI aktualisieren mit den Ergebnissen
|
||||
ui.access(() -> {
|
||||
// Ergebnisse speichern
|
||||
if (pickupResultHolder[0] != null) {
|
||||
addressValidationResults.put("pickup", pickupResultHolder[0]);
|
||||
}
|
||||
if (deliveryResultHolder[0] != null) {
|
||||
addressValidationResults.put("delivery", deliveryResultHolder[0]);
|
||||
for (int i = 0; i < deliveryResults.size(); i++) {
|
||||
addressValidationResults.put("delivery_" + i, deliveryResults.get(i));
|
||||
}
|
||||
|
||||
// Ergebnisse ermitteln
|
||||
AddressValidationResult pickupResult = pickupResultHolder[0] != null ? pickupResultHolder[0]
|
||||
: addressValidationResults.get("pickup");
|
||||
AddressValidationResult deliveryResult = deliveryResultHolder[0] != null ? deliveryResultHolder[0]
|
||||
: addressValidationResults.get("delivery");
|
||||
|
||||
// Use first delivery station result for route calculation (backward compat)
|
||||
AddressValidationResult firstDeliveryResult = !deliveryResults.isEmpty() ? deliveryResults.get(0)
|
||||
: addressValidationResults.get("delivery_0");
|
||||
|
||||
// Lade-Anzeige ausblenden
|
||||
loadingMessage.setVisible(false);
|
||||
progressBar.setVisible(false);
|
||||
|
||||
// Prüfen ob beide Adressen gültig sind
|
||||
// Prüfen ob Abholadresse und erste Lieferadresse gültig sind
|
||||
boolean bothValid = (pickupResult != null && pickupResult.isValid())
|
||||
&& (deliveryResult != null && deliveryResult.isValid());
|
||||
&& (firstDeliveryResult != null && firstDeliveryResult.isValid());
|
||||
|
||||
// Route berechnen wenn beide gültig
|
||||
if (bothValid) {
|
||||
routeCalculationResult = addressValidationService.calculateRoute(pickupResult, deliveryResult);
|
||||
routeCalculationResult = addressValidationService.calculateRoute(pickupResult,
|
||||
firstDeliveryResult);
|
||||
}
|
||||
|
||||
// Ergebnisse anzeigen
|
||||
updateValidationDialogResults(pickupResult, deliveryResult, pickupResultLabel, deliveryResultLabel,
|
||||
routeResultLabel, resultLayout, buttonLayout, continueButton, targetTab);
|
||||
// Ergebnisse anzeigen - show first delivery station result in existing UI
|
||||
updateValidationDialogResults(pickupResult, firstDeliveryResult, pickupResultLabel,
|
||||
deliveryResultLabel, routeResultLabel, resultLayout, buttonLayout, continueButton,
|
||||
targetTab);
|
||||
|
||||
// Show additional station results
|
||||
for (int i = 1; i < deliveryResults.size(); i++) {
|
||||
AddressValidationResult stationResult = deliveryResults.get(i);
|
||||
Span stationLabel = new Span();
|
||||
if (stationResult.isValid()) {
|
||||
stationLabel.setText("\u2713 " + getTranslation("addjob.station.delivery", i + 1) + ": "
|
||||
+ stationResult.getFormattedAddress());
|
||||
stationLabel.getStyle().set("color", "var(--lumo-success-text-color)");
|
||||
} else {
|
||||
stationLabel.setText("\u26A0 " + getTranslation("addjob.station.delivery", i + 1) + ": "
|
||||
+ stationResult.getValidationMessage());
|
||||
stationLabel.getStyle().set("color", "var(--lumo-error-text-color)");
|
||||
}
|
||||
resultLayout.addComponentAtIndex(resultLayout.getComponentCount() - 1, stationLabel);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3209,16 +3162,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
pickupZip.getStyle().set("--vaadin-input-field-background", pickupColor);
|
||||
pickupCity.getStyle().set("--vaadin-input-field-background", pickupColor);
|
||||
|
||||
// Lieferadresse - hellgrün für validiert, hellgelb für nicht validiert
|
||||
String deliveryColor = (deliveryResult != null && deliveryResult.isValid()) ? "rgba(144, 238, 144, 0.5)" // Hellgrün
|
||||
// mit
|
||||
// Transparenz
|
||||
: "rgba(255, 250, 205, 0.5)"; // Hellgelb mit Transparenz
|
||||
|
||||
deliveryStreet.getStyle().set("--vaadin-input-field-background", deliveryColor);
|
||||
deliveryHouseNumber.getStyle().set("--vaadin-input-field-background", deliveryColor);
|
||||
deliveryZip.getStyle().set("--vaadin-input-field-background", deliveryColor);
|
||||
deliveryCity.getStyle().set("--vaadin-input-field-background", deliveryColor);
|
||||
// Delivery station field styling is handled inside the tiles
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3238,11 +3182,11 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt das Validierungsergebnis für die Lieferadresse zurück. Kann null sein,
|
||||
* wenn noch keine Validierung durchgeführt wurde.
|
||||
* Gibt das Validierungsergebnis für die erste Lieferadresse zurück. Kann null
|
||||
* sein, wenn noch keine Validierung durchgeführt wurde.
|
||||
*/
|
||||
public AddressValidationResult getDeliveryAddressValidationResult() {
|
||||
return addressValidationResults.get("delivery");
|
||||
return addressValidationResults.get("delivery_0");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3343,32 +3287,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
}
|
||||
});
|
||||
|
||||
// Lieferadress-Felder
|
||||
deliveryCompany.addValueChangeListener(e -> {
|
||||
if (e.isFromClient()) {
|
||||
resetRouteInformation();
|
||||
}
|
||||
});
|
||||
deliveryStreet.addValueChangeListener(e -> {
|
||||
if (e.isFromClient()) {
|
||||
resetRouteInformation();
|
||||
}
|
||||
});
|
||||
deliveryHouseNumber.addValueChangeListener(e -> {
|
||||
if (e.isFromClient()) {
|
||||
resetRouteInformation();
|
||||
}
|
||||
});
|
||||
deliveryZip.addValueChangeListener(e -> {
|
||||
if (e.isFromClient()) {
|
||||
resetRouteInformation();
|
||||
}
|
||||
});
|
||||
deliveryCity.addValueChangeListener(e -> {
|
||||
if (e.isFromClient()) {
|
||||
resetRouteInformation();
|
||||
}
|
||||
});
|
||||
// Delivery station field change listeners are handled via tile callbacks
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -681,10 +681,10 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
pdfFrame.getStyle().set("border", "none");
|
||||
|
||||
Button downloadButton = new Button("Herunterladen", e -> {
|
||||
parent.getElement().executeJs("const link = document.createElement('a');"
|
||||
+ "link.href = 'data:application/pdf;base64," + base64Pdf + "';"
|
||||
+ "link.download = '" + title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf';"
|
||||
+ "link.click();");
|
||||
parent.getElement()
|
||||
.executeJs("const link = document.createElement('a');" + "link.href = 'data:application/pdf;base64,"
|
||||
+ base64Pdf + "';" + "link.download = '" + title.replaceAll("[^a-zA-Z0-9\\-]", "_")
|
||||
+ ".pdf';" + "link.click();");
|
||||
});
|
||||
downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
|
||||
|
||||
@@ -877,8 +877,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
currentUser, prefixField.getValue());
|
||||
showPdfInDialog(pdfBytes);
|
||||
} catch (Exception ex) {
|
||||
Notification.show(getTranslation("profile.invoice.pdf.preview.error", ex.getMessage()), 3000,
|
||||
Notification.Position.BOTTOM_CENTER);
|
||||
Notification.show(getTranslation("profile.invoice.pdf.preview.error", ex.getMessage()),
|
||||
3000, Notification.Position.BOTTOM_CENTER);
|
||||
}
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
@@ -1027,10 +1027,10 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
||||
"image");
|
||||
|
||||
panel.add(invoiceHeader, senderCompany, senderName, senderAddress, senderCity, senderEmail, senderPhone,
|
||||
invoiceNumber, servicesHeader, servicesListBlock, servicesNetBlock, servicesVatBlock, servicesGrossBlock,
|
||||
customerHeader, customerCompany, customerName, customerAddress, customerCity, customerEmail,
|
||||
customerPhone, freeHeader, textBlock, headerBlock, dateBlock, customerBlock, companyBlock, amountBlock,
|
||||
lineBlock, imageBlock);
|
||||
invoiceNumber, servicesHeader, servicesListBlock, servicesNetBlock, servicesVatBlock,
|
||||
servicesGrossBlock, customerHeader, customerCompany, customerName, customerAddress, customerCity,
|
||||
customerEmail, customerPhone, freeHeader, textBlock, headerBlock, dateBlock, customerBlock,
|
||||
companyBlock, amountBlock, lineBlock, imageBlock);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.vaadin.flow.router.HasDynamicTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||
import de.assecutor.votianlt.model.CargoItem;
|
||||
import de.assecutor.votianlt.model.DeliveryStation;
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.LocationPosition;
|
||||
import de.assecutor.votianlt.model.task.BaseTask;
|
||||
@@ -199,19 +200,50 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
pickupBox.add(new Span(concatAddress(job.getPickupStreet(), job.getPickupHouseNumber())));
|
||||
pickupBox.add(new Span(concatZipCity(job.getPickupZip(), job.getPickupCity())));
|
||||
|
||||
VerticalLayout deliveryBox = borderedBox();
|
||||
deliveryBox.add(new H3(getTranslation("jobsummary.section.delivery") + " "
|
||||
+ formatDateWithTime(job.getDeliveryDate(), job.getDeliveryTime())));
|
||||
deliveryBox.add(new Span(valueOrEmpty(job.getDeliveryCompany())));
|
||||
deliveryBox.add(new Span(valueOrEmpty(job.getDeliverySalutation())
|
||||
+ (job.getDeliverySalutation() != null ? " " : "") + valueOrEmpty(job.getDeliveryFirstName())
|
||||
+ (job.getDeliveryFirstName() != null ? " " : "") + valueOrEmpty(job.getDeliveryLastName())));
|
||||
deliveryBox.add(new Span(concatAddress(job.getDeliveryStreet(), job.getDeliveryHouseNumber())));
|
||||
deliveryBox.add(new Span(concatZipCity(job.getDeliveryZip(), job.getDeliveryCity())));
|
||||
|
||||
pickupBox.setWidth("50%");
|
||||
deliveryBox.setWidth("50%");
|
||||
topRow.add(pickupBox, deliveryBox);
|
||||
|
||||
List<DeliveryStation> stations = job.getDeliveryStations();
|
||||
if (stations != null && !stations.isEmpty()) {
|
||||
// Multiple delivery stations layout
|
||||
VerticalLayout deliveryStationsContainer = new VerticalLayout();
|
||||
deliveryStationsContainer.setPadding(false);
|
||||
deliveryStationsContainer.setSpacing(true);
|
||||
deliveryStationsContainer.setWidth("50%");
|
||||
|
||||
for (int i = 0; i < stations.size(); i++) {
|
||||
DeliveryStation station = stations.get(i);
|
||||
VerticalLayout stationBox = borderedBox();
|
||||
String stationLabel = getTranslation("jobsummary.section.delivery") + " "
|
||||
+ (stations.size() > 1 ? (i + 1) + " " : "")
|
||||
+ formatDateWithTime(station.getDeliveryDate(), station.getDeliveryTime());
|
||||
stationBox.add(new H3(stationLabel));
|
||||
stationBox.add(new Span(valueOrEmpty(station.getCompany())));
|
||||
stationBox.add(new Span(valueOrEmpty(station.getSalutation())
|
||||
+ (station.getSalutation() != null ? " " : "") + valueOrEmpty(station.getFirstName())
|
||||
+ (station.getFirstName() != null ? " " : "") + valueOrEmpty(station.getLastName())));
|
||||
stationBox.add(new Span(concatAddress(station.getStreet(), station.getHouseNumber())));
|
||||
stationBox.add(new Span(concatZipCity(station.getZip(), station.getCity())));
|
||||
if (station.getPhone() != null && !station.getPhone().isBlank()) {
|
||||
stationBox.add(new Span(getTranslation("jobsummary.station.phone") + ": " + station.getPhone()));
|
||||
}
|
||||
deliveryStationsContainer.add(stationBox);
|
||||
}
|
||||
|
||||
topRow.add(pickupBox, deliveryStationsContainer);
|
||||
} else {
|
||||
// Fallback: flat delivery fields for old jobs
|
||||
VerticalLayout deliveryBox = borderedBox();
|
||||
deliveryBox.add(new H3(getTranslation("jobsummary.section.delivery") + " "
|
||||
+ formatDateWithTime(job.getDeliveryDate(), job.getDeliveryTime())));
|
||||
deliveryBox.add(new Span(valueOrEmpty(job.getDeliveryCompany())));
|
||||
deliveryBox.add(new Span(valueOrEmpty(job.getDeliverySalutation())
|
||||
+ (job.getDeliverySalutation() != null ? " " : "") + valueOrEmpty(job.getDeliveryFirstName())
|
||||
+ (job.getDeliveryFirstName() != null ? " " : "") + valueOrEmpty(job.getDeliveryLastName())));
|
||||
deliveryBox.add(new Span(concatAddress(job.getDeliveryStreet(), job.getDeliveryHouseNumber())));
|
||||
deliveryBox.add(new Span(concatZipCity(job.getDeliveryZip(), job.getDeliveryCity())));
|
||||
deliveryBox.setWidth("50%");
|
||||
topRow.add(pickupBox, deliveryBox);
|
||||
}
|
||||
content.add(topRow);
|
||||
|
||||
// Aufgaben
|
||||
@@ -275,9 +307,12 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
Div priceTable = new Div();
|
||||
priceTable.getStyle().set("width", "100%");
|
||||
|
||||
priceTable.add(createPriceRow(getTranslation("jobsummary.info.netto") + ":", formatPrice(priceResult.netAmount()), false));
|
||||
priceTable.add(createPriceRow(getTranslation("jobsummary.info.ust") + ":", formatPrice(priceResult.vatAmount()), false));
|
||||
priceTable.add(createPriceRow(getTranslation("jobsummary.info.gesamt") + ":", formatPrice(priceResult.totalAmount()), true));
|
||||
priceTable.add(createPriceRow(getTranslation("jobsummary.info.netto") + ":",
|
||||
formatPrice(priceResult.netAmount()), false));
|
||||
priceTable.add(createPriceRow(getTranslation("jobsummary.info.ust") + ":", formatPrice(priceResult.vatAmount()),
|
||||
false));
|
||||
priceTable.add(createPriceRow(getTranslation("jobsummary.info.gesamt") + ":",
|
||||
formatPrice(priceResult.totalAmount()), true));
|
||||
|
||||
infoBox.add(priceTable);
|
||||
|
||||
@@ -461,8 +496,32 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
// Baue Adress-Strings
|
||||
String origin = (concatAddress(job.getPickupStreet(), job.getPickupHouseNumber()) + ", "
|
||||
+ concatZipCity(job.getPickupZip(), job.getPickupCity())).trim();
|
||||
String destination = (concatAddress(job.getDeliveryStreet(), job.getDeliveryHouseNumber()) + ", "
|
||||
+ concatZipCity(job.getDeliveryZip(), job.getDeliveryCity())).trim();
|
||||
|
||||
// Build destination and waypoints from delivery stations
|
||||
List<DeliveryStation> stations = job.getDeliveryStations();
|
||||
String destination;
|
||||
List<String> waypoints = new ArrayList<>();
|
||||
|
||||
if (stations != null && !stations.isEmpty()) {
|
||||
// Last station is the final destination
|
||||
DeliveryStation lastStation = stations.get(stations.size() - 1);
|
||||
destination = (concatAddress(lastStation.getStreet(), lastStation.getHouseNumber()) + ", "
|
||||
+ concatZipCity(lastStation.getZip(), lastStation.getCity())).trim();
|
||||
|
||||
// Intermediate stations are waypoints
|
||||
for (int i = 0; i < stations.size() - 1; i++) {
|
||||
DeliveryStation station = stations.get(i);
|
||||
String wp = (concatAddress(station.getStreet(), station.getHouseNumber()) + ", "
|
||||
+ concatZipCity(station.getZip(), station.getCity())).trim();
|
||||
if (!wp.isBlank()) {
|
||||
waypoints.add(wp);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback to flat delivery fields
|
||||
destination = (concatAddress(job.getDeliveryStreet(), job.getDeliveryHouseNumber()) + ", "
|
||||
+ concatZipCity(job.getDeliveryZip(), job.getDeliveryCity())).trim();
|
||||
}
|
||||
|
||||
if (origin.isBlank() || destination.isBlank()) {
|
||||
return;
|
||||
@@ -504,15 +563,16 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
Integer savedDuration = job.getRouteDurationSeconds();
|
||||
boolean hasSavedRouteData = savedDistance != null && savedDuration != null;
|
||||
|
||||
String js = buildMapJs(origin, destination, hasPosition, position, appUserId, shouldUpdate, hasSavedRouteData,
|
||||
savedDistance != null ? savedDistance : 0.0, savedDuration != null ? savedDuration : 0);
|
||||
String js = buildMapJs(origin, destination, waypoints, hasPosition, position, appUserId, shouldUpdate,
|
||||
hasSavedRouteData, savedDistance != null ? savedDistance : 0.0,
|
||||
savedDuration != null ? savedDuration : 0);
|
||||
|
||||
map.getElement().executeJs(js, map.getElement(), routeInfo.getElement());
|
||||
}
|
||||
|
||||
private String buildMapJs(String origin, String destination, boolean hasPosition, LocationPosition position,
|
||||
String appUserId, boolean shouldUpdate, boolean hasSavedRouteData, double savedDistance,
|
||||
int savedDuration) {
|
||||
private String buildMapJs(String origin, String destination, List<String> waypoints, boolean hasPosition,
|
||||
LocationPosition position, String appUserId, boolean shouldUpdate, boolean hasSavedRouteData,
|
||||
double savedDistance, int savedDuration) {
|
||||
String apiKey = getGoogleMapsApiKey();
|
||||
// Explizit mit Punkt als Dezimaltrennzeichen formatieren
|
||||
String lat = hasPosition ? String.format(java.util.Locale.US, "%.6f", position.getLatitude()) : "0";
|
||||
@@ -526,6 +586,17 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
String savedDurationText = hours > 0 ? String.format("%d Std. %d Min.", hours, minutes)
|
||||
: String.format("%d Min.", minutes);
|
||||
|
||||
// Build waypoints JS array
|
||||
StringBuilder waypointsJs = new StringBuilder("[");
|
||||
if (waypoints != null && !waypoints.isEmpty()) {
|
||||
for (int i = 0; i < waypoints.size(); i++) {
|
||||
if (i > 0)
|
||||
waypointsJs.append(",");
|
||||
waypointsJs.append("{location:'").append(escapeJs(waypoints.get(i))).append("',stopover:true}");
|
||||
}
|
||||
}
|
||||
waypointsJs.append("]");
|
||||
|
||||
return """
|
||||
(function(){
|
||||
var host = $0;
|
||||
@@ -541,6 +612,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
var hasSavedRouteData = %s;
|
||||
var savedDistance = %s;
|
||||
var savedDurationText = '%s';
|
||||
var waypoints = %s;
|
||||
|
||||
var appUserMarker = null;
|
||||
var updateInterval = null;
|
||||
@@ -557,7 +629,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
trafficLayer.setMap(map);
|
||||
|
||||
var ds = new google.maps.DirectionsService();
|
||||
ds.route({
|
||||
var routeRequest = {
|
||||
origin: origin,
|
||||
destination: destination,
|
||||
travelMode: google.maps.TravelMode.DRIVING,
|
||||
@@ -566,7 +638,12 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
departureTime: new Date(),
|
||||
trafficModel: google.maps.TrafficModel.BEST_GUESS
|
||||
}
|
||||
}, function(res, status){
|
||||
};
|
||||
if(waypoints.length > 0){
|
||||
routeRequest.waypoints = waypoints;
|
||||
routeRequest.optimizeWaypoints = false;
|
||||
}
|
||||
ds.route(routeRequest, function(res, status){
|
||||
if(status === 'OK'){
|
||||
infoEl.innerHTML = '';
|
||||
|
||||
@@ -684,7 +761,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
"""
|
||||
.formatted(escapeJs(origin), escapeJs(destination), escapeJs(apiKey), lat, lng,
|
||||
Boolean.toString(hasPosition), escapeJs(appUserId), Boolean.toString(shouldUpdate),
|
||||
Boolean.toString(hasSavedRouteData), savedDistanceStr, escapeJs(savedDurationText));
|
||||
Boolean.toString(hasSavedRouteData), savedDistanceStr, escapeJs(savedDurationText),
|
||||
waypointsJs.toString());
|
||||
}
|
||||
|
||||
// Hilfsfunktion zum einfachen Escapen von JS-Zeichen in Strings
|
||||
|
||||
@@ -116,8 +116,8 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
|
||||
.setSortable(true);
|
||||
grid.addColumn(job -> DateTimeFormatUtil.formatDateTime(job.getCreatedAt()))
|
||||
.setHeader(getTranslation("jobs.column.jobdate")).setAutoWidth(true).setSortable(true);
|
||||
grid.addColumn(Job::getDeliveryCity).setHeader(getTranslation("jobs.column.destination")).setAutoWidth(true)
|
||||
.setFlexGrow(1).setSortable(true);
|
||||
grid.addColumn(Job::getFirstDeliveryCity).setHeader(getTranslation("jobs.column.destination"))
|
||||
.setAutoWidth(true).setFlexGrow(1).setSortable(true);
|
||||
|
||||
// Action column: manual completion for jobs without digital processing
|
||||
grid.addComponentColumn(job -> {
|
||||
@@ -144,16 +144,14 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
|
||||
invoiceBtn.setTooltipText(getTranslation("jobs.tooltip.showinvoice"));
|
||||
invoiceBtn.addClickListener(e -> {
|
||||
e.getSource().getElement().getNode();
|
||||
customerInvoiceRepository.findById(job.getInvoiceId()).ifPresentOrElse(
|
||||
invoice -> {
|
||||
if (invoice.getPdfData() != null) {
|
||||
CreateInvoiceView.showSavedInvoiceDialog(invoice.getPdfData(),
|
||||
invoice.getInvoiceNumber(), this);
|
||||
} else {
|
||||
getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString()));
|
||||
}
|
||||
},
|
||||
() -> getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString())));
|
||||
customerInvoiceRepository.findById(job.getInvoiceId()).ifPresentOrElse(invoice -> {
|
||||
if (invoice.getPdfData() != null) {
|
||||
CreateInvoiceView.showSavedInvoiceDialog(invoice.getPdfData(),
|
||||
invoice.getInvoiceNumber(), this);
|
||||
} else {
|
||||
getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString()));
|
||||
}
|
||||
}, () -> getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString())));
|
||||
});
|
||||
} else {
|
||||
invoiceBtn.setTooltipText(getTranslation("jobs.tooltip.createinvoice"));
|
||||
@@ -344,7 +342,7 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
|
||||
csv.append(escapeCsv(extractCompanyName(job.getCustomerSelection()))).append(",");
|
||||
csv.append(escapeCsv(job.getJobNumber())).append(",");
|
||||
csv.append(DateTimeFormatUtil.formatDateTime(job.getCreatedAt())).append(",");
|
||||
csv.append(escapeCsv(job.getDeliveryCity())).append("\n");
|
||||
csv.append(escapeCsv(job.getDeliveryCitiesDisplay())).append("\n");
|
||||
}
|
||||
|
||||
return csv.toString();
|
||||
|
||||
@@ -257,8 +257,7 @@ public class CustomerInvoiceService {
|
||||
}
|
||||
|
||||
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData, de.assecutor.votianlt.model.User user,
|
||||
String invoicePrefix)
|
||||
throws Exception {
|
||||
String invoicePrefix) throws Exception {
|
||||
// Parse the JSON template data
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonTemplateData);
|
||||
@@ -371,18 +370,21 @@ public class CustomerInvoiceService {
|
||||
htmlBuilder.append("left:").append(String.format(java.util.Locale.US, "%.2f", mmX)).append("mm;");
|
||||
htmlBuilder.append("top:").append(String.format(java.util.Locale.US, "%.2f", mmY)).append("mm;");
|
||||
if ("line".equals(type)) {
|
||||
htmlBuilder.append("width:").append(String.format(java.util.Locale.US, "%.2f", mmWidth)).append("mm;");
|
||||
htmlBuilder.append("width:").append(String.format(java.util.Locale.US, "%.2f", mmWidth))
|
||||
.append("mm;");
|
||||
htmlBuilder.append("height:0;border-top:1px solid #333;");
|
||||
} else if ("vline".equals(type)) {
|
||||
htmlBuilder.append("width:0;border-left:1px solid #333;");
|
||||
htmlBuilder.append("height:").append(String.format(java.util.Locale.US, "%.2f", mmHeight)).append("mm;");
|
||||
htmlBuilder.append("height:").append(String.format(java.util.Locale.US, "%.2f", mmHeight))
|
||||
.append("mm;");
|
||||
} else {
|
||||
htmlBuilder.append("width:").append(String.format(java.util.Locale.US, "%.2f", mmWidth)).append("mm;");
|
||||
htmlBuilder.append("width:").append(String.format(java.util.Locale.US, "%.2f", mmWidth))
|
||||
.append("mm;");
|
||||
htmlBuilder.append("height:").append(String.format(java.util.Locale.US, "%.2f", mmHeight))
|
||||
.append("mm;");
|
||||
htmlBuilder.append("font-size:").append(fontSize).append("pt;");
|
||||
htmlBuilder.append("line-height:").append(String.format(java.util.Locale.US, "%.2f", fontSize * 1.2))
|
||||
.append("pt;");
|
||||
htmlBuilder.append("line-height:")
|
||||
.append(String.format(java.util.Locale.US, "%.2f", fontSize * 1.2)).append("pt;");
|
||||
htmlBuilder.append("color:").append(color).append(";");
|
||||
// For services.list use block display to allow table to fill width
|
||||
if ("services.list".equals(variable)) {
|
||||
@@ -668,18 +670,21 @@ public class CustomerInvoiceService {
|
||||
htmlBuilder.append("left:").append(String.format(java.util.Locale.US, "%.2f", mmX)).append("mm;");
|
||||
htmlBuilder.append("top:").append(String.format(java.util.Locale.US, "%.2f", mmY)).append("mm;");
|
||||
if ("line".equals(type)) {
|
||||
htmlBuilder.append("width:").append(String.format(java.util.Locale.US, "%.2f", mmWidth)).append("mm;");
|
||||
htmlBuilder.append("width:").append(String.format(java.util.Locale.US, "%.2f", mmWidth))
|
||||
.append("mm;");
|
||||
htmlBuilder.append("height:0;border-top:1px solid #333;");
|
||||
} else if ("vline".equals(type)) {
|
||||
htmlBuilder.append("width:0;border-left:1px solid #333;");
|
||||
htmlBuilder.append("height:").append(String.format(java.util.Locale.US, "%.2f", mmHeight)).append("mm;");
|
||||
htmlBuilder.append("height:").append(String.format(java.util.Locale.US, "%.2f", mmHeight))
|
||||
.append("mm;");
|
||||
} else {
|
||||
htmlBuilder.append("width:").append(String.format(java.util.Locale.US, "%.2f", mmWidth)).append("mm;");
|
||||
htmlBuilder.append("width:").append(String.format(java.util.Locale.US, "%.2f", mmWidth))
|
||||
.append("mm;");
|
||||
htmlBuilder.append("height:").append(String.format(java.util.Locale.US, "%.2f", mmHeight))
|
||||
.append("mm;");
|
||||
htmlBuilder.append("font-size:").append(fontSize).append("pt;");
|
||||
htmlBuilder.append("line-height:").append(String.format(java.util.Locale.US, "%.2f", fontSize * 1.2))
|
||||
.append("pt;");
|
||||
htmlBuilder.append("line-height:")
|
||||
.append(String.format(java.util.Locale.US, "%.2f", fontSize * 1.2)).append("pt;");
|
||||
htmlBuilder.append("color:").append(color).append(";");
|
||||
// For services.list use block display
|
||||
if ("services.list".equals(variable)) {
|
||||
@@ -706,7 +711,8 @@ public class CustomerInvoiceService {
|
||||
.replace("job.", "") + "]";
|
||||
System.out.println("DEBUG: No value for variable " + variable + ", using placeholder");
|
||||
} else if ("customer".equals(type)) {
|
||||
// Customer-type element without variable: compose address from customer variables
|
||||
// Customer-type element without variable: compose address from customer
|
||||
// variables
|
||||
String line1 = variables.getOrDefault("customer.company_name",
|
||||
variables.getOrDefault("customer.contact_name", ""));
|
||||
String line2 = variables.getOrDefault("customer.street", "");
|
||||
|
||||
@@ -85,16 +85,17 @@ public class EmailService {
|
||||
body.append("Aufgabe: ").append(taskTypeName).append("\n");
|
||||
body.append("Abgeschlossen von: ").append(appUserName).append("\n\n");
|
||||
|
||||
if (job.getPickupCity() != null || job.getDeliveryCity() != null) {
|
||||
String deliveryCities = job.getDeliveryCitiesDisplay();
|
||||
if (job.getPickupCity() != null || deliveryCities != null) {
|
||||
body.append("Route: ");
|
||||
if (job.getPickupCity() != null) {
|
||||
body.append(job.getPickupCity());
|
||||
}
|
||||
if (job.getPickupCity() != null && job.getDeliveryCity() != null) {
|
||||
if (job.getPickupCity() != null && deliveryCities != null) {
|
||||
body.append(" → ");
|
||||
}
|
||||
if (job.getDeliveryCity() != null) {
|
||||
body.append(job.getDeliveryCity());
|
||||
if (deliveryCities != null) {
|
||||
body.append(deliveryCities);
|
||||
}
|
||||
body.append("\n\n");
|
||||
}
|
||||
@@ -204,16 +205,17 @@ public class EmailService {
|
||||
body.append("Kunde: ").append(job.getDeliveryCompany()).append("\n");
|
||||
}
|
||||
|
||||
if (job.getPickupCity() != null || job.getDeliveryCity() != null) {
|
||||
String deliveryCities = job.getDeliveryCitiesDisplay();
|
||||
if (job.getPickupCity() != null || deliveryCities != null) {
|
||||
body.append("Route: ");
|
||||
if (job.getPickupCity() != null) {
|
||||
body.append(job.getPickupCity());
|
||||
}
|
||||
if (job.getPickupCity() != null && job.getDeliveryCity() != null) {
|
||||
if (job.getPickupCity() != null && deliveryCities != null) {
|
||||
body.append(" → ");
|
||||
}
|
||||
if (job.getDeliveryCity() != null) {
|
||||
body.append(job.getDeliveryCity());
|
||||
if (deliveryCities != null) {
|
||||
body.append(deliveryCities);
|
||||
}
|
||||
body.append("\n");
|
||||
}
|
||||
@@ -293,16 +295,17 @@ public class EmailService {
|
||||
body.append("Auftraggeber: ").append(job.getCustomerSelection()).append("\n");
|
||||
}
|
||||
|
||||
if (job.getPickupCity() != null || job.getDeliveryCity() != null) {
|
||||
String deliveryCities = job.getDeliveryCitiesDisplay();
|
||||
if (job.getPickupCity() != null || deliveryCities != null) {
|
||||
body.append("Route: ");
|
||||
if (job.getPickupCity() != null) {
|
||||
body.append(job.getPickupCity());
|
||||
}
|
||||
if (job.getPickupCity() != null && job.getDeliveryCity() != null) {
|
||||
if (job.getPickupCity() != null && deliveryCities != null) {
|
||||
body.append(" → ");
|
||||
}
|
||||
if (job.getDeliveryCity() != null) {
|
||||
body.append(job.getDeliveryCity());
|
||||
if (deliveryCities != null) {
|
||||
body.append(deliveryCities);
|
||||
}
|
||||
body.append("\n");
|
||||
}
|
||||
|
||||
@@ -152,8 +152,8 @@ public class TranslationService {
|
||||
log.info("[TranslationCache] Cache size {} exceeds threshold {}, deleting {} oldest entries", count,
|
||||
CACHE_CLEANUP_THRESHOLD, toDelete);
|
||||
|
||||
List<TranslationCacheEntry> oldest = cacheRepository.findOldestEntries(
|
||||
PageRequest.of(0, (int) toDelete, Sort.by(Sort.Direction.ASC, "inserted_at")));
|
||||
List<TranslationCacheEntry> oldest = cacheRepository
|
||||
.findOldestEntries(PageRequest.of(0, (int) toDelete, Sort.by(Sort.Direction.ASC, "inserted_at")));
|
||||
|
||||
cacheRepository.deleteAll(oldest);
|
||||
log.info("[TranslationCache] Deleted {} entries, new size: {}", oldest.size(), cacheRepository.count());
|
||||
|
||||
@@ -446,6 +446,11 @@ addjob.address.delivery.addition.placeholder=Adresszusatz (Lieferung)
|
||||
addjob.address.save=Adresse speichern
|
||||
addjob.section.pickup=Abholung
|
||||
addjob.section.delivery=Lieferung
|
||||
addjob.station.delivery=Lieferstation {0}
|
||||
addjob.station.add=Lieferstation hinzuf\u00fcgen
|
||||
addjob.station.remove.confirm=Lieferstation {0} wirklich entfernen?
|
||||
addjob.station.max.reached=Maximale Anzahl von 25 Lieferstationen erreicht
|
||||
addjob.appointment.delivery.info=Liefertermine werden direkt in den Lieferstationen festgelegt.
|
||||
addjob.tab.addresses=Auftraggeber & Adressen
|
||||
addjob.tab.appointments=Termine & Verarbeitung
|
||||
addjob.tab.cargo=Fracht
|
||||
@@ -568,6 +573,7 @@ jobsummary.notification.complete.error=Fehler beim Abschließen: {0}
|
||||
jobsummary.notification.noappuser=Diesem Auftrag ist kein App-Nutzer zugeordnet
|
||||
jobsummary.section.pickup=Abholung
|
||||
jobsummary.section.delivery=Lieferung
|
||||
jobsummary.station.phone=Telefon
|
||||
jobsummary.section.tasks=Zu quittierende Aufgaben
|
||||
jobsummary.section.cargo=Zu transportierende Fracht
|
||||
jobsummary.section.info=Weitere Informationen
|
||||
|
||||
@@ -446,6 +446,11 @@ addjob.address.delivery.addition.placeholder=Address addition (Delivery)
|
||||
addjob.address.save=Save Address
|
||||
addjob.section.pickup=Pickup
|
||||
addjob.section.delivery=Delivery
|
||||
addjob.station.delivery=Delivery Station {0}
|
||||
addjob.station.add=Add delivery station
|
||||
addjob.station.remove.confirm=Really remove delivery station {0}?
|
||||
addjob.station.max.reached=Maximum of 25 delivery stations reached
|
||||
addjob.appointment.delivery.info=Delivery dates are set directly in the delivery stations.
|
||||
addjob.tab.addresses=Customer & Addresses
|
||||
addjob.tab.appointments=Appointments & Processing
|
||||
addjob.tab.cargo=Cargo
|
||||
@@ -568,6 +573,7 @@ jobsummary.notification.complete.error=Error completing job: {0}
|
||||
jobsummary.notification.noappuser=No app user assigned to this job
|
||||
jobsummary.section.pickup=Pickup
|
||||
jobsummary.section.delivery=Delivery
|
||||
jobsummary.station.phone=Phone
|
||||
jobsummary.section.tasks=Tasks to Confirm
|
||||
jobsummary.section.cargo=Cargo to Transport
|
||||
jobsummary.section.info=Additional Information
|
||||
|
||||
@@ -446,6 +446,11 @@ addjob.address.delivery.addition.placeholder=Complemento de dirección (Entrega)
|
||||
addjob.address.save=Guardar Dirección
|
||||
addjob.section.pickup=Recogida
|
||||
addjob.section.delivery=Entrega
|
||||
addjob.station.delivery=Estaci\u00f3n de entrega {0}
|
||||
addjob.station.add=A\u00f1adir estaci\u00f3n de entrega
|
||||
addjob.station.remove.confirm=\u00bfRealmente eliminar la estaci\u00f3n de entrega {0}?
|
||||
addjob.station.max.reached=Se alcanz\u00f3 el m\u00e1ximo de 25 estaciones de entrega
|
||||
addjob.appointment.delivery.info=Las fechas de entrega se establecen directamente en las estaciones de entrega.
|
||||
addjob.tab.addresses=Cliente y Direcciones
|
||||
addjob.tab.appointments=Citas y Procesamiento
|
||||
addjob.tab.cargo=Carga
|
||||
@@ -568,6 +573,7 @@ jobsummary.notification.complete.error=Error al completar trabajo: {0}
|
||||
jobsummary.notification.noappuser=Este trabajo no tiene un usuario de app asignado
|
||||
jobsummary.section.pickup=Recogida
|
||||
jobsummary.section.delivery=Entrega
|
||||
jobsummary.station.phone=Teléfono
|
||||
jobsummary.section.tasks=Tareas a Confirmar
|
||||
jobsummary.section.cargo=Carga a Transportar
|
||||
jobsummary.section.info=Información Adicional
|
||||
|
||||
@@ -444,8 +444,13 @@ addjob.address.city.placeholder.delivery=Ville (Livraison)
|
||||
addjob.address.delivery.street.placeholder=Rue (Livraison)
|
||||
addjob.address.delivery.addition.placeholder=Ajout d'adresse (Livraison)
|
||||
addjob.address.save=Enregistrer l'Adresse
|
||||
addjob.section.pickup=Enlèvement
|
||||
addjob.section.pickup=Enl\u00e8vement
|
||||
addjob.section.delivery=Livraison
|
||||
addjob.station.delivery=Station de livraison {0}
|
||||
addjob.station.add=Ajouter une station de livraison
|
||||
addjob.station.remove.confirm=Vraiment supprimer la station de livraison {0}?
|
||||
addjob.station.max.reached=Maximum de 25 stations de livraison atteint
|
||||
addjob.appointment.delivery.info=Les dates de livraison sont d\u00e9finies directement dans les stations de livraison.
|
||||
addjob.tab.addresses=Client & Adresses
|
||||
addjob.tab.appointments=Rendez-vous & Traitement
|
||||
addjob.tab.cargo=Cargaison
|
||||
@@ -568,6 +573,7 @@ jobsummary.notification.complete.error=Erreur lors de la terminaison : {0}
|
||||
jobsummary.notification.noappuser=Aucun utilisateur d'app assigné à cet emploi
|
||||
jobsummary.section.pickup=Enlèvement
|
||||
jobsummary.section.delivery=Livraison
|
||||
jobsummary.station.phone=Téléphone
|
||||
jobsummary.section.tasks=Tâches à Confirmer
|
||||
jobsummary.section.cargo=Cargaison à Transporter
|
||||
jobsummary.section.info=Informations Supplémentaires
|
||||
|
||||
Reference in New Issue
Block a user