Erweiterungen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 16:06:27 +01:00
parent dff716d97f
commit cff5c7ed88
17 changed files with 1002 additions and 445 deletions

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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();
}
}
} }

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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();
}
});
} }
/** /**

View File

@@ -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);

View File

@@ -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;
} }

View File

@@ -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
@@ -275,9 +307,12 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
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

View File

@@ -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();

View File

@@ -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", "");

View File

@@ -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");
} }

View File

@@ -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());

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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