@@ -84,16 +84,12 @@ class MessagingPublisherImpl implements MessagingPublisher {
|
|||||||
return convertToTranslatedJson(dto, translations);
|
return convertToTranslatedJson(dto, translations);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload instanceof List<?> list && !list.isEmpty()
|
if (payload instanceof List<?> list && !list.isEmpty() && list.get(0) instanceof JobWithRelatedDataDTO) {
|
||||||
&& list.get(0) instanceof JobWithRelatedDataDTO) {
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<JobWithRelatedDataDTO> dtoList = (List<JobWithRelatedDataDTO>) list;
|
List<JobWithRelatedDataDTO> dtoList = (List<JobWithRelatedDataDTO>) list;
|
||||||
|
|
||||||
// Collect all texts from all DTOs and translate in one batch
|
// Collect all texts from all DTOs and translate in one batch
|
||||||
List<String> allTexts = dtoList.stream()
|
List<String> allTexts = dtoList.stream().flatMap(d -> collectTexts(d).stream()).distinct().toList();
|
||||||
.flatMap(d -> collectTexts(d).stream())
|
|
||||||
.distinct()
|
|
||||||
.toList();
|
|
||||||
Map<String, List<TranslationService.Translation>> translations = translationService
|
Map<String, List<TranslationService.Translation>> translations = translationService
|
||||||
.translateBatch(allTexts);
|
.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.LocalDateTime;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Document(collection = "jobs")
|
@Document(collection = "jobs")
|
||||||
@@ -158,6 +160,10 @@ public class Job {
|
|||||||
@Field("invoice_id")
|
@Field("invoice_id")
|
||||||
private String invoiceId;
|
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
|
* 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.
|
* job id is returned as a string when jobs are retrieved via API.
|
||||||
@@ -166,4 +172,59 @@ public class Job {
|
|||||||
public String getIdAsString() {
|
public String getIdAsString() {
|
||||||
return id != null ? id.toString() : null;
|
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;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MongoDB document for caching LLM translations. Stores the original text,
|
* MongoDB document for caching LLM translations. Stores the original text, all
|
||||||
* all translations keyed by language code, and the insertion timestamp.
|
* translations keyed by language code, and the insertion timestamp.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@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 jakarta.annotation.security.RolesAllowed;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import de.assecutor.votianlt.model.CargoItem;
|
import de.assecutor.votianlt.model.CargoItem;
|
||||||
|
import de.assecutor.votianlt.model.DeliveryStation;
|
||||||
import de.assecutor.votianlt.model.AddressValidationResult;
|
import de.assecutor.votianlt.model.AddressValidationResult;
|
||||||
import de.assecutor.votianlt.model.RouteCalculationResult;
|
import de.assecutor.votianlt.model.RouteCalculationResult;
|
||||||
|
import de.assecutor.votianlt.pages.base.ui.component.DeliveryStationTile;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -104,18 +106,11 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
private TextField pickupCity;
|
private TextField pickupCity;
|
||||||
private Checkbox savePickupAddress;
|
private Checkbox savePickupAddress;
|
||||||
|
|
||||||
// Delivery address fields
|
// Delivery station tiles (up to 25)
|
||||||
private ComboBox<String> deliveryCompany;
|
private final List<DeliveryStationTile> deliveryStationTiles = new ArrayList<>();
|
||||||
private ComboBox<String> deliverySalutation;
|
private Div stationsScrollContainer;
|
||||||
private TextField deliveryFirstName;
|
private Div addStationButton;
|
||||||
private TextField deliveryLastName;
|
private static final int MAX_DELIVERY_STATIONS = 25;
|
||||||
private TextField deliveryPhone;
|
|
||||||
private TextField deliveryStreet;
|
|
||||||
private TextField deliveryHouseNumber;
|
|
||||||
private TextField deliveryAddressAddition;
|
|
||||||
private TextField deliveryZip;
|
|
||||||
private TextField deliveryCity;
|
|
||||||
private Checkbox saveDeliveryAddress;
|
|
||||||
|
|
||||||
// Digital processing
|
// Digital processing
|
||||||
private Checkbox digitalProcessing;
|
private Checkbox digitalProcessing;
|
||||||
@@ -140,11 +135,9 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
|
|
||||||
// Date picker fields for appointments
|
// Date picker fields for appointments
|
||||||
private DatePicker pickupDate;
|
private DatePicker pickupDate;
|
||||||
private DatePicker deliveryDate;
|
|
||||||
|
|
||||||
// Time picker fields for appointments
|
// Time picker fields for appointments
|
||||||
private TimePicker pickupTime;
|
private TimePicker pickupTime;
|
||||||
private TimePicker deliveryTime;
|
|
||||||
|
|
||||||
private com.vaadin.flow.component.tabs.Tab addressesTab;
|
private com.vaadin.flow.component.tabs.Tab addressesTab;
|
||||||
private com.vaadin.flow.component.tabs.Tab appointmentsTab;
|
private com.vaadin.flow.component.tabs.Tab appointmentsTab;
|
||||||
@@ -169,7 +162,6 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
private ComboBox<TaskTemplate> templateComboBox;
|
private ComboBox<TaskTemplate> templateComboBox;
|
||||||
private TextArea remarkArea;
|
private TextArea remarkArea;
|
||||||
private VerticalLayout pickupSection;
|
private VerticalLayout pickupSection;
|
||||||
private VerticalLayout deliverySection;
|
|
||||||
|
|
||||||
private final Binder<Job> binder = new Binder<>(Job.class);
|
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 = new Checkbox(getTranslation("addjob.address.save"));
|
||||||
savePickupAddress.setValue(true);
|
savePickupAddress.setValue(true);
|
||||||
|
|
||||||
// Delivery address
|
// Delivery station tiles will be created in createCustomerAndAddressesTab()
|
||||||
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);
|
|
||||||
|
|
||||||
// Digital processing - set value based on user's profile setting
|
// Digital processing - set value based on user's profile setting
|
||||||
digitalProcessing = new Checkbox(getTranslation("profile.settings.digitalprocess"));
|
digitalProcessing = new Checkbox(getTranslation("profile.settings.digitalprocess"));
|
||||||
@@ -414,17 +374,6 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
"Freitag", "Samstag"))
|
"Freitag", "Samstag"))
|
||||||
.setWeekdaysShort(java.util.Arrays.asList("So", "Mo", "Di", "Mi", "Do", "Fr", "Sa")));
|
.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
|
// Submit button - initially disabled until all required fields are valid
|
||||||
submitButton = new Button(getTranslation("addjob.button.submit"), event -> submit());
|
submitButton = new Button(getTranslation("addjob.button.submit"), event -> submit());
|
||||||
submitButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
submitButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
@@ -493,6 +442,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
tabContent.setSizeFull();
|
tabContent.setSizeFull();
|
||||||
tabContent.setPadding(true);
|
tabContent.setPadding(true);
|
||||||
tabContent.setSpacing(true);
|
tabContent.setSpacing(true);
|
||||||
|
tabContent.getStyle().set("overflow-y", "auto");
|
||||||
|
|
||||||
// Customer selection section
|
// Customer selection section
|
||||||
HorizontalLayout customerLayout = new HorizontalLayout();
|
HorizontalLayout customerLayout = new HorizontalLayout();
|
||||||
@@ -504,29 +454,129 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
|
|
||||||
tabContent.add(customerLayout);
|
tabContent.add(customerLayout);
|
||||||
|
|
||||||
// Main content layout with two equal columns (50% each)
|
// Horizontal scrolling container for pickup + delivery station tiles
|
||||||
HorizontalLayout mainLayout = new HorizontalLayout();
|
stationsScrollContainer = new Div();
|
||||||
mainLayout.setWidthFull();
|
stationsScrollContainer.getStyle().set("display", "flex");
|
||||||
mainLayout.setSpacing(true);
|
stationsScrollContainer.getStyle().set("overflow-x", "auto");
|
||||||
mainLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.START);
|
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 = 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
|
// "+" add station button tile
|
||||||
deliverySection = createDeliverySection();
|
addStationButton = createAddStationButton();
|
||||||
deliverySection.setWidth("50%");
|
stationsScrollContainer.add(addStationButton);
|
||||||
|
|
||||||
// Setup focus listeners for input fields
|
// Add first delivery station tile
|
||||||
|
addDeliveryStationTile();
|
||||||
|
|
||||||
|
// Setup focus listeners for pickup input fields
|
||||||
setupInputFieldFocusListeners();
|
setupInputFieldFocusListeners();
|
||||||
|
|
||||||
mainLayout.add(pickupSection, deliverySection);
|
tabContent.add(stationsScrollContainer);
|
||||||
tabContent.add(mainLayout);
|
|
||||||
|
|
||||||
return tabContent;
|
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() {
|
private Component createAppointmentsAndProcessingTab() {
|
||||||
VerticalLayout tabContent = new VerticalLayout();
|
VerticalLayout tabContent = new VerticalLayout();
|
||||||
tabContent.setSizeFull();
|
tabContent.setSizeFull();
|
||||||
@@ -565,17 +615,12 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
pickupTime.setWidth("50%");
|
pickupTime.setWidth("50%");
|
||||||
content.add(pickupApptTitle, pickupApptRow);
|
content.add(pickupApptTitle, pickupApptRow);
|
||||||
|
|
||||||
// Appointment (Delivery)
|
// Info: Delivery dates are set per-station in the tiles
|
||||||
H3 deliveryApptTitle = new H3(getTranslation("addjob.appointment.delivery"));
|
Span deliveryInfoLabel = new Span(getTranslation("addjob.appointment.delivery.info"));
|
||||||
deliveryApptTitle.getStyle().set("margin", "0");
|
deliveryInfoLabel.getStyle().set("color", "var(--lumo-secondary-text-color)");
|
||||||
deliveryTime = new TimePicker(getTranslation("addjob.appointment.time"));
|
deliveryInfoLabel.getStyle().set("font-style", "italic");
|
||||||
deliveryTime.setLocale(java.util.Locale.GERMANY);
|
deliveryInfoLabel.getStyle().set("margin-top", "var(--lumo-space-m)");
|
||||||
HorizontalLayout deliveryApptRow = new HorizontalLayout(deliveryDate, deliveryTime);
|
content.add(deliveryInfoLabel);
|
||||||
deliveryApptRow.setWidthFull();
|
|
||||||
deliveryApptRow.setSpacing(true);
|
|
||||||
deliveryDate.setWidth("50%");
|
|
||||||
deliveryTime.setWidth("50%");
|
|
||||||
content.add(deliveryApptTitle, deliveryApptRow);
|
|
||||||
|
|
||||||
tabContent.add(content);
|
tabContent.add(content);
|
||||||
return tabContent;
|
return tabContent;
|
||||||
@@ -974,10 +1019,16 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
|
|
||||||
HorizontalLayout titleLayout = new HorizontalLayout();
|
HorizontalLayout titleLayout = new HorizontalLayout();
|
||||||
titleLayout.setWidthFull();
|
titleLayout.setWidthFull();
|
||||||
titleLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.START);
|
titleLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
|
||||||
titleLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
titleLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||||
titleLayout.add(title);
|
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
|
// Alle einzelnen Controls auf volle Breite setzen
|
||||||
pickupCompany.setWidthFull();
|
pickupCompany.setWidthFull();
|
||||||
pickupSalutation.setWidthFull();
|
pickupSalutation.setWidthFull();
|
||||||
@@ -1018,64 +1069,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
return section;
|
return section;
|
||||||
}
|
}
|
||||||
|
|
||||||
private VerticalLayout createDeliverySection() {
|
// createDeliverySection() removed - delivery stations are now handled by
|
||||||
VerticalLayout section = new VerticalLayout();
|
// DeliveryStationTile
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupCompanyAutocomplete(ComboBox<String> companyField, boolean isPickup) {
|
private void setupCompanyAutocomplete(ComboBox<String> companyField, boolean isPickup) {
|
||||||
// Get all customers for the current owner
|
// Get all customers for the current owner
|
||||||
@@ -1088,7 +1083,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
// Set items for autocomplete
|
// Set items for autocomplete
|
||||||
companyField.setItems(companyNames);
|
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 -> {
|
companyField.addValueChangeListener(event -> {
|
||||||
String selectedCompany = event.getValue();
|
String selectedCompany = event.getValue();
|
||||||
if (selectedCompany == null || selectedCompany.trim().isEmpty()) {
|
if (selectedCompany == null || selectedCompany.trim().isEmpty()) {
|
||||||
@@ -1105,59 +1101,31 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
if (matchingCustomer.isPresent()) {
|
if (matchingCustomer.isPresent()) {
|
||||||
Customer customer = matchingCustomer.get();
|
Customer customer = matchingCustomer.get();
|
||||||
|
|
||||||
if (isPickup) {
|
// Fill pickup address fields
|
||||||
// Fill pickup address fields
|
if (customer.getTitle() != null
|
||||||
if (customer.getTitle() != null && ("Herr".equalsIgnoreCase(customer.getTitle())
|
&& ("Herr".equalsIgnoreCase(customer.getTitle()) || "Frau".equalsIgnoreCase(customer.getTitle())
|
||||||
|| "Frau".equalsIgnoreCase(customer.getTitle())
|
|| "Divers".equalsIgnoreCase(customer.getTitle()))) {
|
||||||
|| "Divers".equalsIgnoreCase(customer.getTitle()))) {
|
pickupSalutation.setValue(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);
|
|
||||||
}
|
}
|
||||||
|
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();
|
resetRouteInformation();
|
||||||
|
|
||||||
// Reactivate save checkbox for custom values
|
// Reactivate save checkbox for custom values
|
||||||
if (isPickup) {
|
savePickupAddress.setValue(true);
|
||||||
savePickupAddress.setValue(true);
|
|
||||||
} else {
|
|
||||||
saveDeliveryAddress.setValue(true);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1192,51 +1156,30 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
|
|
||||||
binder.forField(pickupCity).asRequired("").bind(Job::getPickupCity, Job::setPickupCity);
|
binder.forField(pickupCity).asRequired("").bind(Job::getPickupCity, Job::setPickupCity);
|
||||||
|
|
||||||
// Bind delivery address fields with validation
|
// Delivery address fields are managed by DeliveryStationTile (not binder)
|
||||||
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);
|
|
||||||
|
|
||||||
// Price wird manuell in submit() berechnet und gesetzt - kein Binder notwendig
|
// 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("")
|
binder.forField(pickupDate).asRequired("")
|
||||||
.withValidator(date -> date == null || !date.isBefore(LocalDate.now()),
|
.withValidator(date -> date == null || !date.isBefore(LocalDate.now()),
|
||||||
getTranslation("addjob.validation.pickupdate.future"))
|
getTranslation("addjob.validation.pickupdate.future"))
|
||||||
.bind(Job::getPickupDate, Job::setPickupDate);
|
.bind(Job::getPickupDate, Job::setPickupDate);
|
||||||
|
|
||||||
binder.forField(deliveryDate).asRequired("")
|
// Delivery dates are now per-station (in DeliveryStationTile)
|
||||||
.withValidator(date -> date == null || !date.isBefore(LocalDate.now()),
|
|
||||||
getTranslation("addjob.validation.deliverydate.future"))
|
|
||||||
.bind(Job::getDeliveryDate, Job::setDeliveryDate);
|
|
||||||
|
|
||||||
// Bind time picker fields (optional)
|
// Bind time picker fields (optional)
|
||||||
binder.bind(pickupTime, Job::getPickupTime, Job::setPickupTime);
|
binder.bind(pickupTime, Job::getPickupTime, Job::setPickupTime);
|
||||||
binder.bind(deliveryTime, Job::getDeliveryTime, Job::setDeliveryTime);
|
|
||||||
|
|
||||||
// Bind customerSelection field with validation
|
// Bind customerSelection field with validation
|
||||||
binder.forField(customerSelection).asRequired("").bind(Job::getCustomerSelection, Job::setCustomerSelection);
|
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(pickupCompany, Job::getPickupCompany, Job::setPickupCompany);
|
||||||
binder.bind(pickupSalutation, Job::getPickupSalutation, Job::setPickupSalutation);
|
binder.bind(pickupSalutation, Job::getPickupSalutation, Job::setPickupSalutation);
|
||||||
binder.bind(pickupPhone, Job::getPickupPhone, Job::setPickupPhone);
|
binder.bind(pickupPhone, Job::getPickupPhone, Job::setPickupPhone);
|
||||||
binder.bind(pickupAddressAddition, Job::getPickupAddressAddition, Job::setPickupAddressAddition);
|
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,
|
binder.forField(digitalProcessing).bind(Job::isDigitalProcessing,
|
||||||
(job, value) -> job.setDigitalProcessing(Boolean.TRUE.equals(value)));
|
(job, value) -> job.setDigitalProcessing(Boolean.TRUE.equals(value)));
|
||||||
|
|
||||||
@@ -1309,11 +1252,10 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
private void setupValidationTriggers() {
|
private void setupValidationTriggers() {
|
||||||
// List of all required fields
|
// List of all required fields
|
||||||
TextField[] requiredTextFields = { pickupFirstName, pickupLastName, pickupStreet, pickupHouseNumber, pickupZip,
|
TextField[] requiredTextFields = { pickupFirstName, pickupLastName, pickupStreet, pickupHouseNumber, pickupZip,
|
||||||
pickupCity, deliveryFirstName, deliveryLastName, deliveryStreet, deliveryHouseNumber, deliveryZip,
|
pickupCity };
|
||||||
deliveryCity };
|
|
||||||
|
|
||||||
// List of required date fields
|
// List of required date fields (delivery dates are per-station in tiles)
|
||||||
DatePicker[] requiredDateFields = { pickupDate, deliveryDate };
|
DatePicker[] requiredDateFields = { pickupDate };
|
||||||
|
|
||||||
// Add validation listener for customerSelection ComboBox
|
// Add validation listener for customerSelection ComboBox
|
||||||
customerSelection.addValueChangeListener(event -> {
|
customerSelection.addValueChangeListener(event -> {
|
||||||
@@ -1426,18 +1368,20 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
|| isFieldEmpty(pickupStreet) || isFieldEmpty(pickupHouseNumber) || isFieldEmpty(pickupZip)
|
|| isFieldEmpty(pickupStreet) || isFieldEmpty(pickupHouseNumber) || isFieldEmpty(pickupZip)
|
||||||
|| isFieldEmpty(pickupCity);
|
|| isFieldEmpty(pickupCity);
|
||||||
|
|
||||||
// Check delivery address fields
|
// Check all delivery station tiles for errors
|
||||||
boolean deliveryErrors = isFieldEmpty(deliveryFirstName) || isFieldEmpty(deliveryLastName)
|
boolean deliveryErrors = deliveryStationTiles.isEmpty()
|
||||||
|| isFieldEmpty(deliveryStreet) || isFieldEmpty(deliveryHouseNumber) || isFieldEmpty(deliveryZip)
|
|| deliveryStationTiles.stream().anyMatch(DeliveryStationTile::hasValidationErrors);
|
||||||
|| isFieldEmpty(deliveryCity);
|
|
||||||
|
|
||||||
return customerSelectionEmpty || pickupErrors || deliveryErrors;
|
return customerSelectionEmpty || pickupErrors || deliveryErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasAppointmentValidationErrors() {
|
private boolean hasAppointmentValidationErrors() {
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now();
|
||||||
return pickupDate.getValue() == null || deliveryDate.getValue() == null || pickupDate.getValue().isBefore(today)
|
// Check pickup date
|
||||||
|| deliveryDate.getValue().isBefore(today);
|
if (pickupDate.getValue() == null || pickupDate.getValue().isBefore(today)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasCargoValidationErrors() {
|
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
|
// Zusätzliche Felder, die nicht über den Binder gebunden sind, manuell setzen
|
||||||
job.setPickupDate(pickupDate.getValue());
|
job.setPickupDate(pickupDate.getValue());
|
||||||
job.setPickupTime(pickupTime.getValue());
|
job.setPickupTime(pickupTime.getValue());
|
||||||
job.setDeliveryDate(deliveryDate.getValue());
|
|
||||||
job.setDeliveryTime(deliveryTime.getValue());
|
|
||||||
if (remarkArea != null)
|
if (remarkArea != null)
|
||||||
job.setRemark(remarkArea.getValue());
|
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
|
// Store selected service IDs in job for invoice creation
|
||||||
job.setServiceIds(selectedServices.stream().map(Service::getId).toList());
|
job.setServiceIds(selectedServices.stream().map(Service::getId).toList());
|
||||||
|
|
||||||
@@ -1584,19 +1539,23 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
pickupCustomer.setCity(pickupCity.getValue());
|
pickupCustomer.setCity(pickupCity.getValue());
|
||||||
addCustomerService.addCustomer(pickupCustomer);
|
addCustomerService.addCustomer(pickupCustomer);
|
||||||
}
|
}
|
||||||
if (saveDeliveryAddress.getValue()) {
|
// Save delivery station addresses as customers if checkbox is checked
|
||||||
Customer deliveryCustomer = new Customer();
|
for (DeliveryStationTile tile : deliveryStationTiles) {
|
||||||
deliveryCustomer.setCompanyName(deliveryCompany.getValue());
|
if (tile.isSaveAddressChecked()) {
|
||||||
deliveryCustomer.setTitle(deliverySalutation.getValue());
|
DeliveryStation ds = tile.getDeliveryStation();
|
||||||
deliveryCustomer.setFirstname(deliveryFirstName.getValue());
|
Customer deliveryCustomer = new Customer();
|
||||||
deliveryCustomer.setLastName(deliveryLastName.getValue());
|
deliveryCustomer.setCompanyName(ds.getCompany());
|
||||||
deliveryCustomer.setTelephone(deliveryPhone.getValue());
|
deliveryCustomer.setTitle(ds.getSalutation());
|
||||||
deliveryCustomer.setStreet(deliveryStreet.getValue());
|
deliveryCustomer.setFirstname(ds.getFirstName());
|
||||||
deliveryCustomer.setHouseNumber(deliveryHouseNumber.getValue());
|
deliveryCustomer.setLastName(ds.getLastName());
|
||||||
deliveryCustomer.setAddressAddition(deliveryAddressAddition.getValue());
|
deliveryCustomer.setTelephone(ds.getPhone());
|
||||||
deliveryCustomer.setZip(deliveryZip.getValue());
|
deliveryCustomer.setStreet(ds.getStreet());
|
||||||
deliveryCustomer.setCity(deliveryCity.getValue());
|
deliveryCustomer.setHouseNumber(ds.getHouseNumber());
|
||||||
addCustomerService.addCustomer(deliveryCustomer);
|
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
|
// All validations passed, save the job with cargo items and tasks
|
||||||
@@ -1986,18 +1945,18 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
pickupCity.clear();
|
pickupCity.clear();
|
||||||
savePickupAddress.setValue(false);
|
savePickupAddress.setValue(false);
|
||||||
|
|
||||||
// Delivery address
|
// Delivery stations - remove all but the first, clear the first
|
||||||
deliveryCompany.clear();
|
while (deliveryStationTiles.size() > 1) {
|
||||||
deliverySalutation.clear();
|
DeliveryStationTile tile = deliveryStationTiles.remove(deliveryStationTiles.size() - 1);
|
||||||
deliveryFirstName.clear();
|
stationsScrollContainer.remove(tile);
|
||||||
deliveryLastName.clear();
|
}
|
||||||
deliveryPhone.clear();
|
if (!deliveryStationTiles.isEmpty()) {
|
||||||
deliveryStreet.clear();
|
deliveryStationTiles.get(0).clearFields();
|
||||||
deliveryHouseNumber.clear();
|
}
|
||||||
deliveryAddressAddition.clear();
|
// Ensure "+" button is visible
|
||||||
deliveryZip.clear();
|
if (addStationButton.getParent().isEmpty()) {
|
||||||
deliveryCity.clear();
|
stationsScrollContainer.add(addStationButton);
|
||||||
saveDeliveryAddress.setValue(false);
|
}
|
||||||
|
|
||||||
// Digital processing
|
// Digital processing
|
||||||
digitalProcessing.setValue(true);
|
digitalProcessing.setValue(true);
|
||||||
@@ -2045,27 +2004,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
pickupCity.addFocusListener(e -> disableDragSources());
|
pickupCity.addFocusListener(e -> disableDragSources());
|
||||||
pickupCity.addBlurListener(e -> enableDragSources());
|
pickupCity.addBlurListener(e -> enableDragSources());
|
||||||
|
|
||||||
// Delivery fields
|
// Delivery fields are handled inside DeliveryStationTile
|
||||||
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());
|
|
||||||
|
|
||||||
// Digital processing
|
// Digital processing
|
||||||
appUser.addFocusListener(e -> disableDragSources());
|
appUser.addFocusListener(e -> disableDragSources());
|
||||||
@@ -2080,10 +2019,6 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
pickupSection.getStyle().set("pointer-events", "none");
|
pickupSection.getStyle().set("pointer-events", "none");
|
||||||
pickupSection.getElement().setAttribute("draggable", "false");
|
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.getStyle().remove("pointer-events");
|
||||||
pickupSection.getElement().setAttribute("draggable", "true");
|
pickupSection.getElement().setAttribute("draggable", "true");
|
||||||
}
|
}
|
||||||
if (deliverySection != null) {
|
|
||||||
deliverySection.getStyle().remove("pointer-events");
|
|
||||||
deliverySection.getElement().setAttribute("draggable", "true");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createTaskRow() {
|
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() {
|
private boolean hasDeliveryAddressChanged() {
|
||||||
String currentStreet = getValueOrEmpty(deliveryStreet);
|
if (!addressesDirty) {
|
||||||
String currentZip = getValueOrEmpty(deliveryZip);
|
return false;
|
||||||
String currentCity = getValueOrEmpty(deliveryCity);
|
}
|
||||||
|
return deliveryStationTiles.stream().anyMatch(DeliveryStationTile::hasAddressForValidation);
|
||||||
// Nur true zurückgeben, wenn alle Pflichtfelder ausgefüllt sind und Validierung
|
|
||||||
// nötig ist
|
|
||||||
return addressesDirty && !currentStreet.isEmpty() && !currentZip.isEmpty() && !currentCity.isEmpty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getValueOrEmpty(TextField field) {
|
private String getValueOrEmpty(TextField field) {
|
||||||
@@ -3023,16 +2951,19 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
final String pickupZipValue = getValueOrEmpty(pickupZip);
|
final String pickupZipValue = getValueOrEmpty(pickupZip);
|
||||||
final String pickupCityValue = getValueOrEmpty(pickupCity);
|
final String pickupCityValue = getValueOrEmpty(pickupCity);
|
||||||
final boolean pickupChanged = hasPickupAddressChanged();
|
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();
|
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
|
// Asynchrone Validierung im Hintergrund durchführen
|
||||||
getUI().ifPresent(ui -> {
|
getUI().ifPresent(ui -> {
|
||||||
// CompletableFuture für Hintergrund-Verarbeitung
|
|
||||||
java.util.concurrent.CompletableFuture.runAsync(() -> {
|
java.util.concurrent.CompletableFuture.runAsync(() -> {
|
||||||
// Abholadresse validieren
|
// Abholadresse validieren
|
||||||
final AddressValidationResult[] pickupResultHolder = new AddressValidationResult[1];
|
final AddressValidationResult[] pickupResultHolder = new AddressValidationResult[1];
|
||||||
@@ -3041,45 +2972,67 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
pickupHouseNumberValue, pickupZipValue, pickupCityValue);
|
pickupHouseNumberValue, pickupZipValue, pickupCityValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lieferadresse validieren
|
// Alle Lieferstationen validieren
|
||||||
final AddressValidationResult[] deliveryResultHolder = new AddressValidationResult[1];
|
final List<AddressValidationResult> deliveryResults = new ArrayList<>();
|
||||||
if (deliveryChanged) {
|
if (deliveryChanged) {
|
||||||
deliveryResultHolder[0] = addressValidationService.validateAddress("delivery", deliveryStreetValue,
|
for (int i = 0; i < stationData.size(); i++) {
|
||||||
deliveryHouseNumberValue, deliveryZipValue, deliveryCityValue);
|
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 aktualisieren mit den Ergebnissen
|
||||||
ui.access(() -> {
|
ui.access(() -> {
|
||||||
// Ergebnisse speichern
|
|
||||||
if (pickupResultHolder[0] != null) {
|
if (pickupResultHolder[0] != null) {
|
||||||
addressValidationResults.put("pickup", pickupResultHolder[0]);
|
addressValidationResults.put("pickup", pickupResultHolder[0]);
|
||||||
}
|
}
|
||||||
if (deliveryResultHolder[0] != null) {
|
for (int i = 0; i < deliveryResults.size(); i++) {
|
||||||
addressValidationResults.put("delivery", deliveryResultHolder[0]);
|
addressValidationResults.put("delivery_" + i, deliveryResults.get(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ergebnisse ermitteln
|
|
||||||
AddressValidationResult pickupResult = pickupResultHolder[0] != null ? pickupResultHolder[0]
|
AddressValidationResult pickupResult = pickupResultHolder[0] != null ? pickupResultHolder[0]
|
||||||
: addressValidationResults.get("pickup");
|
: 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
|
// Lade-Anzeige ausblenden
|
||||||
loadingMessage.setVisible(false);
|
loadingMessage.setVisible(false);
|
||||||
progressBar.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())
|
boolean bothValid = (pickupResult != null && pickupResult.isValid())
|
||||||
&& (deliveryResult != null && deliveryResult.isValid());
|
&& (firstDeliveryResult != null && firstDeliveryResult.isValid());
|
||||||
|
|
||||||
// Route berechnen wenn beide gültig
|
// Route berechnen wenn beide gültig
|
||||||
if (bothValid) {
|
if (bothValid) {
|
||||||
routeCalculationResult = addressValidationService.calculateRoute(pickupResult, deliveryResult);
|
routeCalculationResult = addressValidationService.calculateRoute(pickupResult,
|
||||||
|
firstDeliveryResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ergebnisse anzeigen
|
// Ergebnisse anzeigen - show first delivery station result in existing UI
|
||||||
updateValidationDialogResults(pickupResult, deliveryResult, pickupResultLabel, deliveryResultLabel,
|
updateValidationDialogResults(pickupResult, firstDeliveryResult, pickupResultLabel,
|
||||||
routeResultLabel, resultLayout, buttonLayout, continueButton, targetTab);
|
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);
|
pickupZip.getStyle().set("--vaadin-input-field-background", pickupColor);
|
||||||
pickupCity.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
|
// Delivery station field styling is handled inside the tiles
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3238,11 +3182,11 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gibt das Validierungsergebnis für die Lieferadresse zurück. Kann null sein,
|
* Gibt das Validierungsergebnis für die erste Lieferadresse zurück. Kann null
|
||||||
* wenn noch keine Validierung durchgeführt wurde.
|
* sein, wenn noch keine Validierung durchgeführt wurde.
|
||||||
*/
|
*/
|
||||||
public AddressValidationResult getDeliveryAddressValidationResult() {
|
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
|
// Delivery station field change listeners are handled via tile callbacks
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -681,10 +681,10 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
pdfFrame.getStyle().set("border", "none");
|
pdfFrame.getStyle().set("border", "none");
|
||||||
|
|
||||||
Button downloadButton = new Button("Herunterladen", e -> {
|
Button downloadButton = new Button("Herunterladen", e -> {
|
||||||
parent.getElement().executeJs("const link = document.createElement('a');"
|
parent.getElement()
|
||||||
+ "link.href = 'data:application/pdf;base64," + base64Pdf + "';"
|
.executeJs("const link = document.createElement('a');" + "link.href = 'data:application/pdf;base64,"
|
||||||
+ "link.download = '" + title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf';"
|
+ base64Pdf + "';" + "link.download = '" + title.replaceAll("[^a-zA-Z0-9\\-]", "_")
|
||||||
+ "link.click();");
|
+ ".pdf';" + "link.click();");
|
||||||
});
|
});
|
||||||
downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
|
|
||||||
|
|||||||
@@ -877,8 +877,8 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
currentUser, prefixField.getValue());
|
currentUser, prefixField.getValue());
|
||||||
showPdfInDialog(pdfBytes);
|
showPdfInDialog(pdfBytes);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Notification.show(getTranslation("profile.invoice.pdf.preview.error", ex.getMessage()), 3000,
|
Notification.show(getTranslation("profile.invoice.pdf.preview.error", ex.getMessage()),
|
||||||
Notification.Position.BOTTOM_CENTER);
|
3000, Notification.Position.BOTTOM_CENTER);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -1027,10 +1027,10 @@ public class EditProfileView extends HorizontalLayout implements HasDynamicTitle
|
|||||||
"image");
|
"image");
|
||||||
|
|
||||||
panel.add(invoiceHeader, senderCompany, senderName, senderAddress, senderCity, senderEmail, senderPhone,
|
panel.add(invoiceHeader, senderCompany, senderName, senderAddress, senderCity, senderEmail, senderPhone,
|
||||||
invoiceNumber, servicesHeader, servicesListBlock, servicesNetBlock, servicesVatBlock, servicesGrossBlock,
|
invoiceNumber, servicesHeader, servicesListBlock, servicesNetBlock, servicesVatBlock,
|
||||||
customerHeader, customerCompany, customerName, customerAddress, customerCity, customerEmail,
|
servicesGrossBlock, customerHeader, customerCompany, customerName, customerAddress, customerCity,
|
||||||
customerPhone, freeHeader, textBlock, headerBlock, dateBlock, customerBlock, companyBlock, amountBlock,
|
customerEmail, customerPhone, freeHeader, textBlock, headerBlock, dateBlock, customerBlock,
|
||||||
lineBlock, imageBlock);
|
companyBlock, amountBlock, lineBlock, imageBlock);
|
||||||
|
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import com.vaadin.flow.router.HasDynamicTitle;
|
|||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
import com.vaadin.flow.theme.lumo.LumoUtility;
|
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||||
import de.assecutor.votianlt.model.CargoItem;
|
import de.assecutor.votianlt.model.CargoItem;
|
||||||
|
import de.assecutor.votianlt.model.DeliveryStation;
|
||||||
import de.assecutor.votianlt.model.Job;
|
import de.assecutor.votianlt.model.Job;
|
||||||
import de.assecutor.votianlt.model.LocationPosition;
|
import de.assecutor.votianlt.model.LocationPosition;
|
||||||
import de.assecutor.votianlt.model.task.BaseTask;
|
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(concatAddress(job.getPickupStreet(), job.getPickupHouseNumber())));
|
||||||
pickupBox.add(new Span(concatZipCity(job.getPickupZip(), job.getPickupCity())));
|
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%");
|
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);
|
content.add(topRow);
|
||||||
|
|
||||||
// Aufgaben
|
// Aufgaben
|
||||||
@@ -271,13 +303,16 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
|
|
||||||
// Preis basierend auf den hinterlegten Leistungen berechnen
|
// Preis basierend auf den hinterlegten Leistungen berechnen
|
||||||
PriceCalculationResult priceResult = calculatePriceFromServices(job);
|
PriceCalculationResult priceResult = calculatePriceFromServices(job);
|
||||||
|
|
||||||
Div priceTable = new Div();
|
Div priceTable = new Div();
|
||||||
priceTable.getStyle().set("width", "100%");
|
priceTable.getStyle().set("width", "100%");
|
||||||
|
|
||||||
priceTable.add(createPriceRow(getTranslation("jobsummary.info.netto") + ":", formatPrice(priceResult.netAmount()), false));
|
priceTable.add(createPriceRow(getTranslation("jobsummary.info.netto") + ":",
|
||||||
priceTable.add(createPriceRow(getTranslation("jobsummary.info.ust") + ":", formatPrice(priceResult.vatAmount()), false));
|
formatPrice(priceResult.netAmount()), false));
|
||||||
priceTable.add(createPriceRow(getTranslation("jobsummary.info.gesamt") + ":", formatPrice(priceResult.totalAmount()), true));
|
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);
|
infoBox.add(priceTable);
|
||||||
|
|
||||||
@@ -461,8 +496,32 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
// Baue Adress-Strings
|
// Baue Adress-Strings
|
||||||
String origin = (concatAddress(job.getPickupStreet(), job.getPickupHouseNumber()) + ", "
|
String origin = (concatAddress(job.getPickupStreet(), job.getPickupHouseNumber()) + ", "
|
||||||
+ concatZipCity(job.getPickupZip(), job.getPickupCity())).trim();
|
+ 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()) {
|
if (origin.isBlank() || destination.isBlank()) {
|
||||||
return;
|
return;
|
||||||
@@ -504,15 +563,16 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
Integer savedDuration = job.getRouteDurationSeconds();
|
Integer savedDuration = job.getRouteDurationSeconds();
|
||||||
boolean hasSavedRouteData = savedDistance != null && savedDuration != null;
|
boolean hasSavedRouteData = savedDistance != null && savedDuration != null;
|
||||||
|
|
||||||
String js = buildMapJs(origin, destination, hasPosition, position, appUserId, shouldUpdate, hasSavedRouteData,
|
String js = buildMapJs(origin, destination, waypoints, hasPosition, position, appUserId, shouldUpdate,
|
||||||
savedDistance != null ? savedDistance : 0.0, savedDuration != null ? savedDuration : 0);
|
hasSavedRouteData, savedDistance != null ? savedDistance : 0.0,
|
||||||
|
savedDuration != null ? savedDuration : 0);
|
||||||
|
|
||||||
map.getElement().executeJs(js, map.getElement(), routeInfo.getElement());
|
map.getElement().executeJs(js, map.getElement(), routeInfo.getElement());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildMapJs(String origin, String destination, boolean hasPosition, LocationPosition position,
|
private String buildMapJs(String origin, String destination, List<String> waypoints, boolean hasPosition,
|
||||||
String appUserId, boolean shouldUpdate, boolean hasSavedRouteData, double savedDistance,
|
LocationPosition position, String appUserId, boolean shouldUpdate, boolean hasSavedRouteData,
|
||||||
int savedDuration) {
|
double savedDistance, int savedDuration) {
|
||||||
String apiKey = getGoogleMapsApiKey();
|
String apiKey = getGoogleMapsApiKey();
|
||||||
// Explizit mit Punkt als Dezimaltrennzeichen formatieren
|
// Explizit mit Punkt als Dezimaltrennzeichen formatieren
|
||||||
String lat = hasPosition ? String.format(java.util.Locale.US, "%.6f", position.getLatitude()) : "0";
|
String lat = hasPosition ? String.format(java.util.Locale.US, "%.6f", position.getLatitude()) : "0";
|
||||||
@@ -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 savedDurationText = hours > 0 ? String.format("%d Std. %d Min.", hours, minutes)
|
||||||
: String.format("%d Min.", 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 """
|
return """
|
||||||
(function(){
|
(function(){
|
||||||
var host = $0;
|
var host = $0;
|
||||||
@@ -541,6 +612,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
var hasSavedRouteData = %s;
|
var hasSavedRouteData = %s;
|
||||||
var savedDistance = %s;
|
var savedDistance = %s;
|
||||||
var savedDurationText = '%s';
|
var savedDurationText = '%s';
|
||||||
|
var waypoints = %s;
|
||||||
|
|
||||||
var appUserMarker = null;
|
var appUserMarker = null;
|
||||||
var updateInterval = null;
|
var updateInterval = null;
|
||||||
@@ -557,7 +629,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
trafficLayer.setMap(map);
|
trafficLayer.setMap(map);
|
||||||
|
|
||||||
var ds = new google.maps.DirectionsService();
|
var ds = new google.maps.DirectionsService();
|
||||||
ds.route({
|
var routeRequest = {
|
||||||
origin: origin,
|
origin: origin,
|
||||||
destination: destination,
|
destination: destination,
|
||||||
travelMode: google.maps.TravelMode.DRIVING,
|
travelMode: google.maps.TravelMode.DRIVING,
|
||||||
@@ -566,7 +638,12 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
departureTime: new Date(),
|
departureTime: new Date(),
|
||||||
trafficModel: google.maps.TrafficModel.BEST_GUESS
|
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'){
|
if(status === 'OK'){
|
||||||
infoEl.innerHTML = '';
|
infoEl.innerHTML = '';
|
||||||
|
|
||||||
@@ -684,7 +761,8 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
"""
|
"""
|
||||||
.formatted(escapeJs(origin), escapeJs(destination), escapeJs(apiKey), lat, lng,
|
.formatted(escapeJs(origin), escapeJs(destination), escapeJs(apiKey), lat, lng,
|
||||||
Boolean.toString(hasPosition), escapeJs(appUserId), Boolean.toString(shouldUpdate),
|
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
|
// Hilfsfunktion zum einfachen Escapen von JS-Zeichen in Strings
|
||||||
|
|||||||
@@ -116,8 +116,8 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
.setSortable(true);
|
.setSortable(true);
|
||||||
grid.addColumn(job -> DateTimeFormatUtil.formatDateTime(job.getCreatedAt()))
|
grid.addColumn(job -> DateTimeFormatUtil.formatDateTime(job.getCreatedAt()))
|
||||||
.setHeader(getTranslation("jobs.column.jobdate")).setAutoWidth(true).setSortable(true);
|
.setHeader(getTranslation("jobs.column.jobdate")).setAutoWidth(true).setSortable(true);
|
||||||
grid.addColumn(Job::getDeliveryCity).setHeader(getTranslation("jobs.column.destination")).setAutoWidth(true)
|
grid.addColumn(Job::getFirstDeliveryCity).setHeader(getTranslation("jobs.column.destination"))
|
||||||
.setFlexGrow(1).setSortable(true);
|
.setAutoWidth(true).setFlexGrow(1).setSortable(true);
|
||||||
|
|
||||||
// Action column: manual completion for jobs without digital processing
|
// Action column: manual completion for jobs without digital processing
|
||||||
grid.addComponentColumn(job -> {
|
grid.addComponentColumn(job -> {
|
||||||
@@ -144,16 +144,14 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
|
|||||||
invoiceBtn.setTooltipText(getTranslation("jobs.tooltip.showinvoice"));
|
invoiceBtn.setTooltipText(getTranslation("jobs.tooltip.showinvoice"));
|
||||||
invoiceBtn.addClickListener(e -> {
|
invoiceBtn.addClickListener(e -> {
|
||||||
e.getSource().getElement().getNode();
|
e.getSource().getElement().getNode();
|
||||||
customerInvoiceRepository.findById(job.getInvoiceId()).ifPresentOrElse(
|
customerInvoiceRepository.findById(job.getInvoiceId()).ifPresentOrElse(invoice -> {
|
||||||
invoice -> {
|
if (invoice.getPdfData() != null) {
|
||||||
if (invoice.getPdfData() != null) {
|
CreateInvoiceView.showSavedInvoiceDialog(invoice.getPdfData(),
|
||||||
CreateInvoiceView.showSavedInvoiceDialog(invoice.getPdfData(),
|
invoice.getInvoiceNumber(), this);
|
||||||
invoice.getInvoiceNumber(), this);
|
} else {
|
||||||
} else {
|
getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString()));
|
||||||
getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString()));
|
}
|
||||||
}
|
}, () -> getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString())));
|
||||||
},
|
|
||||||
() -> getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString())));
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
invoiceBtn.setTooltipText(getTranslation("jobs.tooltip.createinvoice"));
|
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(extractCompanyName(job.getCustomerSelection()))).append(",");
|
||||||
csv.append(escapeCsv(job.getJobNumber())).append(",");
|
csv.append(escapeCsv(job.getJobNumber())).append(",");
|
||||||
csv.append(DateTimeFormatUtil.formatDateTime(job.getCreatedAt())).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();
|
return csv.toString();
|
||||||
|
|||||||
@@ -257,8 +257,7 @@ public class CustomerInvoiceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData, de.assecutor.votianlt.model.User user,
|
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData, de.assecutor.votianlt.model.User user,
|
||||||
String invoicePrefix)
|
String invoicePrefix) throws Exception {
|
||||||
throws Exception {
|
|
||||||
// Parse the JSON template data
|
// Parse the JSON template data
|
||||||
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonTemplateData);
|
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("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;");
|
htmlBuilder.append("top:").append(String.format(java.util.Locale.US, "%.2f", mmY)).append("mm;");
|
||||||
if ("line".equals(type)) {
|
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;");
|
htmlBuilder.append("height:0;border-top:1px solid #333;");
|
||||||
} else if ("vline".equals(type)) {
|
} else if ("vline".equals(type)) {
|
||||||
htmlBuilder.append("width:0;border-left:1px solid #333;");
|
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 {
|
} 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))
|
htmlBuilder.append("height:").append(String.format(java.util.Locale.US, "%.2f", mmHeight))
|
||||||
.append("mm;");
|
.append("mm;");
|
||||||
htmlBuilder.append("font-size:").append(fontSize).append("pt;");
|
htmlBuilder.append("font-size:").append(fontSize).append("pt;");
|
||||||
htmlBuilder.append("line-height:").append(String.format(java.util.Locale.US, "%.2f", fontSize * 1.2))
|
htmlBuilder.append("line-height:")
|
||||||
.append("pt;");
|
.append(String.format(java.util.Locale.US, "%.2f", fontSize * 1.2)).append("pt;");
|
||||||
htmlBuilder.append("color:").append(color).append(";");
|
htmlBuilder.append("color:").append(color).append(";");
|
||||||
// For services.list use block display to allow table to fill width
|
// For services.list use block display to allow table to fill width
|
||||||
if ("services.list".equals(variable)) {
|
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("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;");
|
htmlBuilder.append("top:").append(String.format(java.util.Locale.US, "%.2f", mmY)).append("mm;");
|
||||||
if ("line".equals(type)) {
|
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;");
|
htmlBuilder.append("height:0;border-top:1px solid #333;");
|
||||||
} else if ("vline".equals(type)) {
|
} else if ("vline".equals(type)) {
|
||||||
htmlBuilder.append("width:0;border-left:1px solid #333;");
|
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 {
|
} 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))
|
htmlBuilder.append("height:").append(String.format(java.util.Locale.US, "%.2f", mmHeight))
|
||||||
.append("mm;");
|
.append("mm;");
|
||||||
htmlBuilder.append("font-size:").append(fontSize).append("pt;");
|
htmlBuilder.append("font-size:").append(fontSize).append("pt;");
|
||||||
htmlBuilder.append("line-height:").append(String.format(java.util.Locale.US, "%.2f", fontSize * 1.2))
|
htmlBuilder.append("line-height:")
|
||||||
.append("pt;");
|
.append(String.format(java.util.Locale.US, "%.2f", fontSize * 1.2)).append("pt;");
|
||||||
htmlBuilder.append("color:").append(color).append(";");
|
htmlBuilder.append("color:").append(color).append(";");
|
||||||
// For services.list use block display
|
// For services.list use block display
|
||||||
if ("services.list".equals(variable)) {
|
if ("services.list".equals(variable)) {
|
||||||
@@ -706,7 +711,8 @@ public class CustomerInvoiceService {
|
|||||||
.replace("job.", "") + "]";
|
.replace("job.", "") + "]";
|
||||||
System.out.println("DEBUG: No value for variable " + variable + ", using placeholder");
|
System.out.println("DEBUG: No value for variable " + variable + ", using placeholder");
|
||||||
} else if ("customer".equals(type)) {
|
} 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",
|
String line1 = variables.getOrDefault("customer.company_name",
|
||||||
variables.getOrDefault("customer.contact_name", ""));
|
variables.getOrDefault("customer.contact_name", ""));
|
||||||
String line2 = variables.getOrDefault("customer.street", "");
|
String line2 = variables.getOrDefault("customer.street", "");
|
||||||
|
|||||||
@@ -85,16 +85,17 @@ public class EmailService {
|
|||||||
body.append("Aufgabe: ").append(taskTypeName).append("\n");
|
body.append("Aufgabe: ").append(taskTypeName).append("\n");
|
||||||
body.append("Abgeschlossen von: ").append(appUserName).append("\n\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: ");
|
body.append("Route: ");
|
||||||
if (job.getPickupCity() != null) {
|
if (job.getPickupCity() != null) {
|
||||||
body.append(job.getPickupCity());
|
body.append(job.getPickupCity());
|
||||||
}
|
}
|
||||||
if (job.getPickupCity() != null && job.getDeliveryCity() != null) {
|
if (job.getPickupCity() != null && deliveryCities != null) {
|
||||||
body.append(" → ");
|
body.append(" → ");
|
||||||
}
|
}
|
||||||
if (job.getDeliveryCity() != null) {
|
if (deliveryCities != null) {
|
||||||
body.append(job.getDeliveryCity());
|
body.append(deliveryCities);
|
||||||
}
|
}
|
||||||
body.append("\n\n");
|
body.append("\n\n");
|
||||||
}
|
}
|
||||||
@@ -204,16 +205,17 @@ public class EmailService {
|
|||||||
body.append("Kunde: ").append(job.getDeliveryCompany()).append("\n");
|
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: ");
|
body.append("Route: ");
|
||||||
if (job.getPickupCity() != null) {
|
if (job.getPickupCity() != null) {
|
||||||
body.append(job.getPickupCity());
|
body.append(job.getPickupCity());
|
||||||
}
|
}
|
||||||
if (job.getPickupCity() != null && job.getDeliveryCity() != null) {
|
if (job.getPickupCity() != null && deliveryCities != null) {
|
||||||
body.append(" → ");
|
body.append(" → ");
|
||||||
}
|
}
|
||||||
if (job.getDeliveryCity() != null) {
|
if (deliveryCities != null) {
|
||||||
body.append(job.getDeliveryCity());
|
body.append(deliveryCities);
|
||||||
}
|
}
|
||||||
body.append("\n");
|
body.append("\n");
|
||||||
}
|
}
|
||||||
@@ -293,16 +295,17 @@ public class EmailService {
|
|||||||
body.append("Auftraggeber: ").append(job.getCustomerSelection()).append("\n");
|
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: ");
|
body.append("Route: ");
|
||||||
if (job.getPickupCity() != null) {
|
if (job.getPickupCity() != null) {
|
||||||
body.append(job.getPickupCity());
|
body.append(job.getPickupCity());
|
||||||
}
|
}
|
||||||
if (job.getPickupCity() != null && job.getDeliveryCity() != null) {
|
if (job.getPickupCity() != null && deliveryCities != null) {
|
||||||
body.append(" → ");
|
body.append(" → ");
|
||||||
}
|
}
|
||||||
if (job.getDeliveryCity() != null) {
|
if (deliveryCities != null) {
|
||||||
body.append(job.getDeliveryCity());
|
body.append(deliveryCities);
|
||||||
}
|
}
|
||||||
body.append("\n");
|
body.append("\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,8 +152,8 @@ public class TranslationService {
|
|||||||
log.info("[TranslationCache] Cache size {} exceeds threshold {}, deleting {} oldest entries", count,
|
log.info("[TranslationCache] Cache size {} exceeds threshold {}, deleting {} oldest entries", count,
|
||||||
CACHE_CLEANUP_THRESHOLD, toDelete);
|
CACHE_CLEANUP_THRESHOLD, toDelete);
|
||||||
|
|
||||||
List<TranslationCacheEntry> oldest = cacheRepository.findOldestEntries(
|
List<TranslationCacheEntry> oldest = cacheRepository
|
||||||
PageRequest.of(0, (int) toDelete, Sort.by(Sort.Direction.ASC, "inserted_at")));
|
.findOldestEntries(PageRequest.of(0, (int) toDelete, Sort.by(Sort.Direction.ASC, "inserted_at")));
|
||||||
|
|
||||||
cacheRepository.deleteAll(oldest);
|
cacheRepository.deleteAll(oldest);
|
||||||
log.info("[TranslationCache] Deleted {} entries, new size: {}", oldest.size(), cacheRepository.count());
|
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.address.save=Adresse speichern
|
||||||
addjob.section.pickup=Abholung
|
addjob.section.pickup=Abholung
|
||||||
addjob.section.delivery=Lieferung
|
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.addresses=Auftraggeber & Adressen
|
||||||
addjob.tab.appointments=Termine & Verarbeitung
|
addjob.tab.appointments=Termine & Verarbeitung
|
||||||
addjob.tab.cargo=Fracht
|
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.notification.noappuser=Diesem Auftrag ist kein App-Nutzer zugeordnet
|
||||||
jobsummary.section.pickup=Abholung
|
jobsummary.section.pickup=Abholung
|
||||||
jobsummary.section.delivery=Lieferung
|
jobsummary.section.delivery=Lieferung
|
||||||
|
jobsummary.station.phone=Telefon
|
||||||
jobsummary.section.tasks=Zu quittierende Aufgaben
|
jobsummary.section.tasks=Zu quittierende Aufgaben
|
||||||
jobsummary.section.cargo=Zu transportierende Fracht
|
jobsummary.section.cargo=Zu transportierende Fracht
|
||||||
jobsummary.section.info=Weitere Informationen
|
jobsummary.section.info=Weitere Informationen
|
||||||
|
|||||||
@@ -446,6 +446,11 @@ addjob.address.delivery.addition.placeholder=Address addition (Delivery)
|
|||||||
addjob.address.save=Save Address
|
addjob.address.save=Save Address
|
||||||
addjob.section.pickup=Pickup
|
addjob.section.pickup=Pickup
|
||||||
addjob.section.delivery=Delivery
|
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.addresses=Customer & Addresses
|
||||||
addjob.tab.appointments=Appointments & Processing
|
addjob.tab.appointments=Appointments & Processing
|
||||||
addjob.tab.cargo=Cargo
|
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.notification.noappuser=No app user assigned to this job
|
||||||
jobsummary.section.pickup=Pickup
|
jobsummary.section.pickup=Pickup
|
||||||
jobsummary.section.delivery=Delivery
|
jobsummary.section.delivery=Delivery
|
||||||
|
jobsummary.station.phone=Phone
|
||||||
jobsummary.section.tasks=Tasks to Confirm
|
jobsummary.section.tasks=Tasks to Confirm
|
||||||
jobsummary.section.cargo=Cargo to Transport
|
jobsummary.section.cargo=Cargo to Transport
|
||||||
jobsummary.section.info=Additional Information
|
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.address.save=Guardar Dirección
|
||||||
addjob.section.pickup=Recogida
|
addjob.section.pickup=Recogida
|
||||||
addjob.section.delivery=Entrega
|
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.addresses=Cliente y Direcciones
|
||||||
addjob.tab.appointments=Citas y Procesamiento
|
addjob.tab.appointments=Citas y Procesamiento
|
||||||
addjob.tab.cargo=Carga
|
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.notification.noappuser=Este trabajo no tiene un usuario de app asignado
|
||||||
jobsummary.section.pickup=Recogida
|
jobsummary.section.pickup=Recogida
|
||||||
jobsummary.section.delivery=Entrega
|
jobsummary.section.delivery=Entrega
|
||||||
|
jobsummary.station.phone=Teléfono
|
||||||
jobsummary.section.tasks=Tareas a Confirmar
|
jobsummary.section.tasks=Tareas a Confirmar
|
||||||
jobsummary.section.cargo=Carga a Transportar
|
jobsummary.section.cargo=Carga a Transportar
|
||||||
jobsummary.section.info=Información Adicional
|
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.street.placeholder=Rue (Livraison)
|
||||||
addjob.address.delivery.addition.placeholder=Ajout d'adresse (Livraison)
|
addjob.address.delivery.addition.placeholder=Ajout d'adresse (Livraison)
|
||||||
addjob.address.save=Enregistrer l'Adresse
|
addjob.address.save=Enregistrer l'Adresse
|
||||||
addjob.section.pickup=Enlèvement
|
addjob.section.pickup=Enl\u00e8vement
|
||||||
addjob.section.delivery=Livraison
|
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.addresses=Client & Adresses
|
||||||
addjob.tab.appointments=Rendez-vous & Traitement
|
addjob.tab.appointments=Rendez-vous & Traitement
|
||||||
addjob.tab.cargo=Cargaison
|
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.notification.noappuser=Aucun utilisateur d'app assigné à cet emploi
|
||||||
jobsummary.section.pickup=Enlèvement
|
jobsummary.section.pickup=Enlèvement
|
||||||
jobsummary.section.delivery=Livraison
|
jobsummary.section.delivery=Livraison
|
||||||
|
jobsummary.station.phone=Téléphone
|
||||||
jobsummary.section.tasks=Tâches à Confirmer
|
jobsummary.section.tasks=Tâches à Confirmer
|
||||||
jobsummary.section.cargo=Cargaison à Transporter
|
jobsummary.section.cargo=Cargaison à Transporter
|
||||||
jobsummary.section.info=Informations Supplémentaires
|
jobsummary.section.info=Informations Supplémentaires
|
||||||
|
|||||||
Reference in New Issue
Block a user