feat: adapt job flow for delivery stations
This commit is contained in:
@@ -1,23 +1,10 @@
|
|||||||
package de.assecutor.votianlt.messaging;
|
package de.assecutor.votianlt.messaging;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
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.
|
* Publishing helper to send JSON payloads to clients via WebSocket.
|
||||||
@@ -32,13 +19,10 @@ class MessagingPublisherImpl implements MessagingPublisher {
|
|||||||
|
|
||||||
private final WebSocketService webSocketService;
|
private final WebSocketService webSocketService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final TranslationService translationService;
|
|
||||||
|
|
||||||
public MessagingPublisherImpl(WebSocketService webSocketService, ObjectMapper objectMapper,
|
public MessagingPublisherImpl(WebSocketService webSocketService, ObjectMapper objectMapper) {
|
||||||
TranslationService translationService) {
|
|
||||||
this.webSocketService = webSocketService;
|
this.webSocketService = webSocketService;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.translationService = translationService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -53,10 +37,7 @@ class MessagingPublisherImpl implements MessagingPublisher {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verarbeite Payload und füge Übersetzungen hinzu wenn nötig
|
String json = objectMapper.writeValueAsString(payload);
|
||||||
Object processedPayload = processPayloadWithTranslations(payload);
|
|
||||||
|
|
||||||
String json = objectMapper.writeValueAsString(processedPayload);
|
|
||||||
byte[] data = json.getBytes(StandardCharsets.UTF_8);
|
byte[] data = json.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
webSocketService.sendToClient(clientId, messageType, data).thenRun(() -> {
|
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);
|
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;
|
package de.assecutor.votianlt.model;
|
||||||
|
|
||||||
|
import de.assecutor.votianlt.model.task.BaseTask;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
@@ -7,6 +8,8 @@ import org.springframework.data.mongodb.core.mapping.Field;
|
|||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalTime;
|
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
|
* Embedded delivery station within a Job. Each job can have up to 25 delivery
|
||||||
@@ -56,4 +59,7 @@ public class DeliveryStation {
|
|||||||
|
|
||||||
@Field("delivery_time")
|
@Field("delivery_time")
|
||||||
private LocalTime deliveryTime;
|
private LocalTime deliveryTime;
|
||||||
|
|
||||||
|
@Field("tasks")
|
||||||
|
private List<BaseTask> tasks = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
private boolean saveAddress;
|
private boolean saveAddress;
|
||||||
private List<BaseTask> tasks = new ArrayList<>();
|
private List<BaseTask> tasks = new ArrayList<>();
|
||||||
private boolean addressValidatedByGoogle;
|
private boolean addressValidatedByGoogle;
|
||||||
|
private AddressValidationResult addressValidationResult;
|
||||||
|
|
||||||
public boolean isAddressValidatedByGoogle() {
|
public boolean isAddressValidatedByGoogle() {
|
||||||
return addressValidatedByGoogle;
|
return addressValidatedByGoogle;
|
||||||
@@ -63,6 +64,14 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
this.addressValidatedByGoogle = addressValidatedByGoogle;
|
this.addressValidatedByGoogle = addressValidatedByGoogle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AddressValidationResult getAddressValidationResult() {
|
||||||
|
return addressValidationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAddressValidationResult(AddressValidationResult addressValidationResult) {
|
||||||
|
this.addressValidationResult = addressValidationResult;
|
||||||
|
}
|
||||||
|
|
||||||
public String getCompany() {
|
public String getCompany() {
|
||||||
return company;
|
return company;
|
||||||
}
|
}
|
||||||
@@ -191,6 +200,7 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
|
|
||||||
private final DeliveryStationTile.TranslationHelper translationHelper;
|
private final DeliveryStationTile.TranslationHelper translationHelper;
|
||||||
private final AddressValidationService addressValidationService;
|
private final AddressValidationService addressValidationService;
|
||||||
|
private final java.util.Map<String, Customer> companyAddressOptions = new java.util.LinkedHashMap<>();
|
||||||
|
|
||||||
public DeliveryStationDialog(String dialogTitle, List<Customer> customers,
|
public DeliveryStationDialog(String dialogTitle, List<Customer> customers,
|
||||||
DeliveryStationTile.TranslationHelper translationHelper, SaveListener saveListener,
|
DeliveryStationTile.TranslationHelper translationHelper, SaveListener saveListener,
|
||||||
@@ -359,6 +369,7 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
|
|
||||||
if (validationResult.isValid()) {
|
if (validationResult.isValid()) {
|
||||||
data.setAddressValidatedByGoogle(true);
|
data.setAddressValidatedByGoogle(true);
|
||||||
|
data.setAddressValidationResult(validationResult);
|
||||||
if (saveListener != null) {
|
if (saveListener != null) {
|
||||||
saveListener.onSave(data);
|
saveListener.onSave(data);
|
||||||
}
|
}
|
||||||
@@ -378,6 +389,7 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
translationHelper.getTranslation("addjob.validation.address.correct"));
|
translationHelper.getTranslation("addjob.validation.address.correct"));
|
||||||
confirmDialog.addConfirmListener(ev -> {
|
confirmDialog.addConfirmListener(ev -> {
|
||||||
data.setAddressValidatedByGoogle(false);
|
data.setAddressValidatedByGoogle(false);
|
||||||
|
data.setAddressValidationResult(validationResult);
|
||||||
if (saveListener != null) {
|
if (saveListener != null) {
|
||||||
saveListener.onSave(data);
|
saveListener.onSave(data);
|
||||||
}
|
}
|
||||||
@@ -406,8 +418,12 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
public void setData(DeliveryData data) {
|
public void setData(DeliveryData data) {
|
||||||
if (data == null)
|
if (data == null)
|
||||||
return;
|
return;
|
||||||
if (data.getCompany() != null)
|
String companyOption = findCompanyOptionLabel(data);
|
||||||
|
if (companyOption != null) {
|
||||||
|
company.setValue(companyOption);
|
||||||
|
} else if (data.getCompany() != null) {
|
||||||
company.setValue(data.getCompany());
|
company.setValue(data.getCompany());
|
||||||
|
}
|
||||||
if (data.getSalutation() != null)
|
if (data.getSalutation() != null)
|
||||||
salutation.setValue(data.getSalutation());
|
salutation.setValue(data.getSalutation());
|
||||||
if (data.getFirstName() != null)
|
if (data.getFirstName() != null)
|
||||||
@@ -448,7 +464,7 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
|
|
||||||
private DeliveryData collectData() {
|
private DeliveryData collectData() {
|
||||||
DeliveryData data = new DeliveryData();
|
DeliveryData data = new DeliveryData();
|
||||||
data.setCompany(company.getValue());
|
data.setCompany(resolveCompanyValue(company.getValue()));
|
||||||
data.setSalutation(salutation.getValue());
|
data.setSalutation(salutation.getValue());
|
||||||
data.setFirstName(firstName.getValue());
|
data.setFirstName(firstName.getValue());
|
||||||
data.setLastName(lastName.getValue());
|
data.setLastName(lastName.getValue());
|
||||||
@@ -527,49 +543,126 @@ public class DeliveryStationDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupCompanyAutocomplete(ComboBox<String> companyField, List<Customer> customers) {
|
private void setupCompanyAutocomplete(ComboBox<String> companyField, List<Customer> customers) {
|
||||||
List<String> companyNames = customers.stream().map(Customer::getCompanyName)
|
companyAddressOptions.clear();
|
||||||
.filter(name -> name != null && !name.trim().isEmpty()).distinct().sorted().toList();
|
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 -> {
|
companyField.addValueChangeListener(event -> {
|
||||||
String selectedCompany = event.getValue();
|
Customer customer = companyAddressOptions.get(event.getValue());
|
||||||
if (selectedCompany == null || selectedCompany.trim().isEmpty()) {
|
if (customer == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Customer> matchingCustomer = customers.stream()
|
if (customer.getTitle() != null
|
||||||
.filter(c -> selectedCompany.equals(c.getCompanyName())).findFirst();
|
&& ("Herr".equalsIgnoreCase(customer.getTitle()) || "Frau".equalsIgnoreCase(customer.getTitle())
|
||||||
|
|| "Divers".equalsIgnoreCase(customer.getTitle()))) {
|
||||||
if (matchingCustomer.isPresent()) {
|
salutation.setValue(customer.getTitle());
|
||||||
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.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()));
|
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
|
// Task Management
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ public class PickupStationDialog extends Dialog {
|
|||||||
private AppUser appUser;
|
private AppUser appUser;
|
||||||
private List<CargoItem> cargoItems = new ArrayList<>();
|
private List<CargoItem> cargoItems = new ArrayList<>();
|
||||||
private boolean addressValidatedByGoogle;
|
private boolean addressValidatedByGoogle;
|
||||||
|
private AddressValidationResult addressValidationResult;
|
||||||
|
|
||||||
public boolean isAddressValidatedByGoogle() {
|
public boolean isAddressValidatedByGoogle() {
|
||||||
return addressValidatedByGoogle;
|
return addressValidatedByGoogle;
|
||||||
@@ -75,6 +76,14 @@ public class PickupStationDialog extends Dialog {
|
|||||||
this.addressValidatedByGoogle = addressValidatedByGoogle;
|
this.addressValidatedByGoogle = addressValidatedByGoogle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AddressValidationResult getAddressValidationResult() {
|
||||||
|
return addressValidationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAddressValidationResult(AddressValidationResult addressValidationResult) {
|
||||||
|
this.addressValidationResult = addressValidationResult;
|
||||||
|
}
|
||||||
|
|
||||||
public String getCompany() {
|
public String getCompany() {
|
||||||
return company;
|
return company;
|
||||||
}
|
}
|
||||||
@@ -485,6 +494,7 @@ public class PickupStationDialog extends Dialog {
|
|||||||
|
|
||||||
if (validationResult.isValid()) {
|
if (validationResult.isValid()) {
|
||||||
data.setAddressValidatedByGoogle(true);
|
data.setAddressValidatedByGoogle(true);
|
||||||
|
data.setAddressValidationResult(validationResult);
|
||||||
if (saveListener != null) {
|
if (saveListener != null) {
|
||||||
saveListener.onSave(data);
|
saveListener.onSave(data);
|
||||||
}
|
}
|
||||||
@@ -504,6 +514,7 @@ public class PickupStationDialog extends Dialog {
|
|||||||
translationHelper.getTranslation("addjob.validation.address.correct"));
|
translationHelper.getTranslation("addjob.validation.address.correct"));
|
||||||
confirmDialog.addConfirmListener(ev -> {
|
confirmDialog.addConfirmListener(ev -> {
|
||||||
data.setAddressValidatedByGoogle(false);
|
data.setAddressValidatedByGoogle(false);
|
||||||
|
data.setAddressValidationResult(validationResult);
|
||||||
if (saveListener != null) {
|
if (saveListener != null) {
|
||||||
saveListener.onSave(data);
|
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.FlexComponent;
|
||||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A compact tile representing a station (pickup or delivery) in a grid layout.
|
* 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.type = type;
|
||||||
this.stationNumber = stationNumber;
|
this.stationNumber = stationNumber;
|
||||||
|
|
||||||
setPadding(true);
|
setPadding(false);
|
||||||
setSpacing(false);
|
setSpacing(false);
|
||||||
getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
|
getStyle().set("border", "1px solid var(--lumo-contrast-20pct)");
|
||||||
getStyle().set("border-radius", "var(--lumo-border-radius-m)");
|
getStyle().set("border-radius", "var(--lumo-border-radius-m)");
|
||||||
@@ -48,6 +49,7 @@ public class StationTile extends VerticalLayout {
|
|||||||
getStyle().set("cursor", "pointer");
|
getStyle().set("cursor", "pointer");
|
||||||
getStyle().set("aspect-ratio", "1 / 1");
|
getStyle().set("aspect-ratio", "1 / 1");
|
||||||
getStyle().set("overflow", "hidden");
|
getStyle().set("overflow", "hidden");
|
||||||
|
getStyle().set("padding", "var(--lumo-space-m)");
|
||||||
|
|
||||||
// Header with title and optional delete button
|
// Header with title and optional delete button
|
||||||
title = new H3(titleText);
|
title = new H3(titleText);
|
||||||
@@ -82,7 +84,8 @@ public class StationTile extends VerticalLayout {
|
|||||||
previewContent = new VerticalLayout();
|
previewContent = new VerticalLayout();
|
||||||
previewContent.setPadding(false);
|
previewContent.setPadding(false);
|
||||||
previewContent.setSpacing(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");
|
previewContent.getStyle().set("flex-grow", "1");
|
||||||
add(previewContent);
|
add(previewContent);
|
||||||
|
|
||||||
@@ -99,6 +102,11 @@ public class StationTile extends VerticalLayout {
|
|||||||
|
|
||||||
public void updatePreview(String company, String firstName, String lastName, String street, String houseNumber,
|
public void updatePreview(String company, String firstName, String lastName, String street, String houseNumber,
|
||||||
String zip, String city) {
|
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.removeAll();
|
||||||
previewContent.setJustifyContentMode(FlexComponent.JustifyContentMode.START);
|
previewContent.setJustifyContentMode(FlexComponent.JustifyContentMode.START);
|
||||||
previewContent.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.START);
|
previewContent.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.START);
|
||||||
@@ -128,6 +136,15 @@ public class StationTile extends VerticalLayout {
|
|||||||
hasData = true;
|
hasData = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (additionalLines != null) {
|
||||||
|
for (String line : additionalLines) {
|
||||||
|
if (line != null && !line.trim().isEmpty()) {
|
||||||
|
addPreviewLine(line);
|
||||||
|
hasData = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasData) {
|
if (!hasData) {
|
||||||
updateEmptyPreview();
|
updateEmptyPreview();
|
||||||
}
|
}
|
||||||
@@ -144,8 +161,8 @@ public class StationTile extends VerticalLayout {
|
|||||||
|
|
||||||
private void addPreviewLine(String text) {
|
private void addPreviewLine(String text) {
|
||||||
Span span = new Span(text);
|
Span span = new Span(text);
|
||||||
span.getStyle().set("font-size", "var(--lumo-font-size-s)").set("word-break", "break-word").set("color",
|
span.getStyle().set("font-size", "var(--lumo-font-size-xs)").set("line-height", "1.2").set("word-break",
|
||||||
"var(--lumo-secondary-text-color)");
|
"break-word").set("color", "var(--lumo-secondary-text-color)");
|
||||||
previewContent.add(span);
|
previewContent.add(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +190,10 @@ public class StationTile extends VerticalLayout {
|
|||||||
this.deleteListener = listener;
|
this.deleteListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setInteractive(boolean interactive) {
|
||||||
|
getStyle().set("cursor", interactive ? "pointer" : "default");
|
||||||
|
}
|
||||||
|
|
||||||
public void setAddressValidated(boolean validated) {
|
public void setAddressValidated(boolean validated) {
|
||||||
if (validated) {
|
if (validated) {
|
||||||
getStyle().set("background-color", "rgba(76, 175, 80, 0.15)");
|
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.server.auth.AnonymousAllowed;
|
||||||
import com.vaadin.flow.shared.Registration;
|
import com.vaadin.flow.shared.Registration;
|
||||||
import de.assecutor.votianlt.model.User;
|
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.AppUserService;
|
||||||
import de.assecutor.votianlt.pages.service.UserInvoiceDataService;
|
|
||||||
import de.assecutor.votianlt.pages.view.EditProfileView;
|
import de.assecutor.votianlt.pages.view.EditProfileView;
|
||||||
import de.assecutor.votianlt.model.Language;
|
import de.assecutor.votianlt.model.Language;
|
||||||
import de.assecutor.votianlt.security.SecurityService;
|
import de.assecutor.votianlt.security.SecurityService;
|
||||||
@@ -46,7 +44,6 @@ import java.util.Objects;
|
|||||||
public final class MainLayout extends AppLayout {
|
public final class MainLayout extends AppLayout {
|
||||||
|
|
||||||
private final SecurityService securityService;
|
private final SecurityService securityService;
|
||||||
private final UserInvoiceDataService userInvoiceDataService;
|
|
||||||
private final MessageService messageService;
|
private final MessageService messageService;
|
||||||
private final MessageBadgeUpdateService messageBadgeUpdateService;
|
private final MessageBadgeUpdateService messageBadgeUpdateService;
|
||||||
private final AppUserService appUserService;
|
private final AppUserService appUserService;
|
||||||
@@ -57,11 +54,9 @@ public final class MainLayout extends AppLayout {
|
|||||||
private MenuTreeItem messagesTreeItem;
|
private MenuTreeItem messagesTreeItem;
|
||||||
private Registration badgeUpdateRegistration;
|
private Registration badgeUpdateRegistration;
|
||||||
|
|
||||||
public MainLayout(SecurityService securityService, UserInvoiceDataService userInvoiceDataService,
|
public MainLayout(SecurityService securityService, MessageService messageService,
|
||||||
MessageService messageService, MessageBadgeUpdateService messageBadgeUpdateService,
|
MessageBadgeUpdateService messageBadgeUpdateService, AppUserService appUserService) {
|
||||||
AppUserService appUserService) {
|
|
||||||
this.securityService = securityService;
|
this.securityService = securityService;
|
||||||
this.userInvoiceDataService = userInvoiceDataService;
|
|
||||||
this.messageService = messageService;
|
this.messageService = messageService;
|
||||||
this.messageBadgeUpdateService = messageBadgeUpdateService;
|
this.messageBadgeUpdateService = messageBadgeUpdateService;
|
||||||
this.appUserService = appUserService;
|
this.appUserService = appUserService;
|
||||||
@@ -141,12 +136,6 @@ public final class MainLayout extends AppLayout {
|
|||||||
treeData.addItem(verwaltungItem,
|
treeData.addItem(verwaltungItem,
|
||||||
new MenuTreeItem(getTranslation("nav.statistics"), "statistics", VaadinIcon.BAR_CHART));
|
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"
|
// Add children to "Benutzer"
|
||||||
treeData.addItem(benutzerItem,
|
treeData.addItem(benutzerItem,
|
||||||
new MenuTreeItem(getTranslation("nav.profile"), "edit-profile", VaadinIcon.USER));
|
new MenuTreeItem(getTranslation("nav.profile"), "edit-profile", VaadinIcon.USER));
|
||||||
@@ -338,20 +327,6 @@ public final class MainLayout extends AppLayout {
|
|||||||
return userMenu;
|
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
|
@Override
|
||||||
protected void onAttach(AttachEvent attachEvent) {
|
protected void onAttach(AttachEvent attachEvent) {
|
||||||
super.onAttach(attachEvent);
|
super.onAttach(attachEvent);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package de.assecutor.votianlt.pages.service;
|
|||||||
|
|
||||||
import de.assecutor.votianlt.dto.JobWithRelatedDataDTO;
|
import de.assecutor.votianlt.dto.JobWithRelatedDataDTO;
|
||||||
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.JobStatus;
|
import de.assecutor.votianlt.model.JobStatus;
|
||||||
import de.assecutor.votianlt.model.task.BaseTask;
|
import de.assecutor.votianlt.model.task.BaseTask;
|
||||||
@@ -21,7 +22,11 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -89,18 +94,30 @@ public class AddJobService {
|
|||||||
.filter(task -> task.getTaskType() != null) // Filter nach TaskType statt Text
|
.filter(task -> task.getTaskType() != null) // Filter nach TaskType statt Text
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Setze JobId und stelle sicher, dass taskOrder korrekt ist
|
Map<Integer, Integer> taskOrderByStation = new HashMap<>();
|
||||||
for (int i = 0; i < filteredTasks.size(); i++) {
|
|
||||||
BaseTask task = filteredTasks.get(i);
|
// Setze JobId und stelle sicher, dass taskOrder je Lieferstation korrekt ist
|
||||||
|
for (BaseTask task : filteredTasks) {
|
||||||
task.setJobId(jobId);
|
task.setJobId(jobId);
|
||||||
// Verwende die bereits gesetzte taskOrder oder setze sie auf den Index
|
int stationOrder = task.getStationOrder() != null ? task.getStationOrder() : 0;
|
||||||
if (task.getTaskOrder() == null || task.getTaskOrder() != i) {
|
if (task.getTaskOrder() == null) {
|
||||||
task.setTaskOrder(i); // Stelle sicher, dass die Reihenfolge stimmt
|
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);
|
taskRepository.saveAll(filteredTasks);
|
||||||
|
attachTasksToDeliveryStations(savedJob, filteredTasks);
|
||||||
|
savedJob = jobRepository.save(savedJob);
|
||||||
log.info("Saved {} tasks for job {} with ordering", filteredTasks.size(), jobId);
|
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) {
|
if (modified) {
|
||||||
@@ -215,4 +232,26 @@ public class AddJobService {
|
|||||||
log.warn("[JOB] Failed to send job_created notification: {}", e.getMessage());
|
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.net.http.HttpResponse;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service zur Validierung von Adressen über die Google Geocoding API.
|
* Service zur Validierung von Adressen über die Google Geocoding API.
|
||||||
@@ -118,11 +120,12 @@ public class AddressValidationService {
|
|||||||
double lat = location.path("lat").asDouble();
|
double lat = location.path("lat").asDouble();
|
||||||
double lng = location.path("lng").asDouble();
|
double lng = location.path("lng").asDouble();
|
||||||
|
|
||||||
// Prüfen, ob die Adresse als "ROOFTOP" (genaue Adresse) oder
|
// Google liefert für valide Adressen nicht immer nur ROOFTOP/RANGE_INTERPOLATED.
|
||||||
// "RANGE_INTERPOLATED" gefunden wurde
|
// Für unseren Flow reicht ein erfolgreicher Geocoding-Treffer mit Koordinaten.
|
||||||
String locationType = geometry.path("location_type").asText();
|
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;
|
boolean hasPostalCode = false;
|
||||||
|
|
||||||
JsonNode addressComponents = firstResult.path("address_components");
|
JsonNode addressComponents = firstResult.path("address_components");
|
||||||
@@ -131,6 +134,7 @@ public class AddressValidationService {
|
|||||||
for (JsonNode type : types) {
|
for (JsonNode type : types) {
|
||||||
String typeStr = type.asText();
|
String typeStr = type.asText();
|
||||||
if ("street_number".equals(typeStr)) {
|
if ("street_number".equals(typeStr)) {
|
||||||
|
hasStreetNumber = true;
|
||||||
} else if ("postal_code".equals(typeStr)) {
|
} else if ("postal_code".equals(typeStr)) {
|
||||||
hasPostalCode = true;
|
hasPostalCode = true;
|
||||||
}
|
}
|
||||||
@@ -138,19 +142,22 @@ public class AddressValidationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ergebnis setzen
|
// Ergebnis setzen
|
||||||
result.setValid(isPrecise && hasPostalCode);
|
result.setValid(hasCoordinates);
|
||||||
result.setFormattedAddress(formattedAddress);
|
result.setFormattedAddress(formattedAddress);
|
||||||
result.setLatitude(lat);
|
result.setLatitude(lat);
|
||||||
result.setLongitude(lng);
|
result.setLongitude(lng);
|
||||||
|
|
||||||
if (result.isValid()) {
|
if (result.isValid()) {
|
||||||
result.setValidationMessage("Adresse erfolgreich validiert");
|
result.setValidationMessage("Adresse erfolgreich validiert");
|
||||||
|
log.debug(
|
||||||
|
"Adressvalidierung erfolgreich: {} -> {} (locationType={}, streetNumber={}, postalCode={})",
|
||||||
|
addressString, formattedAddress, locationType, hasStreetNumber, hasPostalCode);
|
||||||
} else {
|
} 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) {
|
} catch (Exception e) {
|
||||||
log.error("Fehler bei der Adressvalidierung", e);
|
log.error("Fehler bei der Adressvalidierung", e);
|
||||||
result.setValidationMessage("Fehler: " + e.getMessage());
|
result.setValidationMessage("Fehler: " + e.getMessage());
|
||||||
@@ -191,6 +198,17 @@ public class AddressValidationService {
|
|||||||
*/
|
*/
|
||||||
public RouteCalculationResult calculateRoute(AddressValidationResult pickupResult,
|
public RouteCalculationResult calculateRoute(AddressValidationResult pickupResult,
|
||||||
AddressValidationResult deliveryResult) {
|
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();
|
RouteCalculationResult routeResult = new RouteCalculationResult();
|
||||||
|
|
||||||
// Prüfen, ob API-Key konfiguriert ist
|
// Prüfen, ob API-Key konfiguriert ist
|
||||||
@@ -200,26 +218,41 @@ public class AddressValidationService {
|
|||||||
return routeResult;
|
return routeResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfen, ob beide Adressen gültige Koordinaten haben
|
if (stationResults == null || stationResults.size() < 2) {
|
||||||
if (pickupResult == null || !pickupResult.isValid() || deliveryResult == null || !deliveryResult.isValid()) {
|
routeResult.setRouteMessage("Mindestens zwei validierte Stationen werden benötigt");
|
||||||
routeResult.setRouteMessage("Beide Adressen müssen validiert sein");
|
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;
|
return routeResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
AddressValidationResult originResult = stationResults.getFirst();
|
||||||
|
AddressValidationResult destinationResult = stationResults.getLast();
|
||||||
|
|
||||||
// Koordinaten für Start und Ziel
|
// Koordinaten für Start und Ziel
|
||||||
String origin = String.format("%s,%s", pickupResult.getLatitude(), pickupResult.getLongitude());
|
String origin = formatLatLng(originResult);
|
||||||
String destination = String.format("%s,%s", deliveryResult.getLatitude(), deliveryResult.getLongitude());
|
String destination = formatLatLng(destinationResult);
|
||||||
|
|
||||||
// URL für die Directions API erstellen
|
// URL für die Directions API erstellen
|
||||||
String requestUrl = String.format("%s?origin=%s&destination=%s&mode=driving&key=%s&language=de®ion=de",
|
StringBuilder requestUrl = new StringBuilder(String.format(
|
||||||
DIRECTIONS_API_URL, origin, destination, googleMapsApiKey);
|
"%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(),
|
if (stationResults.size() > 2) {
|
||||||
deliveryResult.getFormattedAddress());
|
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
|
// 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();
|
.timeout(Duration.ofSeconds(10)).build();
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
@@ -254,27 +287,26 @@ public class AddressValidationService {
|
|||||||
return routeResult;
|
return routeResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ersten Leg (Hauptstrecke) verwenden
|
int totalDistanceMeters = 0;
|
||||||
JsonNode firstLeg = legs.get(0);
|
int totalDurationSeconds = 0;
|
||||||
|
|
||||||
// Distanz extrahieren
|
for (JsonNode leg : legs) {
|
||||||
JsonNode distanceNode = firstLeg.path("distance");
|
totalDistanceMeters += leg.path("distance").path("value").asInt();
|
||||||
int distanceMeters = distanceNode.path("value").asInt();
|
totalDurationSeconds += leg.path("duration").path("value").asInt();
|
||||||
String distanceText = distanceNode.path("text").asText();
|
}
|
||||||
|
|
||||||
// Dauer extrahieren
|
double totalDistanceKm = totalDistanceMeters / 1000.0;
|
||||||
JsonNode durationNode = firstLeg.path("duration");
|
String distanceText = String.format(Locale.GERMANY, "%.1f km", totalDistanceKm);
|
||||||
int durationSeconds = durationNode.path("value").asInt();
|
String durationText = formatDuration(totalDurationSeconds);
|
||||||
String durationText = durationNode.path("text").asText();
|
|
||||||
|
|
||||||
// Ergebnis setzen
|
// Ergebnis setzen
|
||||||
routeResult.setValid(true);
|
routeResult.setValid(true);
|
||||||
routeResult.setDistanceKm(distanceMeters / 1000.0);
|
routeResult.setDistanceKm(totalDistanceKm);
|
||||||
routeResult.setDurationSeconds(durationSeconds);
|
routeResult.setDurationSeconds(totalDurationSeconds);
|
||||||
routeResult.setFormattedDistance(distanceText);
|
routeResult.setFormattedDistance(distanceText);
|
||||||
routeResult.setFormattedDuration(durationText);
|
routeResult.setFormattedDuration(durationText);
|
||||||
routeResult.setRouteMessage(
|
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(),
|
log.debug("Routenberechnung erfolgreich: {} km, {} Min.", routeResult.getDistanceKm(),
|
||||||
routeResult.getDurationMinutes());
|
routeResult.getDurationMinutes());
|
||||||
@@ -286,4 +318,18 @@ public class AddressValidationService {
|
|||||||
|
|
||||||
return routeResult;
|
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.combobox.ComboBox;
|
||||||
import com.vaadin.flow.component.confirmdialog.ConfirmDialog;
|
import com.vaadin.flow.component.confirmdialog.ConfirmDialog;
|
||||||
import com.vaadin.flow.component.dialog.Dialog;
|
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.datepicker.DatePicker;
|
||||||
import com.vaadin.flow.component.html.Div;
|
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.FlexComponent;
|
||||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
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.component.textfield.TextField;
|
||||||
import com.vaadin.flow.data.binder.Binder;
|
import com.vaadin.flow.data.binder.Binder;
|
||||||
import com.vaadin.flow.component.Component;
|
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.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import de.assecutor.votianlt.model.Job;
|
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.BaseTask;
|
||||||
|
import de.assecutor.votianlt.model.task.CommentTask;
|
||||||
import de.assecutor.votianlt.model.task.ConfirmationTask;
|
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.model.task.TodoListTask;
|
||||||
import de.assecutor.votianlt.pages.service.AddJobService;
|
import de.assecutor.votianlt.pages.service.AddJobService;
|
||||||
import de.assecutor.votianlt.pages.service.CustomerService;
|
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.PickupStationDialog;
|
||||||
import de.assecutor.votianlt.pages.base.ui.component.DeliveryStationDialog;
|
import de.assecutor.votianlt.pages.base.ui.component.DeliveryStationDialog;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
@Route(value = "add_job", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
@Route(value = "add_job", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||||
@RolesAllowed("USER")
|
@RolesAllowed("USER")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class AddJobView extends Main implements HasDynamicTitle {
|
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
|
@Override
|
||||||
public String getPageTitle() {
|
public String getPageTitle() {
|
||||||
@@ -160,6 +171,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
// Adressvalidierung
|
// Adressvalidierung
|
||||||
private final Map<String, AddressValidationResult> addressValidationResults = new HashMap<>();
|
private final Map<String, AddressValidationResult> addressValidationResults = new HashMap<>();
|
||||||
private RouteCalculationResult routeCalculationResult;
|
private RouteCalculationResult routeCalculationResult;
|
||||||
|
private boolean pickupAddressValidatedByGoogle;
|
||||||
|
private final List<Boolean> deliveryStationsValidatedByGoogle = new ArrayList<>();
|
||||||
|
|
||||||
public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService,
|
public AddJobView(AddJobService addJobService, AddCustomerService addCustomerService,
|
||||||
CustomerService customerService, AppUserService appUserService, TaskTemplateService taskTemplateService,
|
CustomerService customerService, AppUserService appUserService, TaskTemplateService taskTemplateService,
|
||||||
@@ -354,11 +367,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
applyStationsButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
applyStationsButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
applyStationsButton.setWidthFull();
|
applyStationsButton.setWidthFull();
|
||||||
applyStationsButton.setEnabled(false);
|
applyStationsButton.setEnabled(false);
|
||||||
applyStationsButton.addClickListener(e -> {
|
applyStationsButton.addClickListener(e -> handleApplyStations());
|
||||||
applyStationsButton.setVisible(false);
|
|
||||||
priceAndDetailsSection.setVisible(true);
|
|
||||||
submitButtonLayout.setVisible(true);
|
|
||||||
});
|
|
||||||
tabContent.add(applyStationsButton);
|
tabContent.add(applyStationsButton);
|
||||||
|
|
||||||
// Route Info Box
|
// Route Info Box
|
||||||
@@ -631,6 +640,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
// Add empty state for this station
|
// Add empty state for this station
|
||||||
deliveryStationsState.add(new DeliveryStation());
|
deliveryStationsState.add(new DeliveryStation());
|
||||||
deliveryStationsSaveAddress.add(true);
|
deliveryStationsSaveAddress.add(true);
|
||||||
|
deliveryStationsValidatedByGoogle.add(false);
|
||||||
|
|
||||||
int stationIndex = deliveryStationTilesList.size();
|
int stationIndex = deliveryStationTilesList.size();
|
||||||
tile.setClickListener(t -> openDeliveryDialog(t, stationIndex));
|
tile.setClickListener(t -> openDeliveryDialog(t, stationIndex));
|
||||||
@@ -647,6 +657,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
stationsGridContainer.add(addStationButton);
|
stationsGridContainer.add(addStationButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetRouteInformation();
|
||||||
resetStationsAppliedState();
|
resetStationsAppliedState();
|
||||||
triggerValidation();
|
triggerValidation();
|
||||||
updateTabLabels();
|
updateTabLabels();
|
||||||
@@ -670,6 +681,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
deliveryStationTilesList.remove(removeIdx);
|
deliveryStationTilesList.remove(removeIdx);
|
||||||
deliveryStationsState.remove(removeIdx);
|
deliveryStationsState.remove(removeIdx);
|
||||||
deliveryStationsSaveAddress.remove(removeIdx);
|
deliveryStationsSaveAddress.remove(removeIdx);
|
||||||
|
deliveryStationsValidatedByGoogle.remove(removeIdx);
|
||||||
deliveryStationTasksState.remove(removeIdx);
|
deliveryStationTasksState.remove(removeIdx);
|
||||||
// Re-index tasks state for remaining stations
|
// Re-index tasks state for remaining stations
|
||||||
Map<Integer, List<BaseTask>> reindexed = new HashMap<>();
|
Map<Integer, List<BaseTask>> reindexed = new HashMap<>();
|
||||||
@@ -737,12 +749,8 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
savePickupAddress.setValue(data.isSaveAddress());
|
savePickupAddress.setValue(data.isSaveAddress());
|
||||||
|
|
||||||
// Sync appointment fields for binder/submit
|
// Sync appointment fields for binder/submit
|
||||||
if (data.getAppointmentDate() != null) {
|
pickupDate.setValue(data.getAppointmentDate());
|
||||||
pickupDate.setValue(data.getAppointmentDate());
|
pickupTime.setValue(data.getAppointmentTime());
|
||||||
}
|
|
||||||
if (data.getAppointmentTime() != null) {
|
|
||||||
pickupTime.setValue(data.getAppointmentTime());
|
|
||||||
}
|
|
||||||
digitalProcessing.setValue(data.isDigitalProcessing());
|
digitalProcessing.setValue(data.isDigitalProcessing());
|
||||||
if (data.getAppUser() != null) {
|
if (data.getAppUser() != null) {
|
||||||
appUser.setValue(data.getAppUser());
|
appUser.setValue(data.getAppUser());
|
||||||
@@ -758,8 +766,12 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
|
|
||||||
// Update tile preview
|
// Update tile preview
|
||||||
pickupTile.updatePreview(data.getCompany(), data.getFirstName(), data.getLastName(),
|
pickupTile.updatePreview(data.getCompany(), data.getFirstName(), data.getLastName(),
|
||||||
data.getStreet(), data.getHouseNumber(), data.getZip(), data.getCity());
|
data.getStreet(), data.getHouseNumber(), data.getZip(), data.getCity(),
|
||||||
pickupTile.setAddressValidated(data.isAddressValidatedByGoogle());
|
buildPickupPreviewDetails(data.getAppointmentDate(), data.getAppointmentTime(),
|
||||||
|
data.isDigitalProcessing(), data.getAppUser(), data.getCargoItems()));
|
||||||
|
pickupAddressValidatedByGoogle = data.isAddressValidatedByGoogle();
|
||||||
|
storePickupAddressValidationResult(data.getAddressValidationResult());
|
||||||
|
pickupTile.setAddressValidated(pickupAddressValidatedByGoogle);
|
||||||
|
|
||||||
resetRouteInformation();
|
resetRouteInformation();
|
||||||
resetStationsAppliedState();
|
resetStationsAppliedState();
|
||||||
@@ -787,11 +799,191 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
currentData.setDigitalProcessing(digitalProcessing.getValue());
|
currentData.setDigitalProcessing(digitalProcessing.getValue());
|
||||||
currentData.setAppUser(appUser.getValue());
|
currentData.setAppUser(appUser.getValue());
|
||||||
currentData.setCargoItems(new ArrayList<>(cargoItemsState));
|
currentData.setCargoItems(new ArrayList<>(cargoItemsState));
|
||||||
|
currentData.setAddressValidatedByGoogle(pickupAddressValidatedByGoogle);
|
||||||
dialog.setData(currentData);
|
dialog.setData(currentData);
|
||||||
|
|
||||||
dialog.open();
|
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) {
|
private void openDeliveryDialog(StationTile tile, int stationIndex) {
|
||||||
// Ensure index is valid (could have changed due to deletions)
|
// Ensure index is valid (could have changed due to deletions)
|
||||||
int actualIndex = deliveryStationTilesList.indexOf(tile);
|
int actualIndex = deliveryStationTilesList.indexOf(tile);
|
||||||
@@ -822,6 +1014,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
station.setAddressAddition(data.getAddressAddition());
|
station.setAddressAddition(data.getAddressAddition());
|
||||||
station.setZip(data.getZip());
|
station.setZip(data.getZip());
|
||||||
station.setCity(data.getCity());
|
station.setCity(data.getCity());
|
||||||
|
station.setTasks(data.getTasks() != null ? new ArrayList<>(data.getTasks()) : new ArrayList<>());
|
||||||
deliveryStationsSaveAddress.set(idx, data.isSaveAddress());
|
deliveryStationsSaveAddress.set(idx, data.isSaveAddress());
|
||||||
|
|
||||||
// Store tasks for this delivery station
|
// Store tasks for this delivery station
|
||||||
@@ -829,7 +1022,10 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
|
|
||||||
// Update tile preview
|
// Update tile preview
|
||||||
tile.updatePreview(data.getCompany(), data.getFirstName(), data.getLastName(), data.getStreet(),
|
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());
|
tile.setAddressValidated(data.isAddressValidatedByGoogle());
|
||||||
|
|
||||||
resetRouteInformation();
|
resetRouteInformation();
|
||||||
@@ -863,6 +1059,9 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
currentData.setZip(station.getZip());
|
currentData.setZip(station.getZip());
|
||||||
currentData.setCity(station.getCity());
|
currentData.setCity(station.getCity());
|
||||||
currentData.setSaveAddress(deliveryStationsSaveAddress.get(actualIndex));
|
currentData.setSaveAddress(deliveryStationsSaveAddress.get(actualIndex));
|
||||||
|
if (actualIndex < deliveryStationsValidatedByGoogle.size()) {
|
||||||
|
currentData.setAddressValidatedByGoogle(deliveryStationsValidatedByGoogle.get(actualIndex));
|
||||||
|
}
|
||||||
// Load existing tasks for this station
|
// Load existing tasks for this station
|
||||||
List<BaseTask> stationTasks = deliveryStationTasksState.get(actualIndex);
|
List<BaseTask> stationTasks = deliveryStationTasksState.get(actualIndex);
|
||||||
if (stationTasks != null) {
|
if (stationTasks != null) {
|
||||||
@@ -1369,6 +1568,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
for (int i = 0; i < deliveryStationsState.size(); i++) {
|
for (int i = 0; i < deliveryStationsState.size(); i++) {
|
||||||
DeliveryStation station = deliveryStationsState.get(i);
|
DeliveryStation station = deliveryStationsState.get(i);
|
||||||
station.setStationOrder(i);
|
station.setStationOrder(i);
|
||||||
|
station.setTasks(new ArrayList<>(deliveryStationTasksState.getOrDefault(i, List.of())));
|
||||||
stations.add(station);
|
stations.add(station);
|
||||||
}
|
}
|
||||||
job.setDeliveryStations(stations);
|
job.setDeliveryStations(stations);
|
||||||
@@ -1697,6 +1897,212 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
return null;
|
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
|
* Registriert ValueChangeListener für alle Adressfelder, um bei Änderungen die
|
||||||
* Streckeninformationen zurückzusetzen.
|
* Streckeninformationen zurückzusetzen.
|
||||||
@@ -1781,6 +2187,12 @@ public class AddJobView extends Main implements HasDynamicTitle {
|
|||||||
if (routeInfoBox != null) {
|
if (routeInfoBox != null) {
|
||||||
routeInfoBox.setVisible(false);
|
routeInfoBox.setVisible(false);
|
||||||
}
|
}
|
||||||
|
if (routeDistanceLabel != null) {
|
||||||
|
routeDistanceLabel.setText("-");
|
||||||
|
}
|
||||||
|
if (routeDurationLabel != null) {
|
||||||
|
routeDurationLabel.setText("-");
|
||||||
|
}
|
||||||
if (manualRouteInputBox != null) {
|
if (manualRouteInputBox != null) {
|
||||||
manualRouteInputBox.setVisible(true);
|
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.BarcodeTask;
|
||||||
import de.assecutor.votianlt.model.task.CommentTask;
|
import de.assecutor.votianlt.model.task.CommentTask;
|
||||||
import de.assecutor.votianlt.model.AppUser;
|
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 de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import de.assecutor.votianlt.repository.CargoItemRepository;
|
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) {
|
private void render(Job job, List<CargoItem> cargoItems, List<BaseTask> tasks) {
|
||||||
content.removeAll();
|
content.removeAll();
|
||||||
|
|
||||||
// Kopfzeile: Abholung/Lieferung
|
content.add(createStationTilesSection(job, tasks));
|
||||||
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);
|
|
||||||
|
|
||||||
// Fracht und weitere Infos
|
// Fracht und weitere Infos
|
||||||
HorizontalLayout midRow = new HorizontalLayout();
|
HorizontalLayout midRow = new HorizontalLayout();
|
||||||
@@ -392,6 +306,179 @@ public class JobSummaryView extends Main implements HasUrlParameter<String>, Has
|
|||||||
return box;
|
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) {
|
private String formatLocalDate(java.time.LocalDate date) {
|
||||||
try {
|
try {
|
||||||
return DateTimeFormatUtil.formatDate(date);
|
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",
|
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)");
|
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
|
// Task status/description
|
||||||
Span taskDescription = new Span(getTaskDescription(task));
|
Span taskDescription = new Span(getTaskDescription(task));
|
||||||
taskDescription.getStyle().set("font-size", "var(--lumo-font-size-s)")
|
taskDescription.getStyle().set("font-size", "var(--lumo-font-size-s)")
|
||||||
.set("color", "var(--lumo-secondary-text-color)").set("margin-top", "var(--lumo-space-xs)");
|
.set("color", "var(--lumo-secondary-text-color)").set("margin-top", "var(--lumo-space-xs)");
|
||||||
|
|
||||||
taskContent.add(taskName, taskDescription);
|
taskContent.add(headerRow, taskDescription);
|
||||||
|
|
||||||
// Status indicator
|
// Status indicator
|
||||||
Div statusIndicator = new Div();
|
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) {
|
private String getTaskDescription(BaseTask task) {
|
||||||
if (task.isCompleted()) {
|
if (task.isCompleted()) {
|
||||||
return "Abgeschlossen"
|
return "Erledigt"
|
||||||
+ (task.getCompletedAt() != null ? " am " + formatLocalDate(task.getCompletedAt().toLocalDate())
|
+ (task.getCompletedAt() != null ? " am " + formatLocalDate(task.getCompletedAt().toLocalDate())
|
||||||
: "");
|
: "");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user