feat: adapt job flow for delivery stations
This commit is contained in:
@@ -1,23 +1,10 @@
|
||||
package de.assecutor.votianlt.messaging;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import de.assecutor.votianlt.dto.JobWithRelatedDataDTO;
|
||||
import de.assecutor.votianlt.model.CargoItem;
|
||||
import de.assecutor.votianlt.model.task.BaseTask;
|
||||
import de.assecutor.votianlt.model.task.ConfirmationTask;
|
||||
import de.assecutor.votianlt.model.task.TodoListTask;
|
||||
import de.assecutor.votianlt.service.TranslationService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
// Force recompile
|
||||
|
||||
/**
|
||||
* Publishing helper to send JSON payloads to clients via WebSocket.
|
||||
@@ -32,13 +19,10 @@ class MessagingPublisherImpl implements MessagingPublisher {
|
||||
|
||||
private final WebSocketService webSocketService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final TranslationService translationService;
|
||||
|
||||
public MessagingPublisherImpl(WebSocketService webSocketService, ObjectMapper objectMapper,
|
||||
TranslationService translationService) {
|
||||
public MessagingPublisherImpl(WebSocketService webSocketService, ObjectMapper objectMapper) {
|
||||
this.webSocketService = webSocketService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.translationService = translationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -53,10 +37,7 @@ class MessagingPublisherImpl implements MessagingPublisher {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verarbeite Payload und füge Übersetzungen hinzu wenn nötig
|
||||
Object processedPayload = processPayloadWithTranslations(payload);
|
||||
|
||||
String json = objectMapper.writeValueAsString(processedPayload);
|
||||
String json = objectMapper.writeValueAsString(payload);
|
||||
byte[] data = json.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
webSocketService.sendToClient(clientId, messageType, data).thenRun(() -> {
|
||||
@@ -70,184 +51,4 @@ class MessagingPublisherImpl implements MessagingPublisher {
|
||||
log.error("[Messaging] Failed to publish to {}/{}: {}", clientId, messageType, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all translatable texts from the payload, fetches all translations in
|
||||
* one batch (at most one LLM call), then applies them to the JSON tree.
|
||||
*/
|
||||
private Object processPayloadWithTranslations(Object payload) {
|
||||
try {
|
||||
if (payload instanceof JobWithRelatedDataDTO dto) {
|
||||
List<String> texts = collectTexts(dto);
|
||||
Map<String, List<TranslationService.Translation>> translations = translationService
|
||||
.translateBatch(texts);
|
||||
return convertToTranslatedJson(dto, translations);
|
||||
}
|
||||
|
||||
if (payload instanceof List<?> list && !list.isEmpty() && list.get(0) instanceof JobWithRelatedDataDTO) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<JobWithRelatedDataDTO> dtoList = (List<JobWithRelatedDataDTO>) list;
|
||||
|
||||
// Collect all texts from all DTOs and translate in one batch
|
||||
List<String> allTexts = dtoList.stream().flatMap(d -> collectTexts(d).stream()).distinct().toList();
|
||||
Map<String, List<TranslationService.Translation>> translations = translationService
|
||||
.translateBatch(allTexts);
|
||||
|
||||
return dtoList.stream().map(d -> convertToTranslatedJson(d, translations)).toList();
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (Exception e) {
|
||||
log.warn("[Messaging] Failed to process translations: {}", e.getMessage());
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all non-blank translatable strings from a DTO.
|
||||
*/
|
||||
private List<String> collectTexts(JobWithRelatedDataDTO dto) {
|
||||
List<String> texts = new ArrayList<>();
|
||||
|
||||
if (dto.getJob() != null && isNonBlank(dto.getJob().getRemark())) {
|
||||
texts.add(dto.getJob().getRemark());
|
||||
}
|
||||
|
||||
if (dto.getTasks() != null) {
|
||||
for (BaseTask task : dto.getTasks()) {
|
||||
if (isNonBlank(task.getDescription())) {
|
||||
texts.add(task.getDescription());
|
||||
}
|
||||
if (isNonBlank(task.getDisplayName())) {
|
||||
texts.add(task.getDisplayName());
|
||||
}
|
||||
if (task instanceof ConfirmationTask ct && isNonBlank(ct.getButtonText())) {
|
||||
texts.add(ct.getButtonText());
|
||||
}
|
||||
if (task instanceof TodoListTask tlt && tlt.getTodoItems() != null) {
|
||||
for (String item : tlt.getTodoItems()) {
|
||||
if (isNonBlank(item)) {
|
||||
texts.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.getCargoItems() != null) {
|
||||
for (CargoItem item : dto.getCargoItems()) {
|
||||
if (isNonBlank(item.getDescription())) {
|
||||
texts.add(item.getDescription());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return texts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a DTO to a JSON tree and replaces translatable string fields with
|
||||
* translation arrays, using the pre-fetched translation map.
|
||||
*/
|
||||
private ObjectNode convertToTranslatedJson(JobWithRelatedDataDTO dto,
|
||||
Map<String, List<TranslationService.Translation>> translations) {
|
||||
|
||||
ObjectNode root = objectMapper.valueToTree(dto);
|
||||
|
||||
// Job remark
|
||||
if (dto.getJob() != null && isNonBlank(dto.getJob().getRemark())) {
|
||||
List<TranslationService.Translation> t = translations.get(dto.getJob().getRemark());
|
||||
if (t != null) {
|
||||
root.withObject("job").set("remark", createTranslationArray(t));
|
||||
}
|
||||
}
|
||||
|
||||
// Tasks
|
||||
if (dto.getTasks() != null && !dto.getTasks().isEmpty()) {
|
||||
ArrayNode tasksNode = root.withArray("tasks");
|
||||
for (int i = 0; i < dto.getTasks().size(); i++) {
|
||||
BaseTask task = dto.getTasks().get(i);
|
||||
ObjectNode taskNode = (ObjectNode) tasksNode.get(i);
|
||||
|
||||
if (isNonBlank(task.getDescription())) {
|
||||
List<TranslationService.Translation> t = translations.get(task.getDescription());
|
||||
if (t != null) {
|
||||
taskNode.set("description", createTranslationArray(t));
|
||||
}
|
||||
}
|
||||
|
||||
if (isNonBlank(task.getDisplayName())) {
|
||||
List<TranslationService.Translation> t = translations.get(task.getDisplayName());
|
||||
if (t != null) {
|
||||
taskNode.set("displayName", createTranslationArray(t));
|
||||
}
|
||||
}
|
||||
|
||||
if (task instanceof ConfirmationTask ct && isNonBlank(ct.getButtonText())) {
|
||||
List<TranslationService.Translation> t = translations.get(ct.getButtonText());
|
||||
if (t != null) {
|
||||
taskNode.set("buttonText", createTranslationArray(t));
|
||||
if (taskNode.has("taskSpecificData")) {
|
||||
ObjectNode tsd = (ObjectNode) taskNode.get("taskSpecificData");
|
||||
if (tsd.has("buttonText")) {
|
||||
tsd.set("buttonText", createTranslationArray(t));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (task instanceof TodoListTask tlt && tlt.getTodoItems() != null
|
||||
&& taskNode.has("taskSpecificData")) {
|
||||
ObjectNode tsd = (ObjectNode) taskNode.get("taskSpecificData");
|
||||
if (tsd.has("todoItems")) {
|
||||
ArrayNode translatedItems = objectMapper.createArrayNode();
|
||||
for (String item : tlt.getTodoItems()) {
|
||||
if (isNonBlank(item)) {
|
||||
List<TranslationService.Translation> t = translations.get(item);
|
||||
translatedItems.add(t != null ? createTranslationArray(t)
|
||||
: objectMapper.createArrayNode().add(item));
|
||||
}
|
||||
}
|
||||
tsd.set("todoItems", translatedItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cargo items
|
||||
if (dto.getCargoItems() != null && !dto.getCargoItems().isEmpty()) {
|
||||
ArrayNode cargoItemsNode = root.withArray("cargoItems");
|
||||
for (int i = 0; i < dto.getCargoItems().size(); i++) {
|
||||
CargoItem item = dto.getCargoItems().get(i);
|
||||
ObjectNode itemNode = (ObjectNode) cargoItemsNode.get(i);
|
||||
|
||||
if (isNonBlank(item.getDescription())) {
|
||||
List<TranslationService.Translation> t = translations.get(item.getDescription());
|
||||
if (t != null) {
|
||||
itemNode.set("description", createTranslationArray(t));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JSON array from translations.
|
||||
*/
|
||||
private ArrayNode createTranslationArray(List<TranslationService.Translation> translations) {
|
||||
ArrayNode array = objectMapper.createArrayNode();
|
||||
for (TranslationService.Translation t : translations) {
|
||||
ObjectNode node = objectMapper.createObjectNode();
|
||||
node.put("language", t.language());
|
||||
node.put("text", t.text());
|
||||
array.add(node);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
private static boolean isNonBlank(String s) {
|
||||
return s != null && !s.isBlank();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import de.assecutor.votianlt.model.task.BaseTask;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -7,6 +8,8 @@ import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Embedded delivery station within a Job. Each job can have up to 25 delivery
|
||||
@@ -56,4 +59,7 @@ public class DeliveryStation {
|
||||
|
||||
@Field("delivery_time")
|
||||
private LocalTime deliveryTime;
|
||||
|
||||
@Field("tasks")
|
||||
private List<BaseTask> tasks = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ public class DeliveryStationDialog extends Dialog {
|
||||
private boolean saveAddress;
|
||||
private List<BaseTask> tasks = new ArrayList<>();
|
||||
private boolean addressValidatedByGoogle;
|
||||
private AddressValidationResult addressValidationResult;
|
||||
|
||||
public boolean isAddressValidatedByGoogle() {
|
||||
return addressValidatedByGoogle;
|
||||
@@ -63,6 +64,14 @@ public class DeliveryStationDialog extends Dialog {
|
||||
this.addressValidatedByGoogle = addressValidatedByGoogle;
|
||||
}
|
||||
|
||||
public AddressValidationResult getAddressValidationResult() {
|
||||
return addressValidationResult;
|
||||
}
|
||||
|
||||
public void setAddressValidationResult(AddressValidationResult addressValidationResult) {
|
||||
this.addressValidationResult = addressValidationResult;
|
||||
}
|
||||
|
||||
public String getCompany() {
|
||||
return company;
|
||||
}
|
||||
@@ -191,6 +200,7 @@ public class DeliveryStationDialog extends Dialog {
|
||||
|
||||
private final DeliveryStationTile.TranslationHelper translationHelper;
|
||||
private final AddressValidationService addressValidationService;
|
||||
private final java.util.Map<String, Customer> companyAddressOptions = new java.util.LinkedHashMap<>();
|
||||
|
||||
public DeliveryStationDialog(String dialogTitle, List<Customer> customers,
|
||||
DeliveryStationTile.TranslationHelper translationHelper, SaveListener saveListener,
|
||||
@@ -359,6 +369,7 @@ public class DeliveryStationDialog extends Dialog {
|
||||
|
||||
if (validationResult.isValid()) {
|
||||
data.setAddressValidatedByGoogle(true);
|
||||
data.setAddressValidationResult(validationResult);
|
||||
if (saveListener != null) {
|
||||
saveListener.onSave(data);
|
||||
}
|
||||
@@ -378,6 +389,7 @@ public class DeliveryStationDialog extends Dialog {
|
||||
translationHelper.getTranslation("addjob.validation.address.correct"));
|
||||
confirmDialog.addConfirmListener(ev -> {
|
||||
data.setAddressValidatedByGoogle(false);
|
||||
data.setAddressValidationResult(validationResult);
|
||||
if (saveListener != null) {
|
||||
saveListener.onSave(data);
|
||||
}
|
||||
@@ -406,8 +418,12 @@ public class DeliveryStationDialog extends Dialog {
|
||||
public void setData(DeliveryData data) {
|
||||
if (data == null)
|
||||
return;
|
||||
if (data.getCompany() != null)
|
||||
String companyOption = findCompanyOptionLabel(data);
|
||||
if (companyOption != null) {
|
||||
company.setValue(companyOption);
|
||||
} else if (data.getCompany() != null) {
|
||||
company.setValue(data.getCompany());
|
||||
}
|
||||
if (data.getSalutation() != null)
|
||||
salutation.setValue(data.getSalutation());
|
||||
if (data.getFirstName() != null)
|
||||
@@ -448,7 +464,7 @@ public class DeliveryStationDialog extends Dialog {
|
||||
|
||||
private DeliveryData collectData() {
|
||||
DeliveryData data = new DeliveryData();
|
||||
data.setCompany(company.getValue());
|
||||
data.setCompany(resolveCompanyValue(company.getValue()));
|
||||
data.setSalutation(salutation.getValue());
|
||||
data.setFirstName(firstName.getValue());
|
||||
data.setLastName(lastName.getValue());
|
||||
@@ -527,49 +543,126 @@ public class DeliveryStationDialog extends Dialog {
|
||||
}
|
||||
|
||||
private void setupCompanyAutocomplete(ComboBox<String> companyField, List<Customer> customers) {
|
||||
List<String> companyNames = customers.stream().map(Customer::getCompanyName)
|
||||
.filter(name -> name != null && !name.trim().isEmpty()).distinct().sorted().toList();
|
||||
companyAddressOptions.clear();
|
||||
for (Customer customer : customers) {
|
||||
String label = buildCompanyAddressLabel(customer);
|
||||
if (label == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
companyField.setItems(companyNames);
|
||||
String uniqueLabel = label;
|
||||
int counter = 2;
|
||||
while (companyAddressOptions.containsKey(uniqueLabel)) {
|
||||
uniqueLabel = label + " (" + counter++ + ")";
|
||||
}
|
||||
companyAddressOptions.put(uniqueLabel, customer);
|
||||
}
|
||||
|
||||
companyField.setItems(new ArrayList<>(companyAddressOptions.keySet()));
|
||||
|
||||
companyField.addValueChangeListener(event -> {
|
||||
String selectedCompany = event.getValue();
|
||||
if (selectedCompany == null || selectedCompany.trim().isEmpty()) {
|
||||
Customer customer = companyAddressOptions.get(event.getValue());
|
||||
if (customer == null) {
|
||||
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());
|
||||
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()));
|
||||
}
|
||||
|
||||
private String buildCompanyAddressLabel(Customer customer) {
|
||||
if (customer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> leftParts = new ArrayList<>();
|
||||
if (customer.getCompanyName() != null && !customer.getCompanyName().isBlank()) {
|
||||
leftParts.add(customer.getCompanyName().trim());
|
||||
}
|
||||
|
||||
String fullName = ((customer.getFirstname() != null ? customer.getFirstname() : "") + " "
|
||||
+ (customer.getLastName() != null ? customer.getLastName() : "")).trim();
|
||||
if (!fullName.isBlank()) {
|
||||
leftParts.add(fullName);
|
||||
}
|
||||
|
||||
List<String> rightParts = new ArrayList<>();
|
||||
String streetLine = ((customer.getStreet() != null ? customer.getStreet() : "") + " "
|
||||
+ (customer.getHouseNumber() != null ? customer.getHouseNumber() : "")).trim();
|
||||
if (!streetLine.isBlank()) {
|
||||
rightParts.add(streetLine);
|
||||
}
|
||||
|
||||
String cityLine = ((customer.getZip() != null ? customer.getZip() : "") + " "
|
||||
+ (customer.getCity() != null ? customer.getCity() : "")).trim();
|
||||
if (!cityLine.isBlank()) {
|
||||
rightParts.add(cityLine);
|
||||
}
|
||||
|
||||
String left = String.join(" | ", leftParts);
|
||||
String right = String.join(", ", rightParts);
|
||||
String label = left;
|
||||
if (!right.isBlank()) {
|
||||
label = label.isBlank() ? right : left + " | " + right;
|
||||
}
|
||||
|
||||
return label.isBlank() ? null : label;
|
||||
}
|
||||
|
||||
private String resolveCompanyValue(String comboValue) {
|
||||
Customer customer = companyAddressOptions.get(comboValue);
|
||||
if (customer != null && customer.getCompanyName() != null && !customer.getCompanyName().isBlank()) {
|
||||
return customer.getCompanyName();
|
||||
}
|
||||
return comboValue;
|
||||
}
|
||||
|
||||
private String findCompanyOptionLabel(DeliveryData data) {
|
||||
for (java.util.Map.Entry<String, Customer> entry : companyAddressOptions.entrySet()) {
|
||||
Customer customer = entry.getValue();
|
||||
if (matchesCustomer(customer, data)) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean matchesCustomer(Customer customer, DeliveryData data) {
|
||||
return equalsNormalized(customer.getCompanyName(), data.getCompany())
|
||||
&& equalsNormalized(customer.getStreet(), data.getStreet())
|
||||
&& equalsNormalized(customer.getHouseNumber(), data.getHouseNumber())
|
||||
&& equalsNormalized(customer.getZip(), data.getZip())
|
||||
&& equalsNormalized(customer.getCity(), data.getCity());
|
||||
}
|
||||
|
||||
private boolean equalsNormalized(String left, String right) {
|
||||
String normalizedLeft = left != null ? left.trim() : "";
|
||||
String normalizedRight = right != null ? right.trim() : "";
|
||||
return normalizedLeft.equalsIgnoreCase(normalizedRight);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Task Management
|
||||
// ============================================
|
||||
|
||||
@@ -66,6 +66,7 @@ public class PickupStationDialog extends Dialog {
|
||||
private AppUser appUser;
|
||||
private List<CargoItem> cargoItems = new ArrayList<>();
|
||||
private boolean addressValidatedByGoogle;
|
||||
private AddressValidationResult addressValidationResult;
|
||||
|
||||
public boolean isAddressValidatedByGoogle() {
|
||||
return addressValidatedByGoogle;
|
||||
@@ -75,6 +76,14 @@ public class PickupStationDialog extends Dialog {
|
||||
this.addressValidatedByGoogle = addressValidatedByGoogle;
|
||||
}
|
||||
|
||||
public AddressValidationResult getAddressValidationResult() {
|
||||
return addressValidationResult;
|
||||
}
|
||||
|
||||
public void setAddressValidationResult(AddressValidationResult addressValidationResult) {
|
||||
this.addressValidationResult = addressValidationResult;
|
||||
}
|
||||
|
||||
public String getCompany() {
|
||||
return company;
|
||||
}
|
||||
@@ -485,6 +494,7 @@ public class PickupStationDialog extends Dialog {
|
||||
|
||||
if (validationResult.isValid()) {
|
||||
data.setAddressValidatedByGoogle(true);
|
||||
data.setAddressValidationResult(validationResult);
|
||||
if (saveListener != null) {
|
||||
saveListener.onSave(data);
|
||||
}
|
||||
@@ -504,6 +514,7 @@ public class PickupStationDialog extends Dialog {
|
||||
translationHelper.getTranslation("addjob.validation.address.correct"));
|
||||
confirmDialog.addConfirmListener(ev -> {
|
||||
data.setAddressValidatedByGoogle(false);
|
||||
data.setAddressValidationResult(validationResult);
|
||||
if (saveListener != null) {
|
||||
saveListener.onSave(data);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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 java.util.List;
|
||||
|
||||
/**
|
||||
* A compact tile representing a station (pickup or delivery) in a grid layout.
|
||||
@@ -40,7 +41,7 @@ public class StationTile extends VerticalLayout {
|
||||
this.type = type;
|
||||
this.stationNumber = stationNumber;
|
||||
|
||||
setPadding(true);
|
||||
setPadding(false);
|
||||
setSpacing(false);
|
||||
getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
|
||||
getStyle().set("border-radius", "var(--lumo-border-radius-m)");
|
||||
@@ -48,6 +49,7 @@ public class StationTile extends VerticalLayout {
|
||||
getStyle().set("cursor", "pointer");
|
||||
getStyle().set("aspect-ratio", "1 / 1");
|
||||
getStyle().set("overflow", "hidden");
|
||||
getStyle().set("padding", "var(--lumo-space-m)");
|
||||
|
||||
// Header with title and optional delete button
|
||||
title = new H3(titleText);
|
||||
@@ -82,7 +84,8 @@ public class StationTile extends VerticalLayout {
|
||||
previewContent = new VerticalLayout();
|
||||
previewContent.setPadding(false);
|
||||
previewContent.setSpacing(false);
|
||||
previewContent.getStyle().set("gap", "var(--lumo-space-xs)");
|
||||
previewContent.getStyle().set("gap", "0.15rem");
|
||||
previewContent.getStyle().set("margin-top", "10px");
|
||||
previewContent.getStyle().set("flex-grow", "1");
|
||||
add(previewContent);
|
||||
|
||||
@@ -99,6 +102,11 @@ public class StationTile extends VerticalLayout {
|
||||
|
||||
public void updatePreview(String company, String firstName, String lastName, String street, String houseNumber,
|
||||
String zip, String city) {
|
||||
updatePreview(company, firstName, lastName, street, houseNumber, zip, city, List.of());
|
||||
}
|
||||
|
||||
public void updatePreview(String company, String firstName, String lastName, String street, String houseNumber,
|
||||
String zip, String city, List<String> additionalLines) {
|
||||
previewContent.removeAll();
|
||||
previewContent.setJustifyContentMode(FlexComponent.JustifyContentMode.START);
|
||||
previewContent.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.START);
|
||||
@@ -128,6 +136,15 @@ public class StationTile extends VerticalLayout {
|
||||
hasData = true;
|
||||
}
|
||||
|
||||
if (additionalLines != null) {
|
||||
for (String line : additionalLines) {
|
||||
if (line != null && !line.trim().isEmpty()) {
|
||||
addPreviewLine(line);
|
||||
hasData = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasData) {
|
||||
updateEmptyPreview();
|
||||
}
|
||||
@@ -144,8 +161,8 @@ public class StationTile extends VerticalLayout {
|
||||
|
||||
private void addPreviewLine(String text) {
|
||||
Span span = new Span(text);
|
||||
span.getStyle().set("font-size", "var(--lumo-font-size-s)").set("word-break", "break-word").set("color",
|
||||
"var(--lumo-secondary-text-color)");
|
||||
span.getStyle().set("font-size", "var(--lumo-font-size-xs)").set("line-height", "1.2").set("word-break",
|
||||
"break-word").set("color", "var(--lumo-secondary-text-color)");
|
||||
previewContent.add(span);
|
||||
}
|
||||
|
||||
@@ -173,6 +190,10 @@ public class StationTile extends VerticalLayout {
|
||||
this.deleteListener = listener;
|
||||
}
|
||||
|
||||
public void setInteractive(boolean interactive) {
|
||||
getStyle().set("cursor", interactive ? "pointer" : "default");
|
||||
}
|
||||
|
||||
public void setAddressValidated(boolean validated) {
|
||||
if (validated) {
|
||||
getStyle().set("background-color", "rgba(76, 175, 80, 0.15)");
|
||||
|
||||
@@ -24,9 +24,7 @@ import com.vaadin.flow.router.Layout;
|
||||
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
||||
import com.vaadin.flow.shared.Registration;
|
||||
import de.assecutor.votianlt.model.User;
|
||||
import de.assecutor.votianlt.model.UserInvoiceData;
|
||||
import de.assecutor.votianlt.pages.service.AppUserService;
|
||||
import de.assecutor.votianlt.pages.service.UserInvoiceDataService;
|
||||
import de.assecutor.votianlt.pages.view.EditProfileView;
|
||||
import de.assecutor.votianlt.model.Language;
|
||||
import de.assecutor.votianlt.security.SecurityService;
|
||||
@@ -46,7 +44,6 @@ import java.util.Objects;
|
||||
public final class MainLayout extends AppLayout {
|
||||
|
||||
private final SecurityService securityService;
|
||||
private final UserInvoiceDataService userInvoiceDataService;
|
||||
private final MessageService messageService;
|
||||
private final MessageBadgeUpdateService messageBadgeUpdateService;
|
||||
private final AppUserService appUserService;
|
||||
@@ -57,11 +54,9 @@ public final class MainLayout extends AppLayout {
|
||||
private MenuTreeItem messagesTreeItem;
|
||||
private Registration badgeUpdateRegistration;
|
||||
|
||||
public MainLayout(SecurityService securityService, UserInvoiceDataService userInvoiceDataService,
|
||||
MessageService messageService, MessageBadgeUpdateService messageBadgeUpdateService,
|
||||
AppUserService appUserService) {
|
||||
public MainLayout(SecurityService securityService, MessageService messageService,
|
||||
MessageBadgeUpdateService messageBadgeUpdateService, AppUserService appUserService) {
|
||||
this.securityService = securityService;
|
||||
this.userInvoiceDataService = userInvoiceDataService;
|
||||
this.messageService = messageService;
|
||||
this.messageBadgeUpdateService = messageBadgeUpdateService;
|
||||
this.appUserService = appUserService;
|
||||
@@ -141,12 +136,6 @@ public final class MainLayout extends AppLayout {
|
||||
treeData.addItem(verwaltungItem,
|
||||
new MenuTreeItem(getTranslation("nav.statistics"), "statistics", VaadinIcon.BAR_CHART));
|
||||
|
||||
// Add invoices only if billing is enabled
|
||||
if (isBillingEnabledForCurrentUser()) {
|
||||
treeData.addItem(verwaltungItem,
|
||||
new MenuTreeItem(getTranslation("nav.invoices"), "invoices", VaadinIcon.FILE_TEXT));
|
||||
}
|
||||
|
||||
// Add children to "Benutzer"
|
||||
treeData.addItem(benutzerItem,
|
||||
new MenuTreeItem(getTranslation("nav.profile"), "edit-profile", VaadinIcon.USER));
|
||||
@@ -338,20 +327,6 @@ public final class MainLayout extends AppLayout {
|
||||
return userMenu;
|
||||
}
|
||||
|
||||
private boolean isBillingEnabledForCurrentUser() {
|
||||
try {
|
||||
User currentUser = securityService.getCurrentDatabaseUser();
|
||||
if (currentUser != null && currentUser.getId() != null) {
|
||||
UserInvoiceData invoiceData = userInvoiceDataService.findByUserId(currentUser.getId()).orElse(null);
|
||||
return invoiceData != null && invoiceData.isBillingEnabled();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Log error or handle appropriately
|
||||
// Return false as safe default if we can't determine billing status
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttach(AttachEvent attachEvent) {
|
||||
super.onAttach(attachEvent);
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.assecutor.votianlt.pages.service;
|
||||
|
||||
import de.assecutor.votianlt.dto.JobWithRelatedDataDTO;
|
||||
import de.assecutor.votianlt.model.CargoItem;
|
||||
import de.assecutor.votianlt.model.DeliveryStation;
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.JobStatus;
|
||||
import de.assecutor.votianlt.model.task.BaseTask;
|
||||
@@ -21,7 +22,11 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@@ -89,18 +94,30 @@ public class AddJobService {
|
||||
.filter(task -> task.getTaskType() != null) // Filter nach TaskType statt Text
|
||||
.toList();
|
||||
|
||||
// Setze JobId und stelle sicher, dass taskOrder korrekt ist
|
||||
for (int i = 0; i < filteredTasks.size(); i++) {
|
||||
BaseTask task = filteredTasks.get(i);
|
||||
Map<Integer, Integer> taskOrderByStation = new HashMap<>();
|
||||
|
||||
// Setze JobId und stelle sicher, dass taskOrder je Lieferstation korrekt ist
|
||||
for (BaseTask task : filteredTasks) {
|
||||
task.setJobId(jobId);
|
||||
// Verwende die bereits gesetzte taskOrder oder setze sie auf den Index
|
||||
if (task.getTaskOrder() == null || task.getTaskOrder() != i) {
|
||||
task.setTaskOrder(i); // Stelle sicher, dass die Reihenfolge stimmt
|
||||
int stationOrder = task.getStationOrder() != null ? task.getStationOrder() : 0;
|
||||
if (task.getTaskOrder() == null) {
|
||||
int nextTaskOrder = taskOrderByStation.getOrDefault(stationOrder, 0);
|
||||
task.setTaskOrder(nextTaskOrder);
|
||||
taskOrderByStation.put(stationOrder, nextTaskOrder + 1);
|
||||
} else {
|
||||
int nextTaskOrder = Math.max(taskOrderByStation.getOrDefault(stationOrder, 0),
|
||||
task.getTaskOrder() + 1);
|
||||
taskOrderByStation.put(stationOrder, nextTaskOrder);
|
||||
}
|
||||
}
|
||||
|
||||
taskRepository.saveAll(filteredTasks);
|
||||
attachTasksToDeliveryStations(savedJob, filteredTasks);
|
||||
savedJob = jobRepository.save(savedJob);
|
||||
log.info("Saved {} tasks for job {} with ordering", filteredTasks.size(), jobId);
|
||||
} else if (savedJob.getDeliveryStations() != null && !savedJob.getDeliveryStations().isEmpty()) {
|
||||
attachTasksToDeliveryStations(savedJob, List.of());
|
||||
savedJob = jobRepository.save(savedJob);
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
@@ -215,4 +232,26 @@ public class AddJobService {
|
||||
log.warn("[JOB] Failed to send job_created notification: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void attachTasksToDeliveryStations(Job job, List<BaseTask> tasks) {
|
||||
if (job.getDeliveryStations() == null || job.getDeliveryStations().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<Integer, List<BaseTask>> tasksByStation = new HashMap<>();
|
||||
for (BaseTask task : tasks) {
|
||||
if (task == null) {
|
||||
continue;
|
||||
}
|
||||
int stationOrder = task.getStationOrder() != null ? task.getStationOrder() : 0;
|
||||
tasksByStation.computeIfAbsent(stationOrder, ignored -> new ArrayList<>()).add(task);
|
||||
}
|
||||
|
||||
for (DeliveryStation station : job.getDeliveryStations()) {
|
||||
int stationOrder = station.getStationOrder();
|
||||
List<BaseTask> stationTasks = new ArrayList<>(tasksByStation.getOrDefault(stationOrder, List.of()));
|
||||
stationTasks.sort(Comparator.comparing(task -> task.getTaskOrder() != null ? task.getTaskOrder() : 0));
|
||||
station.setTasks(stationTasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Service zur Validierung von Adressen über die Google Geocoding API.
|
||||
@@ -118,11 +120,12 @@ public class AddressValidationService {
|
||||
double lat = location.path("lat").asDouble();
|
||||
double lng = location.path("lng").asDouble();
|
||||
|
||||
// Prüfen, ob die Adresse als "ROOFTOP" (genaue Adresse) oder
|
||||
// "RANGE_INTERPOLATED" gefunden wurde
|
||||
// Google liefert für valide Adressen nicht immer nur ROOFTOP/RANGE_INTERPOLATED.
|
||||
// Für unseren Flow reicht ein erfolgreicher Geocoding-Treffer mit Koordinaten.
|
||||
String locationType = geometry.path("location_type").asText();
|
||||
boolean isPrecise = "ROOFTOP".equals(locationType) || "RANGE_INTERPOLATED".equals(locationType);
|
||||
boolean hasCoordinates = location.hasNonNull("lat") && location.hasNonNull("lng");
|
||||
|
||||
boolean hasStreetNumber = false;
|
||||
boolean hasPostalCode = false;
|
||||
|
||||
JsonNode addressComponents = firstResult.path("address_components");
|
||||
@@ -131,6 +134,7 @@ public class AddressValidationService {
|
||||
for (JsonNode type : types) {
|
||||
String typeStr = type.asText();
|
||||
if ("street_number".equals(typeStr)) {
|
||||
hasStreetNumber = true;
|
||||
} else if ("postal_code".equals(typeStr)) {
|
||||
hasPostalCode = true;
|
||||
}
|
||||
@@ -138,19 +142,22 @@ public class AddressValidationService {
|
||||
}
|
||||
|
||||
// Ergebnis setzen
|
||||
result.setValid(isPrecise && hasPostalCode);
|
||||
result.setValid(hasCoordinates);
|
||||
result.setFormattedAddress(formattedAddress);
|
||||
result.setLatitude(lat);
|
||||
result.setLongitude(lng);
|
||||
|
||||
if (result.isValid()) {
|
||||
result.setValidationMessage("Adresse erfolgreich validiert");
|
||||
log.debug(
|
||||
"Adressvalidierung erfolgreich: {} -> {} (locationType={}, streetNumber={}, postalCode={})",
|
||||
addressString, formattedAddress, locationType, hasStreetNumber, hasPostalCode);
|
||||
} else {
|
||||
result.setValidationMessage("Adresse ungenau gefunden (keine Hausnummer oder Postleitzahl)");
|
||||
result.setValidationMessage("Adresse gefunden, aber ohne verwertbare Koordinaten");
|
||||
log.warn("Adressvalidierung unvollständig: {} -> {} (locationType={}, streetNumber={}, postalCode={})",
|
||||
addressString, formattedAddress, locationType, hasStreetNumber, hasPostalCode);
|
||||
}
|
||||
|
||||
log.debug("Adressvalidierung erfolgreich: {} -> {}", addressString, formattedAddress);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Fehler bei der Adressvalidierung", e);
|
||||
result.setValidationMessage("Fehler: " + e.getMessage());
|
||||
@@ -191,6 +198,17 @@ public class AddressValidationService {
|
||||
*/
|
||||
public RouteCalculationResult calculateRoute(AddressValidationResult pickupResult,
|
||||
AddressValidationResult deliveryResult) {
|
||||
return calculateRoute(List.of(pickupResult, deliveryResult));
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die schnellste Route über mehrere validierte Stationen.
|
||||
*
|
||||
* @param stationResults
|
||||
* validierte Stationen in Fahrreihenfolge
|
||||
* @return RouteCalculationResult mit Gesamtentfernung und Gesamtdauer
|
||||
*/
|
||||
public RouteCalculationResult calculateRoute(List<AddressValidationResult> stationResults) {
|
||||
RouteCalculationResult routeResult = new RouteCalculationResult();
|
||||
|
||||
// Prüfen, ob API-Key konfiguriert ist
|
||||
@@ -200,26 +218,41 @@ public class AddressValidationService {
|
||||
return routeResult;
|
||||
}
|
||||
|
||||
// Prüfen, ob beide Adressen gültige Koordinaten haben
|
||||
if (pickupResult == null || !pickupResult.isValid() || deliveryResult == null || !deliveryResult.isValid()) {
|
||||
routeResult.setRouteMessage("Beide Adressen müssen validiert sein");
|
||||
if (stationResults == null || stationResults.size() < 2) {
|
||||
routeResult.setRouteMessage("Mindestens zwei validierte Stationen werden benötigt");
|
||||
return routeResult;
|
||||
}
|
||||
|
||||
// Prüfen, ob alle Adressen gültige Koordinaten haben
|
||||
if (stationResults.stream().anyMatch(result -> result == null || !result.isValid())) {
|
||||
routeResult.setRouteMessage("Alle Stationen müssen validiert sein");
|
||||
return routeResult;
|
||||
}
|
||||
|
||||
try {
|
||||
AddressValidationResult originResult = stationResults.getFirst();
|
||||
AddressValidationResult destinationResult = stationResults.getLast();
|
||||
|
||||
// Koordinaten für Start und Ziel
|
||||
String origin = String.format("%s,%s", pickupResult.getLatitude(), pickupResult.getLongitude());
|
||||
String destination = String.format("%s,%s", deliveryResult.getLatitude(), deliveryResult.getLongitude());
|
||||
String origin = formatLatLng(originResult);
|
||||
String destination = formatLatLng(destinationResult);
|
||||
|
||||
// URL für die Directions API erstellen
|
||||
String requestUrl = String.format("%s?origin=%s&destination=%s&mode=driving&key=%s&language=de®ion=de",
|
||||
DIRECTIONS_API_URL, origin, destination, googleMapsApiKey);
|
||||
StringBuilder requestUrl = new StringBuilder(String.format(
|
||||
"%s?origin=%s&destination=%s&mode=driving&key=%s&language=de®ion=de", DIRECTIONS_API_URL,
|
||||
origin, destination, googleMapsApiKey));
|
||||
|
||||
log.debug("Berechne Route von {} nach {}", pickupResult.getFormattedAddress(),
|
||||
deliveryResult.getFormattedAddress());
|
||||
if (stationResults.size() > 2) {
|
||||
List<String> waypoints = stationResults.subList(1, stationResults.size() - 1).stream()
|
||||
.map(this::formatLatLng).toList();
|
||||
requestUrl.append("&waypoints=").append(String.join("|", waypoints));
|
||||
}
|
||||
|
||||
log.debug("Berechne Route über {} Stationen von {} nach {}", stationResults.size(),
|
||||
originResult.getFormattedAddress(), destinationResult.getFormattedAddress());
|
||||
|
||||
// HTTP Request senden
|
||||
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(requestUrl)).GET()
|
||||
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(requestUrl.toString())).GET()
|
||||
.timeout(Duration.ofSeconds(10)).build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
@@ -254,27 +287,26 @@ public class AddressValidationService {
|
||||
return routeResult;
|
||||
}
|
||||
|
||||
// Ersten Leg (Hauptstrecke) verwenden
|
||||
JsonNode firstLeg = legs.get(0);
|
||||
int totalDistanceMeters = 0;
|
||||
int totalDurationSeconds = 0;
|
||||
|
||||
// Distanz extrahieren
|
||||
JsonNode distanceNode = firstLeg.path("distance");
|
||||
int distanceMeters = distanceNode.path("value").asInt();
|
||||
String distanceText = distanceNode.path("text").asText();
|
||||
for (JsonNode leg : legs) {
|
||||
totalDistanceMeters += leg.path("distance").path("value").asInt();
|
||||
totalDurationSeconds += leg.path("duration").path("value").asInt();
|
||||
}
|
||||
|
||||
// Dauer extrahieren
|
||||
JsonNode durationNode = firstLeg.path("duration");
|
||||
int durationSeconds = durationNode.path("value").asInt();
|
||||
String durationText = durationNode.path("text").asText();
|
||||
double totalDistanceKm = totalDistanceMeters / 1000.0;
|
||||
String distanceText = String.format(Locale.GERMANY, "%.1f km", totalDistanceKm);
|
||||
String durationText = formatDuration(totalDurationSeconds);
|
||||
|
||||
// Ergebnis setzen
|
||||
routeResult.setValid(true);
|
||||
routeResult.setDistanceKm(distanceMeters / 1000.0);
|
||||
routeResult.setDurationSeconds(durationSeconds);
|
||||
routeResult.setDistanceKm(totalDistanceKm);
|
||||
routeResult.setDurationSeconds(totalDurationSeconds);
|
||||
routeResult.setFormattedDistance(distanceText);
|
||||
routeResult.setFormattedDuration(durationText);
|
||||
routeResult.setRouteMessage(
|
||||
String.format("Route: %s, Dauer: %s", distanceText, routeResult.getFormattedDurationLong()));
|
||||
String.format("Route: %s, Dauer: %s", distanceText, durationText));
|
||||
|
||||
log.debug("Routenberechnung erfolgreich: {} km, {} Min.", routeResult.getDistanceKm(),
|
||||
routeResult.getDurationMinutes());
|
||||
@@ -286,4 +318,18 @@ public class AddressValidationService {
|
||||
|
||||
return routeResult;
|
||||
}
|
||||
|
||||
private String formatLatLng(AddressValidationResult result) {
|
||||
return String.format(Locale.US, "%s,%s", result.getLatitude(), result.getLongitude());
|
||||
}
|
||||
|
||||
private String formatDuration(int durationSeconds) {
|
||||
int hours = durationSeconds / 3600;
|
||||
int minutes = (durationSeconds % 3600) / 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return String.format("%d Std. %d Min.", hours, minutes);
|
||||
}
|
||||
return String.format("%d Min.", minutes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.confirmdialog.ConfirmDialog;
|
||||
import com.vaadin.flow.component.dialog.Dialog;
|
||||
import com.vaadin.flow.component.UI;
|
||||
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
@@ -20,6 +21,7 @@ import com.vaadin.flow.component.notification.Notification;
|
||||
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.progressbar.ProgressBar;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.data.binder.Binder;
|
||||
import com.vaadin.flow.component.Component;
|
||||
@@ -30,8 +32,12 @@ import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.task.BarcodeTask;
|
||||
import de.assecutor.votianlt.model.task.BaseTask;
|
||||
import de.assecutor.votianlt.model.task.CommentTask;
|
||||
import de.assecutor.votianlt.model.task.ConfirmationTask;
|
||||
import de.assecutor.votianlt.model.task.PhotoTask;
|
||||
import de.assecutor.votianlt.model.task.SignatureTask;
|
||||
import de.assecutor.votianlt.model.task.TodoListTask;
|
||||
import de.assecutor.votianlt.pages.service.AddJobService;
|
||||
import de.assecutor.votianlt.pages.service.CustomerService;
|
||||
@@ -61,14 +67,19 @@ import de.assecutor.votianlt.pages.base.ui.component.StationTile;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.PickupStationDialog;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.DeliveryStationDialog;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Route(value = "add_job", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||
@RolesAllowed("USER")
|
||||
@Slf4j
|
||||
public class AddJobView extends Main implements HasDynamicTitle {
|
||||
private static final DateTimeFormatter PICKUP_PREVIEW_DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy");
|
||||
private static final DateTimeFormatter PICKUP_PREVIEW_TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
|
||||
|
||||
@Override
|
||||
public String getPageTitle() {
|
||||
@@ -160,6 +171,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
// Adressvalidierung
|
||||
private final Map<String, AddressValidationResult> addressValidationResults = new HashMap<>();
|
||||
private RouteCalculationResult routeCalculationResult;
|
||||
private boolean pickupAddressValidatedByGoogle;
|
||||
private final List<Boolean> deliveryStationsValidatedByGoogle = new ArrayList<>();
|
||||
|
||||
public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService,
|
||||
CustomerService customerService, AppUserService appUserService, TaskTemplateService taskTemplateService,
|
||||
@@ -354,11 +367,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
applyStationsButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
applyStationsButton.setWidthFull();
|
||||
applyStationsButton.setEnabled(false);
|
||||
applyStationsButton.addClickListener(e -> {
|
||||
applyStationsButton.setVisible(false);
|
||||
priceAndDetailsSection.setVisible(true);
|
||||
submitButtonLayout.setVisible(true);
|
||||
});
|
||||
applyStationsButton.addClickListener(e -> handleApplyStations());
|
||||
tabContent.add(applyStationsButton);
|
||||
|
||||
// Route Info Box
|
||||
@@ -631,6 +640,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
// Add empty state for this station
|
||||
deliveryStationsState.add(new DeliveryStation());
|
||||
deliveryStationsSaveAddress.add(true);
|
||||
deliveryStationsValidatedByGoogle.add(false);
|
||||
|
||||
int stationIndex = deliveryStationTilesList.size();
|
||||
tile.setClickListener(t -> openDeliveryDialog(t, stationIndex));
|
||||
@@ -647,6 +657,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
stationsGridContainer.add(addStationButton);
|
||||
}
|
||||
|
||||
resetRouteInformation();
|
||||
resetStationsAppliedState();
|
||||
triggerValidation();
|
||||
updateTabLabels();
|
||||
@@ -670,6 +681,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
deliveryStationTilesList.remove(removeIdx);
|
||||
deliveryStationsState.remove(removeIdx);
|
||||
deliveryStationsSaveAddress.remove(removeIdx);
|
||||
deliveryStationsValidatedByGoogle.remove(removeIdx);
|
||||
deliveryStationTasksState.remove(removeIdx);
|
||||
// Re-index tasks state for remaining stations
|
||||
Map<Integer, List<BaseTask>> reindexed = new HashMap<>();
|
||||
@@ -737,12 +749,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
savePickupAddress.setValue(data.isSaveAddress());
|
||||
|
||||
// Sync appointment fields for binder/submit
|
||||
if (data.getAppointmentDate() != null) {
|
||||
pickupDate.setValue(data.getAppointmentDate());
|
||||
}
|
||||
if (data.getAppointmentTime() != null) {
|
||||
pickupTime.setValue(data.getAppointmentTime());
|
||||
}
|
||||
pickupDate.setValue(data.getAppointmentDate());
|
||||
pickupTime.setValue(data.getAppointmentTime());
|
||||
digitalProcessing.setValue(data.isDigitalProcessing());
|
||||
if (data.getAppUser() != null) {
|
||||
appUser.setValue(data.getAppUser());
|
||||
@@ -758,8 +766,12 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
|
||||
// Update tile preview
|
||||
pickupTile.updatePreview(data.getCompany(), data.getFirstName(), data.getLastName(),
|
||||
data.getStreet(), data.getHouseNumber(), data.getZip(), data.getCity());
|
||||
pickupTile.setAddressValidated(data.isAddressValidatedByGoogle());
|
||||
data.getStreet(), data.getHouseNumber(), data.getZip(), data.getCity(),
|
||||
buildPickupPreviewDetails(data.getAppointmentDate(), data.getAppointmentTime(),
|
||||
data.isDigitalProcessing(), data.getAppUser(), data.getCargoItems()));
|
||||
pickupAddressValidatedByGoogle = data.isAddressValidatedByGoogle();
|
||||
storePickupAddressValidationResult(data.getAddressValidationResult());
|
||||
pickupTile.setAddressValidated(pickupAddressValidatedByGoogle);
|
||||
|
||||
resetRouteInformation();
|
||||
resetStationsAppliedState();
|
||||
@@ -787,11 +799,191 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
currentData.setDigitalProcessing(digitalProcessing.getValue());
|
||||
currentData.setAppUser(appUser.getValue());
|
||||
currentData.setCargoItems(new ArrayList<>(cargoItemsState));
|
||||
currentData.setAddressValidatedByGoogle(pickupAddressValidatedByGoogle);
|
||||
dialog.setData(currentData);
|
||||
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
private List<String> buildPickupPreviewDetails(LocalDate appointmentDate, LocalTime appointmentTime,
|
||||
boolean digitalProcessingEnabled, AppUser assignedAppUser, List<CargoItem> cargoItems) {
|
||||
List<String> previewDetails = new ArrayList<>();
|
||||
|
||||
previewDetails.add(getTranslation("addjob.tab.cargo") + ": " + summarizeCargoItems(cargoItems));
|
||||
|
||||
String appointmentPreview = formatPickupAppointment(appointmentDate, appointmentTime);
|
||||
if (appointmentPreview != null) {
|
||||
previewDetails.add(getTranslation("addjob.appointment.pickup") + ": " + appointmentPreview);
|
||||
}
|
||||
|
||||
previewDetails.add(buildDigitalProcessingPreview(digitalProcessingEnabled, assignedAppUser));
|
||||
return previewDetails;
|
||||
}
|
||||
|
||||
private String summarizeCargoItems(List<CargoItem> cargoItems) {
|
||||
if (cargoItems == null || cargoItems.isEmpty()) {
|
||||
return getTranslation("jobsummary.cargo.none");
|
||||
}
|
||||
|
||||
List<String> summaries = new ArrayList<>();
|
||||
for (CargoItem cargoItem : cargoItems) {
|
||||
if (cargoItem == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String description = cargoItem.getDescription() != null ? cargoItem.getDescription().trim() : "";
|
||||
Integer quantity = cargoItem.getQuantity();
|
||||
if (description.isEmpty() && quantity == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
StringBuilder summary = new StringBuilder();
|
||||
if (quantity != null) {
|
||||
summary.append(quantity).append("x ");
|
||||
}
|
||||
summary.append(description.isEmpty() ? getTranslation("addjob.tab.cargo") : description);
|
||||
summaries.add(summary.toString().trim());
|
||||
}
|
||||
|
||||
if (summaries.isEmpty()) {
|
||||
return getTranslation("jobsummary.cargo.none");
|
||||
}
|
||||
if (summaries.size() <= 2) {
|
||||
return String.join(", ", summaries);
|
||||
}
|
||||
return String.join(", ", summaries.subList(0, 2)) + " +" + (summaries.size() - 2);
|
||||
}
|
||||
|
||||
private String formatPickupAppointment(LocalDate appointmentDate, LocalTime appointmentTime) {
|
||||
if (appointmentDate == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String formattedDate = appointmentDate.format(PICKUP_PREVIEW_DATE_FORMATTER);
|
||||
if (appointmentTime == null) {
|
||||
return formattedDate;
|
||||
}
|
||||
return formattedDate + " " + appointmentTime.format(PICKUP_PREVIEW_TIME_FORMATTER);
|
||||
}
|
||||
|
||||
private String buildDigitalProcessingPreview(boolean digitalProcessingEnabled, AppUser assignedAppUser) {
|
||||
StringBuilder preview = new StringBuilder();
|
||||
preview.append(getTranslation("profile.settings.digitalprocess")).append(": ")
|
||||
.append(getTranslation(digitalProcessingEnabled ? "common.yes" : "common.no"));
|
||||
|
||||
String appUserLabel = formatAssignedAppUser(assignedAppUser);
|
||||
if (digitalProcessingEnabled && appUserLabel != null) {
|
||||
preview.append(" (").append(appUserLabel).append(")");
|
||||
}
|
||||
return preview.toString();
|
||||
}
|
||||
|
||||
private String formatAssignedAppUser(AppUser assignedAppUser) {
|
||||
if (assignedAppUser == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String fullName = ((assignedAppUser.getVorname() != null ? assignedAppUser.getVorname() : "") + " "
|
||||
+ (assignedAppUser.getNachname() != null ? assignedAppUser.getNachname() : "")).trim();
|
||||
if (!fullName.isEmpty()) {
|
||||
return fullName;
|
||||
}
|
||||
if (assignedAppUser.getBezeichnung() != null && !assignedAppUser.getBezeichnung().isBlank()) {
|
||||
return assignedAppUser.getBezeichnung().trim();
|
||||
}
|
||||
if (assignedAppUser.getEmail() != null && !assignedAppUser.getEmail().isBlank()) {
|
||||
return assignedAppUser.getEmail().trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> buildDeliveryPreviewDetails(List<BaseTask> tasks) {
|
||||
if (tasks == null || tasks.isEmpty()) {
|
||||
return List.of(getTranslation("addjob.tab.tasks") + ": " + getTranslation("jobsummary.tasks.none"));
|
||||
}
|
||||
|
||||
List<String> summaries = new ArrayList<>();
|
||||
for (BaseTask task : tasks) {
|
||||
if (task != null) {
|
||||
summaries.add(summarizeDeliveryTask(task));
|
||||
}
|
||||
}
|
||||
|
||||
if (summaries.isEmpty()) {
|
||||
return List.of(getTranslation("addjob.tab.tasks") + ": " + getTranslation("jobsummary.tasks.none"));
|
||||
}
|
||||
|
||||
return summaries;
|
||||
}
|
||||
|
||||
private String summarizeDeliveryTask(BaseTask task) {
|
||||
if (task instanceof ConfirmationTask confirmationTask) {
|
||||
String buttonText = trimToNull(confirmationTask.getButtonText());
|
||||
if (buttonText != null) {
|
||||
return confirmationTask.getDisplayName() + " \"" + buttonText + "\"";
|
||||
}
|
||||
|
||||
String description = trimToNull(confirmationTask.getDescription());
|
||||
return description != null ? confirmationTask.getDisplayName() + " \"" + description + "\""
|
||||
: confirmationTask.getDisplayName();
|
||||
}
|
||||
|
||||
if (task instanceof TodoListTask todoListTask) {
|
||||
long itemCount = todoListTask.getTodoItems() == null ? 0
|
||||
: todoListTask.getTodoItems().stream().filter(Objects::nonNull).map(String::trim)
|
||||
.filter(item -> !item.isEmpty()).count();
|
||||
return itemCount > 0 ? task.getDisplayName() + " (" + itemCount + ")" : task.getDisplayName();
|
||||
}
|
||||
|
||||
if (task instanceof PhotoTask photoTask) {
|
||||
String range = formatMinMaxRange(photoTask.getMinPhotoCount(), photoTask.getMaxPhotoCount());
|
||||
return range.isBlank() ? task.getDisplayName() : task.getDisplayName() + " " + range;
|
||||
}
|
||||
|
||||
if (task instanceof BarcodeTask barcodeTask) {
|
||||
String range = formatMinMaxRange(barcodeTask.getMinBarcodeCount(), barcodeTask.getMaxBarcodeCount());
|
||||
return range.isBlank() ? task.getDisplayName() : task.getDisplayName() + " " + range;
|
||||
}
|
||||
|
||||
if (task instanceof CommentTask commentTask) {
|
||||
String commentText = trimToNull(commentTask.getCommentText());
|
||||
if (commentText != null) {
|
||||
return task.getDisplayName() + " \"" + commentText + "\"";
|
||||
}
|
||||
return commentTask.isRequired() ? task.getDisplayName() + " (" + getTranslation("common.required") + ")"
|
||||
: task.getDisplayName();
|
||||
}
|
||||
|
||||
if (task instanceof SignatureTask) {
|
||||
return task.getDisplayName();
|
||||
}
|
||||
|
||||
String description = trimToNull(task.getDescription());
|
||||
return description != null ? task.getDisplayName() + " \"" + description + "\"" : task.getDisplayName();
|
||||
}
|
||||
|
||||
private String formatMinMaxRange(Integer minValue, Integer maxValue) {
|
||||
if (minValue != null && maxValue != null) {
|
||||
return minValue.equals(maxValue) ? String.valueOf(minValue) : minValue + "-" + maxValue;
|
||||
}
|
||||
if (minValue != null) {
|
||||
return String.valueOf(minValue);
|
||||
}
|
||||
if (maxValue != null) {
|
||||
return String.valueOf(maxValue);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
private void openDeliveryDialog(StationTile tile, int stationIndex) {
|
||||
// Ensure index is valid (could have changed due to deletions)
|
||||
int actualIndex = deliveryStationTilesList.indexOf(tile);
|
||||
@@ -822,6 +1014,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
station.setAddressAddition(data.getAddressAddition());
|
||||
station.setZip(data.getZip());
|
||||
station.setCity(data.getCity());
|
||||
station.setTasks(data.getTasks() != null ? new ArrayList<>(data.getTasks()) : new ArrayList<>());
|
||||
deliveryStationsSaveAddress.set(idx, data.isSaveAddress());
|
||||
|
||||
// Store tasks for this delivery station
|
||||
@@ -829,7 +1022,10 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
|
||||
// Update tile preview
|
||||
tile.updatePreview(data.getCompany(), data.getFirstName(), data.getLastName(), data.getStreet(),
|
||||
data.getHouseNumber(), data.getZip(), data.getCity());
|
||||
data.getHouseNumber(), data.getZip(), data.getCity(),
|
||||
buildDeliveryPreviewDetails(data.getTasks()));
|
||||
deliveryStationsValidatedByGoogle.set(idx, data.isAddressValidatedByGoogle());
|
||||
storeDeliveryAddressValidationResult(idx, data.getAddressValidationResult());
|
||||
tile.setAddressValidated(data.isAddressValidatedByGoogle());
|
||||
|
||||
resetRouteInformation();
|
||||
@@ -863,6 +1059,9 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
currentData.setZip(station.getZip());
|
||||
currentData.setCity(station.getCity());
|
||||
currentData.setSaveAddress(deliveryStationsSaveAddress.get(actualIndex));
|
||||
if (actualIndex < deliveryStationsValidatedByGoogle.size()) {
|
||||
currentData.setAddressValidatedByGoogle(deliveryStationsValidatedByGoogle.get(actualIndex));
|
||||
}
|
||||
// Load existing tasks for this station
|
||||
List<BaseTask> stationTasks = deliveryStationTasksState.get(actualIndex);
|
||||
if (stationTasks != null) {
|
||||
@@ -1369,6 +1568,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
for (int i = 0; i < deliveryStationsState.size(); i++) {
|
||||
DeliveryStation station = deliveryStationsState.get(i);
|
||||
station.setStationOrder(i);
|
||||
station.setTasks(new ArrayList<>(deliveryStationTasksState.getOrDefault(i, List.of())));
|
||||
stations.add(station);
|
||||
}
|
||||
job.setDeliveryStations(stations);
|
||||
@@ -1697,6 +1897,212 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void handleApplyStations() {
|
||||
revealPriceAndDetailsSection();
|
||||
|
||||
if (!areAllStationsValidatedByGoogle()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Dialog loadingDialog = createRouteLoadingDialog();
|
||||
UI ui = UI.getCurrent();
|
||||
loadingDialog.open();
|
||||
|
||||
CompletableFuture.supplyAsync(this::calculateRouteAcrossAllStations).whenComplete((routeResult, throwable) -> {
|
||||
if (ui == null) {
|
||||
return;
|
||||
}
|
||||
ui.access(() -> {
|
||||
loadingDialog.close();
|
||||
|
||||
if (throwable != null) {
|
||||
log.error("Fehler bei der Berechnung der Gesamtstrecke", throwable);
|
||||
Notification.show("Die Strecke konnte nicht berechnet werden.", 4000, Notification.Position.MIDDLE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (routeResult != null && routeResult.isValid()) {
|
||||
applyCalculatedRoute(routeResult);
|
||||
showRouteSummaryDialog(routeResult);
|
||||
} else {
|
||||
String message = routeResult != null && routeResult.getRouteMessage() != null
|
||||
? routeResult.getRouteMessage()
|
||||
: "Die Strecke konnte nicht berechnet werden.";
|
||||
Notification.show(message, 4000, Notification.Position.MIDDLE);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void revealPriceAndDetailsSection() {
|
||||
applyStationsButton.setVisible(false);
|
||||
priceAndDetailsSection.setVisible(true);
|
||||
submitButtonLayout.setVisible(true);
|
||||
}
|
||||
|
||||
private boolean areAllStationsValidatedByGoogle() {
|
||||
if (!pickupAddressValidatedByGoogle || deliveryStationsState.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (deliveryStationsValidatedByGoogle.size() != deliveryStationsState.size()) {
|
||||
return false;
|
||||
}
|
||||
return deliveryStationsValidatedByGoogle.stream().allMatch(Boolean.TRUE::equals);
|
||||
}
|
||||
|
||||
private RouteCalculationResult calculateRouteAcrossAllStations() {
|
||||
if (!pickupAddressValidatedByGoogle) {
|
||||
return createInvalidRouteResult("Die Abholstation ist nicht validiert.");
|
||||
}
|
||||
|
||||
List<AddressValidationResult> stationResults = new ArrayList<>();
|
||||
AddressValidationResult pickupValidation = getOrValidatePickupAddressResult();
|
||||
if (pickupValidation == null || !pickupValidation.isValid()) {
|
||||
return createInvalidRouteResult("Die Abholstation konnte nicht validiert werden.");
|
||||
}
|
||||
stationResults.add(pickupValidation);
|
||||
|
||||
for (int i = 0; i < deliveryStationsState.size(); i++) {
|
||||
DeliveryStation station = deliveryStationsState.get(i);
|
||||
if (hasDeliveryStationValidationErrors(station)) {
|
||||
return createInvalidRouteResult("Nicht alle Lieferstationen sind vollständig ausgefüllt.");
|
||||
}
|
||||
if (i >= deliveryStationsValidatedByGoogle.size() || !Boolean.TRUE.equals(deliveryStationsValidatedByGoogle.get(i))) {
|
||||
return createInvalidRouteResult("Nicht alle Lieferstationen sind validiert.");
|
||||
}
|
||||
|
||||
AddressValidationResult deliveryValidation = getOrValidateDeliveryAddressResult(i);
|
||||
if (deliveryValidation == null || !deliveryValidation.isValid()) {
|
||||
return createInvalidRouteResult(
|
||||
String.format("Die Strecke konnte für Lieferstation %d nicht berechnet werden.", i + 1));
|
||||
}
|
||||
stationResults.add(deliveryValidation);
|
||||
}
|
||||
|
||||
return addressValidationService.calculateRoute(stationResults);
|
||||
}
|
||||
|
||||
private AddressValidationResult getOrValidatePickupAddressResult() {
|
||||
AddressValidationResult existingResult = addressValidationResults.get("pickup");
|
||||
if (existingResult != null && existingResult.matches(pickupStreet.getValue(), pickupHouseNumber.getValue(),
|
||||
pickupZip.getValue(), pickupCity.getValue())) {
|
||||
return existingResult;
|
||||
}
|
||||
|
||||
AddressValidationResult validationResult = addressValidationService.validateAddress("pickup",
|
||||
pickupStreet.getValue(), pickupHouseNumber.getValue(), pickupZip.getValue(), pickupCity.getValue());
|
||||
storePickupAddressValidationResult(validationResult);
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
private AddressValidationResult getOrValidateDeliveryAddressResult(int index) {
|
||||
String resultKey = "delivery_" + index;
|
||||
DeliveryStation station = deliveryStationsState.get(index);
|
||||
|
||||
AddressValidationResult existingResult = addressValidationResults.get(resultKey);
|
||||
if (existingResult != null && existingResult.matches(station.getStreet(), station.getHouseNumber(),
|
||||
station.getZip(), station.getCity())) {
|
||||
return existingResult;
|
||||
}
|
||||
|
||||
AddressValidationResult validationResult = addressValidationService.validateAddress(resultKey,
|
||||
station.getStreet(), station.getHouseNumber(), station.getZip(), station.getCity());
|
||||
storeDeliveryAddressValidationResult(index, validationResult);
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
private void storePickupAddressValidationResult(AddressValidationResult validationResult) {
|
||||
if (validationResult == null) {
|
||||
addressValidationResults.remove("pickup");
|
||||
return;
|
||||
}
|
||||
addressValidationResults.put("pickup", validationResult);
|
||||
}
|
||||
|
||||
private void storeDeliveryAddressValidationResult(int index, AddressValidationResult validationResult) {
|
||||
String resultKey = "delivery_" + index;
|
||||
if (validationResult == null) {
|
||||
addressValidationResults.remove(resultKey);
|
||||
return;
|
||||
}
|
||||
addressValidationResults.put(resultKey, validationResult);
|
||||
}
|
||||
|
||||
private RouteCalculationResult createInvalidRouteResult(String message) {
|
||||
RouteCalculationResult routeResult = new RouteCalculationResult();
|
||||
routeResult.setRouteMessage(message);
|
||||
return routeResult;
|
||||
}
|
||||
|
||||
private Dialog createRouteLoadingDialog() {
|
||||
Dialog dialog = new Dialog();
|
||||
dialog.setCloseOnOutsideClick(false);
|
||||
dialog.setCloseOnEsc(false);
|
||||
dialog.setHeaderTitle(getTranslation("addjob.route.title"));
|
||||
|
||||
VerticalLayout content = new VerticalLayout();
|
||||
content.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||
content.setPadding(true);
|
||||
content.setSpacing(true);
|
||||
|
||||
Span loadingText = new Span("Strecke zwischen allen Stationen wird berechnet...");
|
||||
ProgressBar progressBar = new ProgressBar();
|
||||
progressBar.setIndeterminate(true);
|
||||
progressBar.setWidthFull();
|
||||
|
||||
content.add(loadingText, progressBar);
|
||||
dialog.add(content);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private void applyCalculatedRoute(RouteCalculationResult routeResult) {
|
||||
routeCalculationResult = routeResult;
|
||||
|
||||
routeDistanceLabel.setText(routeResult.getFormattedDistance());
|
||||
routeDurationLabel.setText(routeResult.getFormattedDurationLong());
|
||||
routeInfoBox.setVisible(true);
|
||||
manualRouteInputBox.setVisible(false);
|
||||
|
||||
updatePriceSummary();
|
||||
if (servicesGrid != null) {
|
||||
servicesGrid.getDataProvider().refreshAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void showRouteSummaryDialog(RouteCalculationResult routeResult) {
|
||||
Dialog dialog = new Dialog();
|
||||
dialog.setHeaderTitle(getTranslation("addjob.route.title"));
|
||||
dialog.setWidth("420px");
|
||||
|
||||
VerticalLayout content = new VerticalLayout();
|
||||
content.setPadding(false);
|
||||
content.setSpacing(true);
|
||||
content.add(createRouteSummaryRow(getTranslation("addjob.route.distance"), routeResult.getFormattedDistance()));
|
||||
content.add(createRouteSummaryRow(getTranslation("addjob.route.duration"),
|
||||
routeResult.getFormattedDurationLong()));
|
||||
|
||||
Button closeButton = new Button(getTranslation("dialog.confirm"), event -> dialog.close());
|
||||
closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
|
||||
dialog.add(content);
|
||||
dialog.getFooter().add(closeButton);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
private HorizontalLayout createRouteSummaryRow(String label, String value) {
|
||||
HorizontalLayout row = new HorizontalLayout();
|
||||
row.setWidthFull();
|
||||
row.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
|
||||
row.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||
|
||||
Span labelSpan = new Span(label + ":");
|
||||
Span valueSpan = new Span(value);
|
||||
valueSpan.getStyle().set("font-weight", "bold");
|
||||
|
||||
row.add(labelSpan, valueSpan);
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registriert ValueChangeListener für alle Adressfelder, um bei Änderungen die
|
||||
* Streckeninformationen zurückzusetzen.
|
||||
@@ -1781,6 +2187,12 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
||||
if (routeInfoBox != null) {
|
||||
routeInfoBox.setVisible(false);
|
||||
}
|
||||
if (routeDistanceLabel != null) {
|
||||
routeDistanceLabel.setText("-");
|
||||
}
|
||||
if (routeDurationLabel != null) {
|
||||
routeDurationLabel.setText("-");
|
||||
}
|
||||
if (manualRouteInputBox != null) {
|
||||
manualRouteInputBox.setVisible(true);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import de.assecutor.votianlt.model.task.ConfirmationTask;
|
||||
import de.assecutor.votianlt.model.task.BarcodeTask;
|
||||
import de.assecutor.votianlt.model.task.CommentTask;
|
||||
import de.assecutor.votianlt.model.AppUser;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.StationTile;
|
||||
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import de.assecutor.votianlt.repository.CargoItemRepository;
|
||||
@@ -185,94 +186,7 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
private void render(Job job, List<CargoItem> cargoItems, List<BaseTask> tasks) {
|
||||
content.removeAll();
|
||||
|
||||
// Kopfzeile: Abholung/Lieferung
|
||||
HorizontalLayout topRow = new HorizontalLayout();
|
||||
topRow.setWidthFull();
|
||||
topRow.setSpacing(true);
|
||||
|
||||
VerticalLayout pickupBox = borderedBox();
|
||||
pickupBox.add(new H3(getTranslation("jobsummary.section.pickup") + " "
|
||||
+ formatDateWithTime(job.getPickupDate(), job.getPickupTime())));
|
||||
pickupBox.add(new Span(valueOrEmpty(job.getPickupCompany())));
|
||||
pickupBox.add(new Span(valueOrEmpty(job.getPickupSalutation()) + (job.getPickupSalutation() != null ? " " : "")
|
||||
+ valueOrEmpty(job.getPickupFirstName()) + (job.getPickupFirstName() != null ? " " : "")
|
||||
+ valueOrEmpty(job.getPickupLastName())));
|
||||
pickupBox.add(new Span(concatAddress(job.getPickupStreet(), job.getPickupHouseNumber())));
|
||||
pickupBox.add(new Span(concatZipCity(job.getPickupZip(), job.getPickupCity())));
|
||||
|
||||
pickupBox.setWidth("50%");
|
||||
|
||||
List<DeliveryStation> stations = job.getDeliveryStations();
|
||||
if (stations != null && !stations.isEmpty()) {
|
||||
// Multiple delivery stations layout
|
||||
VerticalLayout deliveryStationsContainer = new VerticalLayout();
|
||||
deliveryStationsContainer.setPadding(false);
|
||||
deliveryStationsContainer.setSpacing(true);
|
||||
deliveryStationsContainer.setWidth("50%");
|
||||
|
||||
for (int i = 0; i < stations.size(); i++) {
|
||||
DeliveryStation station = stations.get(i);
|
||||
VerticalLayout stationBox = borderedBox();
|
||||
String stationLabel = getTranslation("jobsummary.section.delivery") + " "
|
||||
+ (stations.size() > 1 ? (i + 1) + " " : "")
|
||||
+ formatDateWithTime(station.getDeliveryDate(), station.getDeliveryTime());
|
||||
stationBox.add(new H3(stationLabel));
|
||||
stationBox.add(new Span(valueOrEmpty(station.getCompany())));
|
||||
stationBox.add(new Span(valueOrEmpty(station.getSalutation())
|
||||
+ (station.getSalutation() != null ? " " : "") + valueOrEmpty(station.getFirstName())
|
||||
+ (station.getFirstName() != null ? " " : "") + valueOrEmpty(station.getLastName())));
|
||||
stationBox.add(new Span(concatAddress(station.getStreet(), station.getHouseNumber())));
|
||||
stationBox.add(new Span(concatZipCity(station.getZip(), station.getCity())));
|
||||
if (station.getPhone() != null && !station.getPhone().isBlank()) {
|
||||
stationBox.add(new Span(getTranslation("jobsummary.station.phone") + ": " + station.getPhone()));
|
||||
}
|
||||
deliveryStationsContainer.add(stationBox);
|
||||
}
|
||||
|
||||
topRow.add(pickupBox, deliveryStationsContainer);
|
||||
} else {
|
||||
// Fallback: flat delivery fields for old jobs
|
||||
VerticalLayout deliveryBox = borderedBox();
|
||||
deliveryBox.add(new H3(getTranslation("jobsummary.section.delivery") + " "
|
||||
+ formatDateWithTime(job.getDeliveryDate(), job.getDeliveryTime())));
|
||||
deliveryBox.add(new Span(valueOrEmpty(job.getDeliveryCompany())));
|
||||
deliveryBox.add(new Span(valueOrEmpty(job.getDeliverySalutation())
|
||||
+ (job.getDeliverySalutation() != null ? " " : "") + valueOrEmpty(job.getDeliveryFirstName())
|
||||
+ (job.getDeliveryFirstName() != null ? " " : "") + valueOrEmpty(job.getDeliveryLastName())));
|
||||
deliveryBox.add(new Span(concatAddress(job.getDeliveryStreet(), job.getDeliveryHouseNumber())));
|
||||
deliveryBox.add(new Span(concatZipCity(job.getDeliveryZip(), job.getDeliveryCity())));
|
||||
deliveryBox.setWidth("50%");
|
||||
topRow.add(pickupBox, deliveryBox);
|
||||
}
|
||||
content.add(topRow);
|
||||
|
||||
// Aufgaben
|
||||
VerticalLayout tasksBox = borderedBox();
|
||||
tasksBox.add(new H3(getTranslation("jobsummary.section.tasks")));
|
||||
|
||||
// Ensure consistent spacing and width for task cards
|
||||
tasksBox.setSpacing(false);
|
||||
tasksBox.getStyle().set("gap", "var(--lumo-space-xs)");
|
||||
|
||||
// Clear previous task cards
|
||||
taskCards.clear();
|
||||
|
||||
if (tasks == null || tasks.isEmpty()) {
|
||||
tasksBox.add(new Span(getTranslation("jobsummary.tasks.none")));
|
||||
} else {
|
||||
for (BaseTask task : tasks) {
|
||||
if (task != null) {
|
||||
// Use getDisplayName() instead of getText() for task display
|
||||
String displayName = task.getDisplayName();
|
||||
if (displayName != null && !displayName.isBlank()) {
|
||||
Div taskCard = createTaskCard(task, displayName);
|
||||
taskCards.add(taskCard); // Keep reference for hover reset
|
||||
tasksBox.add(taskCard);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
content.add(tasksBox);
|
||||
content.add(createStationTilesSection(job, tasks));
|
||||
|
||||
// Fracht und weitere Infos
|
||||
HorizontalLayout midRow = new HorizontalLayout();
|
||||
@@ -392,6 +306,179 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
return box;
|
||||
}
|
||||
|
||||
private Div createStationTilesSection(Job job, List<BaseTask> tasks) {
|
||||
Div stationGrid = new Div();
|
||||
stationGrid.getStyle().set("display", "grid");
|
||||
stationGrid.getStyle().set("grid-template-columns", "repeat(auto-fit, minmax(220px, 1fr))");
|
||||
stationGrid.getStyle().set("gap", "var(--lumo-space-m)");
|
||||
stationGrid.setWidthFull();
|
||||
|
||||
stationGrid.add(createPickupSummaryTile(job));
|
||||
|
||||
List<DeliveryStation> stations = job.getDeliveryStations();
|
||||
if (stations != null && !stations.isEmpty()) {
|
||||
for (int i = 0; i < stations.size(); i++) {
|
||||
stationGrid.add(createDeliverySummaryTile(stations.get(i), i, stations.size(), tasks));
|
||||
}
|
||||
} else {
|
||||
stationGrid.add(createLegacyDeliverySummaryTile(job, tasks));
|
||||
}
|
||||
|
||||
return stationGrid;
|
||||
}
|
||||
|
||||
private StationTile createPickupSummaryTile(Job job) {
|
||||
String title = getTranslation("jobsummary.section.pickup") + " "
|
||||
+ formatDateWithTime(job.getPickupDate(), job.getPickupTime());
|
||||
List<String> additionalLines = new ArrayList<>();
|
||||
if (job.getPickupPhone() != null && !job.getPickupPhone().isBlank()) {
|
||||
additionalLines.add(getTranslation("jobsummary.station.phone") + ": " + job.getPickupPhone());
|
||||
}
|
||||
|
||||
return createSummaryTile(StationTile.StationType.PICKUP, 0, title, job.getPickupCompany(),
|
||||
buildDisplayName(job.getPickupSalutation(), job.getPickupFirstName(), job.getPickupLastName()),
|
||||
job.getPickupStreet(), job.getPickupHouseNumber(), job.getPickupZip(), job.getPickupCity(),
|
||||
additionalLines);
|
||||
}
|
||||
|
||||
private StationTile createDeliverySummaryTile(DeliveryStation station, int index, int stationCount,
|
||||
List<BaseTask> tasks) {
|
||||
String title = getTranslation("jobsummary.section.delivery") + " "
|
||||
+ (stationCount > 1 ? (index + 1) + " " : "")
|
||||
+ formatDateWithTime(station.getDeliveryDate(), station.getDeliveryTime());
|
||||
List<String> additionalLines = new ArrayList<>();
|
||||
if (station.getPhone() != null && !station.getPhone().isBlank()) {
|
||||
additionalLines.add(getTranslation("jobsummary.station.phone") + ": " + station.getPhone());
|
||||
}
|
||||
|
||||
StationTile tile = createSummaryTile(StationTile.StationType.DELIVERY, index + 1, title, station.getCompany(),
|
||||
buildDisplayName(station.getSalutation(), station.getFirstName(), station.getLastName()),
|
||||
station.getStreet(), station.getHouseNumber(), station.getZip(), station.getCity(), additionalLines);
|
||||
List<BaseTask> stationTasks = getTasksForStation(station, tasks, false);
|
||||
tile.setInteractive(true);
|
||||
tile.setClickListener(clickedTile -> showStationTasksDialog(title, stationTasks));
|
||||
return tile;
|
||||
}
|
||||
|
||||
private StationTile createLegacyDeliverySummaryTile(Job job, List<BaseTask> tasks) {
|
||||
String title = getTranslation("jobsummary.section.delivery") + " "
|
||||
+ formatDateWithTime(job.getDeliveryDate(), job.getDeliveryTime());
|
||||
List<String> additionalLines = new ArrayList<>();
|
||||
if (job.getDeliveryPhone() != null && !job.getDeliveryPhone().isBlank()) {
|
||||
additionalLines.add(getTranslation("jobsummary.station.phone") + ": " + job.getDeliveryPhone());
|
||||
}
|
||||
|
||||
StationTile tile = createSummaryTile(StationTile.StationType.DELIVERY, 1, title, job.getDeliveryCompany(),
|
||||
buildDisplayName(job.getDeliverySalutation(), job.getDeliveryFirstName(), job.getDeliveryLastName()),
|
||||
job.getDeliveryStreet(), job.getDeliveryHouseNumber(), job.getDeliveryZip(), job.getDeliveryCity(),
|
||||
additionalLines);
|
||||
List<BaseTask> stationTasks = getTasksForStation(null, tasks, true);
|
||||
tile.setInteractive(true);
|
||||
tile.setClickListener(clickedTile -> showStationTasksDialog(title, stationTasks));
|
||||
return tile;
|
||||
}
|
||||
|
||||
private StationTile createSummaryTile(StationTile.StationType type, int stationNumber, String title,
|
||||
String company, String displayName, String street, String houseNumber, String zip, String city,
|
||||
List<String> additionalLines) {
|
||||
StationTile tile = new StationTile(type, stationNumber, title.trim(), false);
|
||||
tile.setInteractive(false);
|
||||
tile.updatePreview(company, displayName, null, street, houseNumber, zip, city, additionalLines);
|
||||
return tile;
|
||||
}
|
||||
|
||||
private String buildDisplayName(String salutation, String firstName, String lastName) {
|
||||
List<String> parts = new ArrayList<>();
|
||||
if (salutation != null && !salutation.isBlank()) {
|
||||
parts.add(salutation.trim());
|
||||
}
|
||||
if (firstName != null && !firstName.isBlank()) {
|
||||
parts.add(firstName.trim());
|
||||
}
|
||||
if (lastName != null && !lastName.isBlank()) {
|
||||
parts.add(lastName.trim());
|
||||
}
|
||||
return String.join(" ", parts);
|
||||
}
|
||||
|
||||
private List<BaseTask> getTasksForStation(DeliveryStation station, List<BaseTask> tasks, boolean legacyMode) {
|
||||
if (!legacyMode && station != null && station.getTasks() != null && !station.getTasks().isEmpty()) {
|
||||
return station.getTasks().stream()
|
||||
.filter(task -> task != null && task.getDisplayName() != null && !task.getDisplayName().isBlank())
|
||||
.sorted((left, right) -> Integer.compare(left.getTaskOrder() != null ? left.getTaskOrder() : 0,
|
||||
right.getTaskOrder() != null ? right.getTaskOrder() : 0))
|
||||
.toList();
|
||||
}
|
||||
|
||||
if (tasks == null || tasks.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<BaseTask> stationTasks = new ArrayList<>();
|
||||
int stationOrder = station != null ? station.getStationOrder() : 0;
|
||||
for (BaseTask task : tasks) {
|
||||
if (task == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Integer taskStationOrder = task.getStationOrder();
|
||||
if (legacyMode) {
|
||||
if (taskStationOrder == null || taskStationOrder == stationOrder) {
|
||||
stationTasks.add(task);
|
||||
}
|
||||
} else if (taskStationOrder != null && taskStationOrder == stationOrder) {
|
||||
stationTasks.add(task);
|
||||
}
|
||||
}
|
||||
return stationTasks;
|
||||
}
|
||||
|
||||
private void showStationTasksDialog(String stationTitle, List<BaseTask> tasks) {
|
||||
Dialog dialog = new Dialog();
|
||||
dialog.setWidth("720px");
|
||||
dialog.setMaxWidth("95vw");
|
||||
dialog.setMaxHeight("85vh");
|
||||
dialog.setResizable(true);
|
||||
dialog.setHeaderTitle(stationTitle);
|
||||
dialog.addDialogCloseActionListener(e -> resetAllTaskCardHoverStates());
|
||||
|
||||
taskCards.clear();
|
||||
|
||||
VerticalLayout dialogContent = new VerticalLayout();
|
||||
dialogContent.setPadding(true);
|
||||
dialogContent.setSpacing(true);
|
||||
dialogContent.setWidthFull();
|
||||
dialogContent.getStyle().set("min-width", "0");
|
||||
|
||||
H4 header = new H4(getTranslation("jobsummary.section.tasks"));
|
||||
header.getStyle().set("margin", "0");
|
||||
dialogContent.add(header);
|
||||
|
||||
if (tasks == null || tasks.isEmpty()) {
|
||||
dialogContent.add(new Span(getTranslation("jobsummary.tasks.none")));
|
||||
} else {
|
||||
for (BaseTask task : tasks) {
|
||||
String displayName = task.getDisplayName();
|
||||
if (displayName == null || displayName.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
Div taskCard = createTaskCard(task, displayName);
|
||||
taskCards.add(taskCard);
|
||||
dialogContent.add(taskCard);
|
||||
}
|
||||
}
|
||||
|
||||
Button closeButton = new Button("Schließen", e -> {
|
||||
dialog.close();
|
||||
resetAllTaskCardHoverStates();
|
||||
});
|
||||
closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
dialog.getFooter().add(closeButton);
|
||||
|
||||
dialog.add(dialogContent);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
private String formatLocalDate(java.time.LocalDate date) {
|
||||
try {
|
||||
return DateTimeFormatUtil.formatDate(date);
|
||||
@@ -1054,12 +1141,28 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
taskName.getStyle().set("font-weight", "500").set("font-size", "var(--lumo-font-size-m)").set("color",
|
||||
task.isCompleted() ? "var(--lumo-success-text-color)" : "var(--lumo-body-text-color)");
|
||||
|
||||
Span statusBadge = new Span(getTaskStatusLabel(task));
|
||||
statusBadge.getStyle().set("font-size", "var(--lumo-font-size-xs)")
|
||||
.set("font-weight", "600")
|
||||
.set("padding", "0.2rem 0.55rem")
|
||||
.set("border-radius", "999px")
|
||||
.set("background-color",
|
||||
task.isCompleted() ? "rgba(76, 175, 80, 0.15)" : "rgba(244, 67, 54, 0.12)")
|
||||
.set("color", task.isCompleted() ? "var(--lumo-success-text-color)" : "var(--lumo-error-text-color)");
|
||||
|
||||
HorizontalLayout headerRow = new HorizontalLayout(taskName, statusBadge);
|
||||
headerRow.setWidthFull();
|
||||
headerRow.setPadding(false);
|
||||
headerRow.setSpacing(true);
|
||||
headerRow.setAlignItems(HorizontalLayout.Alignment.CENTER);
|
||||
headerRow.setJustifyContentMode(HorizontalLayout.JustifyContentMode.BETWEEN);
|
||||
|
||||
// Task status/description
|
||||
Span taskDescription = new Span(getTaskDescription(task));
|
||||
taskDescription.getStyle().set("font-size", "var(--lumo-font-size-s)")
|
||||
.set("color", "var(--lumo-secondary-text-color)").set("margin-top", "var(--lumo-space-xs)");
|
||||
|
||||
taskContent.add(taskName, taskDescription);
|
||||
taskContent.add(headerRow, taskDescription);
|
||||
|
||||
// Status indicator
|
||||
Div statusIndicator = new Div();
|
||||
@@ -1097,9 +1200,13 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
||||
}
|
||||
}
|
||||
|
||||
private String getTaskStatusLabel(BaseTask task) {
|
||||
return task.isCompleted() ? "Abgeschlossen" : "Offen";
|
||||
}
|
||||
|
||||
private String getTaskDescription(BaseTask task) {
|
||||
if (task.isCompleted()) {
|
||||
return "Abgeschlossen"
|
||||
return "Erledigt"
|
||||
+ (task.getCompletedAt() != null ? " am " + formatLocalDate(task.getCompletedAt().toLocalDate())
|
||||
: "");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user