Erweiterungen
This commit is contained in:
@@ -4,6 +4,9 @@ public class Station {
|
|||||||
private String name;
|
private String name;
|
||||||
private String address;
|
private String address;
|
||||||
private StationAction action;
|
private StationAction action;
|
||||||
|
private Boolean addressValidated;
|
||||||
|
private Boolean addressValid;
|
||||||
|
private String validatedAddress;
|
||||||
|
|
||||||
public Station() {
|
public Station() {
|
||||||
}
|
}
|
||||||
@@ -37,4 +40,40 @@ public class Station {
|
|||||||
public void setAction(StationAction action) {
|
public void setAction(StationAction action) {
|
||||||
this.action = action;
|
this.action = action;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getAddressValidated() {
|
||||||
|
return addressValidated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAddressValidated(Boolean addressValidated) {
|
||||||
|
this.addressValidated = addressValidated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getAddressValid() {
|
||||||
|
return addressValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAddressValid(Boolean addressValid) {
|
||||||
|
this.addressValid = addressValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValidatedAddress() {
|
||||||
|
return validatedAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValidatedAddress(String validatedAddress) {
|
||||||
|
this.validatedAddress = validatedAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValidationPending() {
|
||||||
|
return addressValidated == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAddressValidAndChecked() {
|
||||||
|
return Boolean.TRUE.equals(addressValidated) && Boolean.TRUE.equals(addressValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAddressInvalidAndChecked() {
|
||||||
|
return Boolean.TRUE.equals(addressValidated) && Boolean.FALSE.equals(addressValid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package de.assecutor.aimailassistant.mail.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service für Google Geocoding API zur Adressvalidierung.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class GoogleGeocodingService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GoogleGeocodingService.class);
|
||||||
|
|
||||||
|
private final WebClient webClient;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final String apiKey;
|
||||||
|
|
||||||
|
public GoogleGeocodingService(
|
||||||
|
@Value("${google.maps.api.key:}") String apiKey,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
|
this.webClient = WebClient.builder()
|
||||||
|
.baseUrl("https://maps.googleapis.com")
|
||||||
|
.build();
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ergebnis einer Adressvalidierung.
|
||||||
|
*/
|
||||||
|
public record ValidationResult(
|
||||||
|
boolean valid,
|
||||||
|
String formattedAddress,
|
||||||
|
String streetNumber,
|
||||||
|
String street,
|
||||||
|
String postalCode,
|
||||||
|
String city,
|
||||||
|
String country,
|
||||||
|
Double latitude,
|
||||||
|
Double longitude,
|
||||||
|
String errorMessage
|
||||||
|
) {
|
||||||
|
public static ValidationResult error(String message) {
|
||||||
|
return new ValidationResult(false, null, null, null, null, null, null, null, null, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ValidationResult success(String formattedAddress, String streetNumber, String street,
|
||||||
|
String postalCode, String city, String country,
|
||||||
|
Double latitude, Double longitude) {
|
||||||
|
return new ValidationResult(true, formattedAddress, streetNumber, street, postalCode, city, country,
|
||||||
|
latitude, longitude, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validiert eine Adresse über die Google Geocoding API.
|
||||||
|
*
|
||||||
|
* @param address Die zu validierende Adresse
|
||||||
|
* @return ValidationResult mit der formatierten Adresse oder Fehlermeldung
|
||||||
|
*/
|
||||||
|
public ValidationResult validateAddress(String address) {
|
||||||
|
if (address == null || address.isBlank()) {
|
||||||
|
log.warn("validateAddress called with empty address");
|
||||||
|
return ValidationResult.error("Keine Adresse angegeben");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey == null || apiKey.isBlank()) {
|
||||||
|
log.warn("Google Maps API-Key not configured, skipping validation");
|
||||||
|
return ValidationResult.error("Google Maps API-Key nicht konfiguriert");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String encodedAddress = URLEncoder.encode(address, StandardCharsets.UTF_8);
|
||||||
|
String uri = "/maps/api/geocode/json?address=" + encodedAddress +
|
||||||
|
"®ion=de&language=de&key=" + apiKey;
|
||||||
|
|
||||||
|
log.info("Validating address: {}", address);
|
||||||
|
String response = webClient.get()
|
||||||
|
.uri(uri)
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono(String.class)
|
||||||
|
.timeout(Duration.ofSeconds(10))
|
||||||
|
.block();
|
||||||
|
|
||||||
|
log.debug("Geocoding response: {}", response);
|
||||||
|
return parseGeocodingResponse(response);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error validating address: {}", address, e);
|
||||||
|
return ValidationResult.error("Fehler bei der Adressvalidierung: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValidationResult parseGeocodingResponse(String response) {
|
||||||
|
try {
|
||||||
|
JsonNode root = objectMapper.readTree(response);
|
||||||
|
String status = root.path("status").asText();
|
||||||
|
|
||||||
|
if (!"OK".equals(status)) {
|
||||||
|
String errorMessage = switch (status) {
|
||||||
|
case "ZERO_RESULTS" -> "Keine Ergebnisse für diese Adresse gefunden";
|
||||||
|
case "OVER_DAILY_LIMIT", "OVER_QUERY_LIMIT" -> "API-Limit erreicht";
|
||||||
|
case "REQUEST_DENIED" -> "API-Zugriff verweigert";
|
||||||
|
case "INVALID_REQUEST" -> "Ungültige Anfrage";
|
||||||
|
default -> "Unbekannter Fehler: " + status;
|
||||||
|
};
|
||||||
|
return ValidationResult.error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode results = root.path("results");
|
||||||
|
if (!results.isArray() || results.isEmpty()) {
|
||||||
|
return ValidationResult.error("Keine Ergebnisse gefunden");
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode firstResult = results.get(0);
|
||||||
|
String formattedAddress = firstResult.path("formatted_address").asText();
|
||||||
|
|
||||||
|
// Koordinaten extrahieren
|
||||||
|
JsonNode location = firstResult.path("geometry").path("location");
|
||||||
|
Double lat = location.has("lat") ? location.path("lat").asDouble() : null;
|
||||||
|
Double lng = location.has("lng") ? location.path("lng").asDouble() : null;
|
||||||
|
|
||||||
|
// Adresskomponenten extrahieren
|
||||||
|
String streetNumber = null;
|
||||||
|
String street = null;
|
||||||
|
String postalCode = null;
|
||||||
|
String city = null;
|
||||||
|
String country = null;
|
||||||
|
|
||||||
|
JsonNode components = firstResult.path("address_components");
|
||||||
|
if (components.isArray()) {
|
||||||
|
for (JsonNode component : components) {
|
||||||
|
JsonNode types = component.path("types");
|
||||||
|
String longName = component.path("long_name").asText();
|
||||||
|
|
||||||
|
for (JsonNode type : types) {
|
||||||
|
String typeStr = type.asText();
|
||||||
|
switch (typeStr) {
|
||||||
|
case "street_number" -> streetNumber = longName;
|
||||||
|
case "route" -> street = longName;
|
||||||
|
case "postal_code" -> postalCode = longName;
|
||||||
|
case "locality" -> city = longName;
|
||||||
|
case "country" -> country = longName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Address validated successfully: {}", formattedAddress);
|
||||||
|
return ValidationResult.success(formattedAddress, streetNumber, street, postalCode, city, country, lat, lng);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error parsing geocoding response", e);
|
||||||
|
return ValidationResult.error("Fehler beim Parsen der Antwort: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -315,4 +315,94 @@ public class LlmService {
|
|||||||
log.info("Reprocessing email: {}", email.getSubject());
|
log.info("Reprocessing email: {}", email.getSubject());
|
||||||
return summarizeEmail(email);
|
return summarizeEmail(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrahiert eine Adresse aus markiertem Text mittels AI.
|
||||||
|
* @param selectedText Der vom Benutzer markierte Text
|
||||||
|
* @return Eine Station mit extrahierter Adresse, oder null wenn keine Adresse erkannt wurde
|
||||||
|
*/
|
||||||
|
public Station extractAddressFromText(String selectedText) {
|
||||||
|
if (selectedText == null || selectedText.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String systemPrompt = """
|
||||||
|
Du bist ein Assistent für die Extraktion von Adressen aus Text.
|
||||||
|
Deine Aufgabe ist es, aus dem gegebenen Text eine Adresse zu erkennen und zu strukturieren.
|
||||||
|
|
||||||
|
Antworte IMMER im folgenden JSON-Format:
|
||||||
|
{
|
||||||
|
"found": true oder false,
|
||||||
|
"name": "Name der Person/Firma (falls erkennbar)",
|
||||||
|
"address": "Vollständige Adresse (Straße, Hausnummer, PLZ, Ort)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
- Wenn eine Adresse erkennbar ist, setze "found": true
|
||||||
|
- Extrahiere die Adresse so vollständig wie möglich
|
||||||
|
- Wenn ein Firmenname oder Personenname zur Adresse gehört, extrahiere diesen als "name"
|
||||||
|
- Wenn keine Adresse erkennbar ist, setze "found": false und leere Strings für name/address
|
||||||
|
- Die Adresse sollte im deutschen Format sein (Straße Hausnummer, PLZ Ort)
|
||||||
|
""";
|
||||||
|
|
||||||
|
String userPrompt = String.format("""
|
||||||
|
Analysiere den folgenden markierten Text und extrahiere die Adresse:
|
||||||
|
|
||||||
|
%s
|
||||||
|
""", selectedText);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, Object> request = Map.of(
|
||||||
|
"model", model,
|
||||||
|
"messages", List.of(
|
||||||
|
Map.of("role", "system", "content", systemPrompt),
|
||||||
|
Map.of("role", "user", "content", userPrompt)
|
||||||
|
),
|
||||||
|
"temperature", 0.1,
|
||||||
|
"max_tokens", 500
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info("Extracting address from selected text...");
|
||||||
|
String response = webClient.post()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.bodyValue(request)
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono(String.class)
|
||||||
|
.block();
|
||||||
|
|
||||||
|
log.debug("Address extraction response: {}", response);
|
||||||
|
return parseAddressResponse(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error extracting address from text", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Station parseAddressResponse(String response) {
|
||||||
|
try {
|
||||||
|
JsonNode root = objectMapper.readTree(response);
|
||||||
|
String content = root.path("choices").get(0).path("message").path("content").asText();
|
||||||
|
String jsonContent = extractJson(content);
|
||||||
|
JsonNode addressNode = objectMapper.readTree(jsonContent);
|
||||||
|
|
||||||
|
boolean found = addressNode.path("found").asBoolean(false);
|
||||||
|
if (!found) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Station station = new Station();
|
||||||
|
station.setName(getTextOrNull(addressNode, "name"));
|
||||||
|
station.setAddress(getTextOrNull(addressNode, "address"));
|
||||||
|
station.setAction(StationAction.PICKUP); // Default action
|
||||||
|
|
||||||
|
// Nur zurückgeben wenn mindestens eine Adresse gefunden wurde
|
||||||
|
if (station.getAddress() != null && !station.getAddress().isBlank()) {
|
||||||
|
return station;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("Error parsing address response", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package de.assecutor.aimailassistant.mail.ui;
|
||||||
|
|
||||||
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
import com.vaadin.flow.component.dialog.Dialog;
|
||||||
|
import com.vaadin.flow.component.html.Div;
|
||||||
|
import com.vaadin.flow.component.html.Span;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
|
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog zur Anzeige einer einzelnen Adresse auf einer Google Maps Karte.
|
||||||
|
*/
|
||||||
|
public class AddressMapDialog extends Dialog {
|
||||||
|
|
||||||
|
private final String address;
|
||||||
|
private final String name;
|
||||||
|
private final String apiKey;
|
||||||
|
|
||||||
|
public AddressMapDialog(String name, String address, String apiKey) {
|
||||||
|
this.name = name;
|
||||||
|
this.address = address;
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
|
||||||
|
setHeaderTitle("Adresse auf Karte");
|
||||||
|
setWidth("700px");
|
||||||
|
setHeight("600px");
|
||||||
|
setCloseOnOutsideClick(true);
|
||||||
|
|
||||||
|
add(createContent());
|
||||||
|
createFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private VerticalLayout createContent() {
|
||||||
|
VerticalLayout layout = new VerticalLayout();
|
||||||
|
layout.setSizeFull();
|
||||||
|
layout.setPadding(false);
|
||||||
|
layout.setSpacing(true);
|
||||||
|
|
||||||
|
// Info section
|
||||||
|
HorizontalLayout infoSection = createInfoSection();
|
||||||
|
layout.add(infoSection);
|
||||||
|
|
||||||
|
// Map container
|
||||||
|
Div mapContainer = createMapContainer();
|
||||||
|
layout.add(mapContainer);
|
||||||
|
layout.setFlexGrow(1, mapContainer);
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HorizontalLayout createInfoSection() {
|
||||||
|
HorizontalLayout layout = new HorizontalLayout();
|
||||||
|
layout.setWidthFull();
|
||||||
|
layout.setSpacing(true);
|
||||||
|
layout.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||||
|
|
||||||
|
// Address info box
|
||||||
|
Div addressBox = new Div();
|
||||||
|
addressBox.addClassNames(
|
||||||
|
LumoUtility.Background.CONTRAST_5,
|
||||||
|
LumoUtility.BorderRadius.MEDIUM,
|
||||||
|
LumoUtility.Padding.MEDIUM
|
||||||
|
);
|
||||||
|
addressBox.setWidthFull();
|
||||||
|
|
||||||
|
VerticalLayout addressContent = new VerticalLayout();
|
||||||
|
addressContent.setPadding(false);
|
||||||
|
addressContent.setSpacing(false);
|
||||||
|
|
||||||
|
if (name != null && !name.isBlank()) {
|
||||||
|
Span nameSpan = new Span(name);
|
||||||
|
nameSpan.addClassNames(LumoUtility.FontWeight.BOLD, LumoUtility.FontSize.MEDIUM);
|
||||||
|
addressContent.add(nameSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span addressSpan = new Span(address != null ? address : "Keine Adresse");
|
||||||
|
addressSpan.addClassNames(LumoUtility.TextColor.SECONDARY);
|
||||||
|
addressContent.add(addressSpan);
|
||||||
|
|
||||||
|
addressBox.add(addressContent);
|
||||||
|
layout.add(addressBox);
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Div createMapContainer() {
|
||||||
|
Div mapDiv = new Div();
|
||||||
|
mapDiv.setId("address-map-" + System.currentTimeMillis());
|
||||||
|
mapDiv.setSizeFull();
|
||||||
|
mapDiv.getStyle()
|
||||||
|
.set("min-height", "400px")
|
||||||
|
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||||
|
.set("overflow", "hidden");
|
||||||
|
|
||||||
|
String mapId = mapDiv.getId().orElse("address-map");
|
||||||
|
String escapedAddress = escapeJs(address != null ? address : "");
|
||||||
|
String escapedName = escapeJs(name != null ? name : "");
|
||||||
|
|
||||||
|
// JavaScript to initialize Google Maps with a single marker
|
||||||
|
String initScript = """
|
||||||
|
(function() {
|
||||||
|
const mapDiv = document.getElementById('%s');
|
||||||
|
if (!mapDiv) return;
|
||||||
|
|
||||||
|
// Load Google Maps script if not already loaded
|
||||||
|
if (!window.google || !window.google.maps) {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://maps.googleapis.com/maps/api/js?key=%s&libraries=places';
|
||||||
|
script.async = true;
|
||||||
|
script.defer = true;
|
||||||
|
script.onload = function() {
|
||||||
|
initMap();
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
} else {
|
||||||
|
initMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMap() {
|
||||||
|
const address = "%s";
|
||||||
|
const name = "%s";
|
||||||
|
|
||||||
|
if (!address) {
|
||||||
|
mapDiv.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%%;color:var(--lumo-secondary-text-color)">Keine Adresse angegeben</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = new google.maps.Map(mapDiv, {
|
||||||
|
zoom: 15,
|
||||||
|
center: { lat: 51.1657, lng: 10.4515 }, // Germany center
|
||||||
|
mapTypeControl: true,
|
||||||
|
streetViewControl: true,
|
||||||
|
fullscreenControl: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const geocoder = new google.maps.Geocoder();
|
||||||
|
geocoder.geocode({ address: address, region: 'de' }, function(results, status) {
|
||||||
|
if (status === 'OK' && results[0]) {
|
||||||
|
const location = results[0].geometry.location;
|
||||||
|
map.setCenter(location);
|
||||||
|
|
||||||
|
const marker = new google.maps.Marker({
|
||||||
|
map: map,
|
||||||
|
position: location,
|
||||||
|
title: name || address,
|
||||||
|
animation: google.maps.Animation.DROP
|
||||||
|
});
|
||||||
|
|
||||||
|
const infoWindow = new google.maps.InfoWindow({
|
||||||
|
content: '<div style="padding:8px;"><strong>' + (name || 'Adresse') + '</strong><br>' + results[0].formatted_address + '</div>'
|
||||||
|
});
|
||||||
|
|
||||||
|
marker.addListener('click', function() {
|
||||||
|
infoWindow.open(map, marker);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-open info window
|
||||||
|
infoWindow.open(map, marker);
|
||||||
|
} else {
|
||||||
|
console.error('Geocoding failed:', status);
|
||||||
|
mapDiv.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%%;color:var(--lumo-error-text-color)">Adresse konnte nicht gefunden werden: ' + status + '</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
""".formatted(mapId, apiKey, escapedAddress, escapedName);
|
||||||
|
|
||||||
|
// Execute script after attach
|
||||||
|
mapDiv.getElement().executeJs(initScript);
|
||||||
|
|
||||||
|
return mapDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escapeJs(String str) {
|
||||||
|
if (str == null) return "";
|
||||||
|
return str.replace("\\", "\\\\")
|
||||||
|
.replace("\"", "\\\"")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createFooter() {
|
||||||
|
Button closeButton = new Button("Schließen", e -> close());
|
||||||
|
getFooter().add(closeButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import com.vaadin.flow.theme.lumo.LumoUtility;
|
|||||||
import de.assecutor.aimailassistant.mail.domain.OrderEmail;
|
import de.assecutor.aimailassistant.mail.domain.OrderEmail;
|
||||||
import de.assecutor.aimailassistant.mail.domain.OrderEmailRepository;
|
import de.assecutor.aimailassistant.mail.domain.OrderEmailRepository;
|
||||||
import de.assecutor.aimailassistant.mail.event.EmailBroadcaster;
|
import de.assecutor.aimailassistant.mail.event.EmailBroadcaster;
|
||||||
|
import de.assecutor.aimailassistant.mail.service.GoogleGeocodingService;
|
||||||
import de.assecutor.aimailassistant.mail.service.ImapEmailService;
|
import de.assecutor.aimailassistant.mail.service.ImapEmailService;
|
||||||
import de.assecutor.aimailassistant.mail.service.LlmService;
|
import de.assecutor.aimailassistant.mail.service.LlmService;
|
||||||
import de.assecutor.aimailassistant.mail.service.SmtpEmailService;
|
import de.assecutor.aimailassistant.mail.service.SmtpEmailService;
|
||||||
@@ -44,6 +45,7 @@ public class MainView extends VerticalLayout {
|
|||||||
private final OrderEmailRepository orderEmailRepository;
|
private final OrderEmailRepository orderEmailRepository;
|
||||||
private final LlmService llmService;
|
private final LlmService llmService;
|
||||||
private final SmtpEmailService smtpEmailService;
|
private final SmtpEmailService smtpEmailService;
|
||||||
|
private final GoogleGeocodingService geocodingService;
|
||||||
private final ImapEmailService imapEmailService;
|
private final ImapEmailService imapEmailService;
|
||||||
private final String googleMapsApiKey;
|
private final String googleMapsApiKey;
|
||||||
|
|
||||||
@@ -54,11 +56,13 @@ public class MainView extends VerticalLayout {
|
|||||||
OrderEmailRepository orderEmailRepository,
|
OrderEmailRepository orderEmailRepository,
|
||||||
LlmService llmService,
|
LlmService llmService,
|
||||||
SmtpEmailService smtpEmailService,
|
SmtpEmailService smtpEmailService,
|
||||||
|
GoogleGeocodingService geocodingService,
|
||||||
ImapEmailService imapEmailService,
|
ImapEmailService imapEmailService,
|
||||||
@Value("${google.maps.api.key}") String googleMapsApiKey) {
|
@Value("${google.maps.api.key}") String googleMapsApiKey) {
|
||||||
this.orderEmailRepository = orderEmailRepository;
|
this.orderEmailRepository = orderEmailRepository;
|
||||||
this.llmService = llmService;
|
this.llmService = llmService;
|
||||||
this.smtpEmailService = smtpEmailService;
|
this.smtpEmailService = smtpEmailService;
|
||||||
|
this.geocodingService = geocodingService;
|
||||||
this.imapEmailService = imapEmailService;
|
this.imapEmailService = imapEmailService;
|
||||||
this.googleMapsApiKey = googleMapsApiKey;
|
this.googleMapsApiKey = googleMapsApiKey;
|
||||||
|
|
||||||
@@ -248,6 +252,7 @@ public class MainView extends VerticalLayout {
|
|||||||
email,
|
email,
|
||||||
llmService,
|
llmService,
|
||||||
smtpEmailService,
|
smtpEmailService,
|
||||||
|
geocodingService,
|
||||||
googleMapsApiKey,
|
googleMapsApiKey,
|
||||||
this::onEmailProcessed,
|
this::onEmailProcessed,
|
||||||
this::onEmailDeleted
|
this::onEmailDeleted
|
||||||
|
|||||||
@@ -31,20 +31,26 @@ import de.assecutor.aimailassistant.mail.domain.OrderEmail;
|
|||||||
import de.assecutor.aimailassistant.mail.domain.OrderSummary;
|
import de.assecutor.aimailassistant.mail.domain.OrderSummary;
|
||||||
import de.assecutor.aimailassistant.mail.domain.Station;
|
import de.assecutor.aimailassistant.mail.domain.Station;
|
||||||
import de.assecutor.aimailassistant.mail.domain.StationAction;
|
import de.assecutor.aimailassistant.mail.domain.StationAction;
|
||||||
|
import de.assecutor.aimailassistant.mail.service.GoogleGeocodingService;
|
||||||
import de.assecutor.aimailassistant.mail.service.LlmService;
|
import de.assecutor.aimailassistant.mail.service.LlmService;
|
||||||
import de.assecutor.aimailassistant.mail.service.SmtpEmailService;
|
import de.assecutor.aimailassistant.mail.service.SmtpEmailService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class OrderDetailDialog extends Dialog {
|
public class OrderDetailDialog extends Dialog {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(OrderDetailDialog.class);
|
||||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
|
||||||
|
|
||||||
private final OrderEmail orderEmail;
|
private final OrderEmail orderEmail;
|
||||||
private OrderSummary summary;
|
private OrderSummary summary;
|
||||||
private final LlmService llmService;
|
private final LlmService llmService;
|
||||||
private final SmtpEmailService smtpEmailService;
|
private final SmtpEmailService smtpEmailService;
|
||||||
|
private final GoogleGeocodingService geocodingService;
|
||||||
private final Consumer<OrderEmail> onProcessed;
|
private final Consumer<OrderEmail> onProcessed;
|
||||||
private final Consumer<OrderEmail> onDelete;
|
private final Consumer<OrderEmail> onDelete;
|
||||||
private final String googleMapsApiKey;
|
private final String googleMapsApiKey;
|
||||||
@@ -54,11 +60,13 @@ public class OrderDetailDialog extends Dialog {
|
|||||||
private EmailField recipientField;
|
private EmailField recipientField;
|
||||||
private HorizontalLayout contentLayout;
|
private HorizontalLayout contentLayout;
|
||||||
private Binder<OrderSummary> binder;
|
private Binder<OrderSummary> binder;
|
||||||
|
private VerticalLayout stationsSection;
|
||||||
|
|
||||||
public OrderDetailDialog(
|
public OrderDetailDialog(
|
||||||
OrderEmail orderEmail,
|
OrderEmail orderEmail,
|
||||||
LlmService llmService,
|
LlmService llmService,
|
||||||
SmtpEmailService smtpEmailService,
|
SmtpEmailService smtpEmailService,
|
||||||
|
GoogleGeocodingService geocodingService,
|
||||||
String googleMapsApiKey,
|
String googleMapsApiKey,
|
||||||
Consumer<OrderEmail> onProcessed,
|
Consumer<OrderEmail> onProcessed,
|
||||||
Consumer<OrderEmail> onDelete) {
|
Consumer<OrderEmail> onDelete) {
|
||||||
@@ -66,6 +74,7 @@ public class OrderDetailDialog extends Dialog {
|
|||||||
this.llmService = llmService;
|
this.llmService = llmService;
|
||||||
this.summary = llmService.deserializeSummary(orderEmail.getSummaryJson());
|
this.summary = llmService.deserializeSummary(orderEmail.getSummaryJson());
|
||||||
this.smtpEmailService = smtpEmailService;
|
this.smtpEmailService = smtpEmailService;
|
||||||
|
this.geocodingService = geocodingService;
|
||||||
this.googleMapsApiKey = googleMapsApiKey;
|
this.googleMapsApiKey = googleMapsApiKey;
|
||||||
this.onProcessed = onProcessed;
|
this.onProcessed = onProcessed;
|
||||||
this.onDelete = onDelete;
|
this.onDelete = onDelete;
|
||||||
@@ -144,6 +153,17 @@ public class OrderDetailDialog extends Dialog {
|
|||||||
title.addClassName(LumoUtility.Margin.Bottom.SMALL);
|
title.addClassName(LumoUtility.Margin.Bottom.SMALL);
|
||||||
title.addClassName(LumoUtility.Margin.Top.NONE);
|
title.addClassName(LumoUtility.Margin.Top.NONE);
|
||||||
|
|
||||||
|
// Hinweis für Textauswahl (nur im Bearbeitungsmodus)
|
||||||
|
Span selectionHint = new Span();
|
||||||
|
if (!readOnly) {
|
||||||
|
selectionHint.setText("Tipp: Markieren Sie Text, um eine Adresse zu extrahieren");
|
||||||
|
selectionHint.addClassNames(
|
||||||
|
LumoUtility.TextColor.SECONDARY,
|
||||||
|
LumoUtility.FontSize.XSMALL,
|
||||||
|
LumoUtility.Margin.Bottom.SMALL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Div contentBox = new Div();
|
Div contentBox = new Div();
|
||||||
contentBox.addClassNames(
|
contentBox.addClassNames(
|
||||||
LumoUtility.Background.CONTRAST_5,
|
LumoUtility.Background.CONTRAST_5,
|
||||||
@@ -165,20 +185,243 @@ public class OrderDetailDialog extends Dialog {
|
|||||||
.set("margin", "0")
|
.set("margin", "0")
|
||||||
.set("max-width", "100%")
|
.set("max-width", "100%")
|
||||||
.set("font-family", "var(--lumo-font-family)")
|
.set("font-family", "var(--lumo-font-family)")
|
||||||
.set("font-size", "var(--lumo-font-size-s)");
|
.set("font-size", "var(--lumo-font-size-s)")
|
||||||
|
.set("user-select", "text")
|
||||||
|
.set("-webkit-user-select", "text")
|
||||||
|
.set("cursor", "text");
|
||||||
|
|
||||||
contentBox.add(emailContent);
|
contentBox.add(emailContent);
|
||||||
|
|
||||||
|
// Textauswahl-Erkennung nur im Bearbeitungsmodus
|
||||||
|
if (!readOnly) {
|
||||||
|
setupTextSelectionListener(emailContent);
|
||||||
|
}
|
||||||
|
|
||||||
Scroller scroller = new Scroller(contentBox);
|
Scroller scroller = new Scroller(contentBox);
|
||||||
scroller.setSizeFull();
|
scroller.setSizeFull();
|
||||||
scroller.setScrollDirection(Scroller.ScrollDirection.VERTICAL);
|
scroller.setScrollDirection(Scroller.ScrollDirection.VERTICAL);
|
||||||
|
|
||||||
|
if (!readOnly) {
|
||||||
|
panel.add(title, selectionHint, scroller);
|
||||||
|
} else {
|
||||||
panel.add(title, scroller);
|
panel.add(title, scroller);
|
||||||
|
}
|
||||||
panel.setFlexGrow(1, scroller);
|
panel.setFlexGrow(1, scroller);
|
||||||
|
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupTextSelectionListener(Pre emailContent) {
|
||||||
|
emailContent.getElement().executeJs(
|
||||||
|
"const element = this;" +
|
||||||
|
"let selectionTimeout = null;" +
|
||||||
|
"element.addEventListener('mouseup', function(e) {" +
|
||||||
|
" if (selectionTimeout) clearTimeout(selectionTimeout);" +
|
||||||
|
" selectionTimeout = setTimeout(function() {" +
|
||||||
|
" const selection = window.getSelection();" +
|
||||||
|
" const selectedText = selection.toString().trim();" +
|
||||||
|
" if (selectedText.length > 10) {" +
|
||||||
|
" $0.$server.onTextSelected(selectedText);" +
|
||||||
|
" }" +
|
||||||
|
" }, 300);" +
|
||||||
|
"});",
|
||||||
|
getElement()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@com.vaadin.flow.component.ClientCallable
|
||||||
|
public void onTextSelected(String selectedText) {
|
||||||
|
if (selectedText == null || selectedText.isBlank() || readOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeige Lade-Benachrichtigung
|
||||||
|
Notification loadingNotification = Notification.show(
|
||||||
|
"Analysiere markierten Text...",
|
||||||
|
0,
|
||||||
|
Notification.Position.MIDDLE
|
||||||
|
);
|
||||||
|
|
||||||
|
// Führe die Adressextraktion asynchron aus
|
||||||
|
getUI().ifPresent(ui -> {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
Station extractedStation = llmService.extractAddressFromText(selectedText);
|
||||||
|
|
||||||
|
ui.access(() -> {
|
||||||
|
loadingNotification.close();
|
||||||
|
|
||||||
|
if (extractedStation != null) {
|
||||||
|
// Dialog zur Bestätigung und Bearbeitung der extrahierten Adresse
|
||||||
|
openAddStationDialog(extractedStation);
|
||||||
|
} else {
|
||||||
|
Notification.show(
|
||||||
|
"Keine Adresse im markierten Text erkannt",
|
||||||
|
3000,
|
||||||
|
Notification.Position.BOTTOM_START
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
ui.access(() -> {
|
||||||
|
loadingNotification.close();
|
||||||
|
Notification.show(
|
||||||
|
"Fehler bei der Adresserkennung: " + e.getMessage(),
|
||||||
|
5000,
|
||||||
|
Notification.Position.MIDDLE
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openAddStationDialog(Station extractedStation) {
|
||||||
|
Dialog addDialog = new Dialog();
|
||||||
|
addDialog.setHeaderTitle("Neue Station hinzufügen");
|
||||||
|
addDialog.setWidth("500px");
|
||||||
|
|
||||||
|
VerticalLayout dialogContent = new VerticalLayout();
|
||||||
|
dialogContent.setPadding(false);
|
||||||
|
dialogContent.setSpacing(true);
|
||||||
|
|
||||||
|
FormLayout formLayout = new FormLayout();
|
||||||
|
formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
|
||||||
|
|
||||||
|
TextField nameField = new TextField("Name");
|
||||||
|
nameField.setWidthFull();
|
||||||
|
nameField.setValue(extractedStation.getName() != null ? extractedStation.getName() : "");
|
||||||
|
|
||||||
|
TextArea addressField = new TextArea("Adresse");
|
||||||
|
addressField.setWidthFull();
|
||||||
|
addressField.setMinHeight("80px");
|
||||||
|
addressField.setValue(extractedStation.getAddress() != null ? extractedStation.getAddress() : "");
|
||||||
|
|
||||||
|
ComboBox<StationAction> actionComboBox = new ComboBox<>("Aktion");
|
||||||
|
actionComboBox.setItems(StationAction.values());
|
||||||
|
actionComboBox.setItemLabelGenerator(StationAction::getDisplayName);
|
||||||
|
actionComboBox.setValue(StationAction.PICKUP);
|
||||||
|
actionComboBox.setWidthFull();
|
||||||
|
|
||||||
|
formLayout.add(nameField, addressField, actionComboBox);
|
||||||
|
|
||||||
|
// Buttons für Google-Validierung und Kartenansicht
|
||||||
|
HorizontalLayout addressActions = new HorizontalLayout();
|
||||||
|
addressActions.setSpacing(true);
|
||||||
|
addressActions.setWidthFull();
|
||||||
|
|
||||||
|
Button validateButton = new Button("Adresse validieren", new Icon(VaadinIcon.CHECK_CIRCLE));
|
||||||
|
validateButton.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_TERTIARY);
|
||||||
|
validateButton.getElement().setAttribute("title", "Adresse über Google validieren und formatieren");
|
||||||
|
validateButton.addClickListener(e -> validateAddressWithGoogle(addressField));
|
||||||
|
|
||||||
|
Button showMapButton = new Button("Auf Karte anzeigen", new Icon(VaadinIcon.MAP_MARKER));
|
||||||
|
showMapButton.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_TERTIARY);
|
||||||
|
showMapButton.getElement().setAttribute("title", "Adresse auf Google Maps anzeigen");
|
||||||
|
showMapButton.addClickListener(e -> {
|
||||||
|
String address = addressField.getValue();
|
||||||
|
String name = nameField.getValue();
|
||||||
|
if (address == null || address.isBlank()) {
|
||||||
|
Notification.show("Bitte geben Sie eine Adresse ein", 3000, Notification.Position.BOTTOM_START);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AddressMapDialog mapDialog = new AddressMapDialog(name, address, googleMapsApiKey);
|
||||||
|
mapDialog.open();
|
||||||
|
});
|
||||||
|
|
||||||
|
addressActions.add(validateButton, showMapButton);
|
||||||
|
|
||||||
|
dialogContent.add(formLayout, addressActions);
|
||||||
|
addDialog.add(dialogContent);
|
||||||
|
|
||||||
|
Button cancelButton = new Button("Abbrechen", e -> addDialog.close());
|
||||||
|
Button addButton = new Button("Hinzufügen", e -> {
|
||||||
|
Station newStation = new Station();
|
||||||
|
newStation.setName(nameField.getValue());
|
||||||
|
newStation.setAddress(addressField.getValue());
|
||||||
|
newStation.setAction(actionComboBox.getValue());
|
||||||
|
|
||||||
|
// Station zur Liste hinzufügen
|
||||||
|
if (summary.getStations() == null) {
|
||||||
|
summary.setStations(new ArrayList<>());
|
||||||
|
}
|
||||||
|
summary.getStations().add(newStation);
|
||||||
|
|
||||||
|
// Summary aktualisieren
|
||||||
|
orderEmail.setSummaryJson(llmService.serializeSummary(summary));
|
||||||
|
onProcessed.accept(orderEmail);
|
||||||
|
|
||||||
|
// UI komplett neu aufbauen, da möglicherweise die Stationen-Sektion noch nicht existierte
|
||||||
|
remove(contentLayout);
|
||||||
|
getFooter().removeAll();
|
||||||
|
contentLayout = createContent();
|
||||||
|
add(contentLayout);
|
||||||
|
createFooter();
|
||||||
|
|
||||||
|
addDialog.close();
|
||||||
|
Notification.show("Station hinzugefügt", 3000, Notification.Position.BOTTOM_START);
|
||||||
|
});
|
||||||
|
addButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
|
||||||
|
addDialog.getFooter().add(cancelButton, addButton);
|
||||||
|
addDialog.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateAddressWithGoogle(TextArea addressField) {
|
||||||
|
String address = addressField.getValue();
|
||||||
|
if (address == null || address.isBlank()) {
|
||||||
|
Notification.show("Bitte geben Sie eine Adresse ein", 3000, Notification.Position.BOTTOM_START);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification loadingNotification = Notification.show(
|
||||||
|
"Validiere Adresse...",
|
||||||
|
0,
|
||||||
|
Notification.Position.MIDDLE
|
||||||
|
);
|
||||||
|
|
||||||
|
getUI().ifPresent(ui -> {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
GoogleGeocodingService.ValidationResult result = geocodingService.validateAddress(address);
|
||||||
|
|
||||||
|
ui.access(() -> {
|
||||||
|
loadingNotification.close();
|
||||||
|
|
||||||
|
if (result.valid()) {
|
||||||
|
Notification validNotification = new Notification();
|
||||||
|
validNotification.addThemeVariants(
|
||||||
|
com.vaadin.flow.component.notification.NotificationVariant.LUMO_SUCCESS
|
||||||
|
);
|
||||||
|
validNotification.setText("Adresse gültig: " + result.formattedAddress());
|
||||||
|
validNotification.setDuration(5000);
|
||||||
|
validNotification.setPosition(Notification.Position.BOTTOM_START);
|
||||||
|
validNotification.open();
|
||||||
|
} else {
|
||||||
|
Notification invalidNotification = new Notification();
|
||||||
|
invalidNotification.addThemeVariants(
|
||||||
|
com.vaadin.flow.component.notification.NotificationVariant.LUMO_ERROR
|
||||||
|
);
|
||||||
|
invalidNotification.setText("Adresse nicht gefunden: " + result.errorMessage());
|
||||||
|
invalidNotification.setDuration(5000);
|
||||||
|
invalidNotification.setPosition(Notification.Position.MIDDLE);
|
||||||
|
invalidNotification.open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
ui.access(() -> {
|
||||||
|
loadingNotification.close();
|
||||||
|
Notification.show(
|
||||||
|
"Fehler bei der Validierung: " + e.getMessage(),
|
||||||
|
5000,
|
||||||
|
Notification.Position.MIDDLE
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private VerticalLayout createRightPanel() {
|
private VerticalLayout createRightPanel() {
|
||||||
VerticalLayout panel = new VerticalLayout();
|
VerticalLayout panel = new VerticalLayout();
|
||||||
panel.setPadding(false);
|
panel.setPadding(false);
|
||||||
@@ -411,7 +654,6 @@ public class OrderDetailDialog extends Dialog {
|
|||||||
|
|
||||||
Div stationCard = new Div();
|
Div stationCard = new Div();
|
||||||
stationCard.addClassNames(
|
stationCard.addClassNames(
|
||||||
LumoUtility.Background.CONTRAST_5,
|
|
||||||
LumoUtility.BorderRadius.MEDIUM,
|
LumoUtility.BorderRadius.MEDIUM,
|
||||||
LumoUtility.Padding.SMALL
|
LumoUtility.Padding.SMALL
|
||||||
);
|
);
|
||||||
@@ -419,6 +661,9 @@ public class OrderDetailDialog extends Dialog {
|
|||||||
.set("flex", "1")
|
.set("flex", "1")
|
||||||
.set("position", "relative");
|
.set("position", "relative");
|
||||||
|
|
||||||
|
// Hintergrundfarbe basierend auf Validierungsstatus
|
||||||
|
applyValidationBackground(stationCard, station);
|
||||||
|
|
||||||
HorizontalLayout stationContent = new HorizontalLayout();
|
HorizontalLayout stationContent = new HorizontalLayout();
|
||||||
stationContent.setAlignItems(FlexComponent.Alignment.CENTER);
|
stationContent.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||||
stationContent.setSpacing(true);
|
stationContent.setSpacing(true);
|
||||||
@@ -516,6 +761,109 @@ public class OrderDetailDialog extends Dialog {
|
|||||||
showTourButton.setWidthFull();
|
showTourButton.setWidthFull();
|
||||||
section.add(showTourButton);
|
section.add(showTourButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automatisch nicht validierte Stationen validieren (nach Attach)
|
||||||
|
section.addAttachListener(event -> validatePendingStations(section));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyValidationBackground(Div stationCard, Station station) {
|
||||||
|
if (station.isValidationPending()) {
|
||||||
|
// Noch nicht validiert - dezentes Gelb
|
||||||
|
stationCard.getStyle().set("background-color", "var(--lumo-warning-color-10pct)");
|
||||||
|
stationCard.getElement().setAttribute("title", "Adresse wird validiert...");
|
||||||
|
} else if (station.isAddressValidAndChecked()) {
|
||||||
|
// Validiert und gültig - dezentes Grün
|
||||||
|
stationCard.getStyle().set("background-color", "var(--lumo-success-color-10pct)");
|
||||||
|
String tooltip = "Adresse gültig";
|
||||||
|
if (station.getValidatedAddress() != null) {
|
||||||
|
tooltip += ": " + station.getValidatedAddress();
|
||||||
|
}
|
||||||
|
stationCard.getElement().setAttribute("title", tooltip);
|
||||||
|
} else if (station.isAddressInvalidAndChecked()) {
|
||||||
|
// Validiert aber ungültig - dezentes Rot
|
||||||
|
stationCard.getStyle().set("background-color", "var(--lumo-error-color-10pct)");
|
||||||
|
stationCard.getElement().setAttribute("title", "Adresse nicht gefunden");
|
||||||
|
} else {
|
||||||
|
// Standard-Hintergrund
|
||||||
|
stationCard.addClassName(LumoUtility.Background.CONTRAST_5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validatePendingStations(VerticalLayout stationsSection) {
|
||||||
|
var stations = summary.getStations();
|
||||||
|
if (stations == null || stations.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finde Stationen, die noch nicht validiert wurden
|
||||||
|
var pendingStations = stations.stream()
|
||||||
|
.filter(Station::isValidationPending)
|
||||||
|
.filter(s -> s.getAddress() != null && !s.getAddress().isBlank())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (pendingStations.isEmpty()) {
|
||||||
|
log.debug("No pending stations to validate");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Validating {} pending stations", pendingStations.size());
|
||||||
|
|
||||||
|
var uiOptional = getUI();
|
||||||
|
if (uiOptional.isEmpty()) {
|
||||||
|
log.warn("UI not available, cannot validate stations");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uiOptional.ifPresent(ui -> {
|
||||||
|
log.info("Starting validation thread...");
|
||||||
|
new Thread(() -> {
|
||||||
|
log.info("Validation thread started");
|
||||||
|
try {
|
||||||
|
for (Station station : pendingStations) {
|
||||||
|
try {
|
||||||
|
log.info("Validating address: {}", station.getAddress());
|
||||||
|
GoogleGeocodingService.ValidationResult result =
|
||||||
|
geocodingService.validateAddress(station.getAddress());
|
||||||
|
|
||||||
|
station.setAddressValidated(true);
|
||||||
|
station.setAddressValid(result.valid());
|
||||||
|
if (result.valid()) {
|
||||||
|
station.setValidatedAddress(result.formattedAddress());
|
||||||
|
log.info("Address valid: {}", result.formattedAddress());
|
||||||
|
} else {
|
||||||
|
log.info("Address invalid: {}", result.errorMessage());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error validating address: {}", station.getAddress(), e);
|
||||||
|
station.setAddressValidated(true);
|
||||||
|
station.setAddressValid(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("All stations validated, updating UI");
|
||||||
|
|
||||||
|
// Update UI - nur wenn noch attached
|
||||||
|
if (ui.isAttached()) {
|
||||||
|
ui.access(() -> {
|
||||||
|
try {
|
||||||
|
orderEmail.setSummaryJson(llmService.serializeSummary(summary));
|
||||||
|
onProcessed.accept(orderEmail);
|
||||||
|
// Section neu aufbauen - validatePendingStations wird erneut
|
||||||
|
// aufgerufen, findet aber keine pending stations mehr
|
||||||
|
refreshStationsSection(stationsSection);
|
||||||
|
log.debug("UI updated successfully");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error updating UI after validation", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.warn("UI not attached, skipping UI update");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error in validation thread", e);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reorderStations(int fromIndex, int toIndex, VerticalLayout section) {
|
private void reorderStations(int fromIndex, int toIndex, VerticalLayout section) {
|
||||||
@@ -541,7 +889,11 @@ public class OrderDetailDialog extends Dialog {
|
|||||||
private void openStationEditDialog(Station station, VerticalLayout stationsSection) {
|
private void openStationEditDialog(Station station, VerticalLayout stationsSection) {
|
||||||
Dialog editDialog = new Dialog();
|
Dialog editDialog = new Dialog();
|
||||||
editDialog.setHeaderTitle("Station bearbeiten");
|
editDialog.setHeaderTitle("Station bearbeiten");
|
||||||
editDialog.setWidth("400px");
|
editDialog.setWidth("500px");
|
||||||
|
|
||||||
|
VerticalLayout dialogContent = new VerticalLayout();
|
||||||
|
dialogContent.setPadding(false);
|
||||||
|
dialogContent.setSpacing(true);
|
||||||
|
|
||||||
FormLayout formLayout = new FormLayout();
|
FormLayout formLayout = new FormLayout();
|
||||||
formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
|
formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
|
||||||
@@ -562,14 +914,52 @@ public class OrderDetailDialog extends Dialog {
|
|||||||
actionComboBox.setWidthFull();
|
actionComboBox.setWidthFull();
|
||||||
|
|
||||||
formLayout.add(nameField, addressField, actionComboBox);
|
formLayout.add(nameField, addressField, actionComboBox);
|
||||||
editDialog.add(formLayout);
|
|
||||||
|
// Buttons für Google-Validierung und Kartenansicht
|
||||||
|
HorizontalLayout addressActions = new HorizontalLayout();
|
||||||
|
addressActions.setSpacing(true);
|
||||||
|
addressActions.setWidthFull();
|
||||||
|
|
||||||
|
Button validateButton = new Button("Adresse validieren", new Icon(VaadinIcon.CHECK_CIRCLE));
|
||||||
|
validateButton.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_TERTIARY);
|
||||||
|
validateButton.getElement().setAttribute("title", "Adresse über Google validieren");
|
||||||
|
validateButton.addClickListener(e -> validateAddressWithGoogle(addressField));
|
||||||
|
|
||||||
|
Button showMapButton = new Button("Auf Karte anzeigen", new Icon(VaadinIcon.MAP_MARKER));
|
||||||
|
showMapButton.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_TERTIARY);
|
||||||
|
showMapButton.getElement().setAttribute("title", "Adresse auf Google Maps anzeigen");
|
||||||
|
showMapButton.addClickListener(e -> {
|
||||||
|
String address = addressField.getValue();
|
||||||
|
String name = nameField.getValue();
|
||||||
|
if (address == null || address.isBlank()) {
|
||||||
|
Notification.show("Bitte geben Sie eine Adresse ein", 3000, Notification.Position.BOTTOM_START);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AddressMapDialog mapDialog = new AddressMapDialog(name, address, googleMapsApiKey);
|
||||||
|
mapDialog.open();
|
||||||
|
});
|
||||||
|
|
||||||
|
addressActions.add(validateButton, showMapButton);
|
||||||
|
|
||||||
|
dialogContent.add(formLayout, addressActions);
|
||||||
|
editDialog.add(dialogContent);
|
||||||
|
|
||||||
Button cancelButton = new Button("Abbrechen", e -> editDialog.close());
|
Button cancelButton = new Button("Abbrechen", e -> editDialog.close());
|
||||||
Button saveButton = new Button("Speichern", e -> {
|
Button saveButton = new Button("Speichern", e -> {
|
||||||
|
// Prüfen ob Adresse geändert wurde
|
||||||
|
boolean addressChanged = !addressField.getValue().equals(station.getAddress());
|
||||||
|
|
||||||
station.setName(nameField.getValue());
|
station.setName(nameField.getValue());
|
||||||
station.setAddress(addressField.getValue());
|
station.setAddress(addressField.getValue());
|
||||||
station.setAction(actionComboBox.getValue());
|
station.setAction(actionComboBox.getValue());
|
||||||
|
|
||||||
|
// Bei Adressänderung Validierung zurücksetzen
|
||||||
|
if (addressChanged) {
|
||||||
|
station.setAddressValidated(null);
|
||||||
|
station.setAddressValid(null);
|
||||||
|
station.setValidatedAddress(null);
|
||||||
|
}
|
||||||
|
|
||||||
// Update summary JSON
|
// Update summary JSON
|
||||||
orderEmail.setSummaryJson(llmService.serializeSummary(summary));
|
orderEmail.setSummaryJson(llmService.serializeSummary(summary));
|
||||||
onProcessed.accept(orderEmail);
|
onProcessed.accept(orderEmail);
|
||||||
|
|||||||
Reference in New Issue
Block a user