Erweiterungen
This commit is contained in:
@@ -4,6 +4,9 @@ public class Station {
|
||||
private String name;
|
||||
private String address;
|
||||
private StationAction action;
|
||||
private Boolean addressValidated;
|
||||
private Boolean addressValid;
|
||||
private String validatedAddress;
|
||||
|
||||
public Station() {
|
||||
}
|
||||
@@ -37,4 +40,40 @@ public class Station {
|
||||
public void setAction(StationAction 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());
|
||||
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.OrderEmailRepository;
|
||||
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.LlmService;
|
||||
import de.assecutor.aimailassistant.mail.service.SmtpEmailService;
|
||||
@@ -44,6 +45,7 @@ public class MainView extends VerticalLayout {
|
||||
private final OrderEmailRepository orderEmailRepository;
|
||||
private final LlmService llmService;
|
||||
private final SmtpEmailService smtpEmailService;
|
||||
private final GoogleGeocodingService geocodingService;
|
||||
private final ImapEmailService imapEmailService;
|
||||
private final String googleMapsApiKey;
|
||||
|
||||
@@ -54,11 +56,13 @@ public class MainView extends VerticalLayout {
|
||||
OrderEmailRepository orderEmailRepository,
|
||||
LlmService llmService,
|
||||
SmtpEmailService smtpEmailService,
|
||||
GoogleGeocodingService geocodingService,
|
||||
ImapEmailService imapEmailService,
|
||||
@Value("${google.maps.api.key}") String googleMapsApiKey) {
|
||||
this.orderEmailRepository = orderEmailRepository;
|
||||
this.llmService = llmService;
|
||||
this.smtpEmailService = smtpEmailService;
|
||||
this.geocodingService = geocodingService;
|
||||
this.imapEmailService = imapEmailService;
|
||||
this.googleMapsApiKey = googleMapsApiKey;
|
||||
|
||||
@@ -248,6 +252,7 @@ public class MainView extends VerticalLayout {
|
||||
email,
|
||||
llmService,
|
||||
smtpEmailService,
|
||||
geocodingService,
|
||||
googleMapsApiKey,
|
||||
this::onEmailProcessed,
|
||||
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.Station;
|
||||
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.SmtpEmailService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
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 final OrderEmail orderEmail;
|
||||
private OrderSummary summary;
|
||||
private final LlmService llmService;
|
||||
private final SmtpEmailService smtpEmailService;
|
||||
private final GoogleGeocodingService geocodingService;
|
||||
private final Consumer<OrderEmail> onProcessed;
|
||||
private final Consumer<OrderEmail> onDelete;
|
||||
private final String googleMapsApiKey;
|
||||
@@ -54,11 +60,13 @@ public class OrderDetailDialog extends Dialog {
|
||||
private EmailField recipientField;
|
||||
private HorizontalLayout contentLayout;
|
||||
private Binder<OrderSummary> binder;
|
||||
private VerticalLayout stationsSection;
|
||||
|
||||
public OrderDetailDialog(
|
||||
OrderEmail orderEmail,
|
||||
LlmService llmService,
|
||||
SmtpEmailService smtpEmailService,
|
||||
GoogleGeocodingService geocodingService,
|
||||
String googleMapsApiKey,
|
||||
Consumer<OrderEmail> onProcessed,
|
||||
Consumer<OrderEmail> onDelete) {
|
||||
@@ -66,6 +74,7 @@ public class OrderDetailDialog extends Dialog {
|
||||
this.llmService = llmService;
|
||||
this.summary = llmService.deserializeSummary(orderEmail.getSummaryJson());
|
||||
this.smtpEmailService = smtpEmailService;
|
||||
this.geocodingService = geocodingService;
|
||||
this.googleMapsApiKey = googleMapsApiKey;
|
||||
this.onProcessed = onProcessed;
|
||||
this.onDelete = onDelete;
|
||||
@@ -144,6 +153,17 @@ public class OrderDetailDialog extends Dialog {
|
||||
title.addClassName(LumoUtility.Margin.Bottom.SMALL);
|
||||
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();
|
||||
contentBox.addClassNames(
|
||||
LumoUtility.Background.CONTRAST_5,
|
||||
@@ -165,20 +185,243 @@ public class OrderDetailDialog extends Dialog {
|
||||
.set("margin", "0")
|
||||
.set("max-width", "100%")
|
||||
.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);
|
||||
|
||||
// Textauswahl-Erkennung nur im Bearbeitungsmodus
|
||||
if (!readOnly) {
|
||||
setupTextSelectionListener(emailContent);
|
||||
}
|
||||
|
||||
Scroller scroller = new Scroller(contentBox);
|
||||
scroller.setSizeFull();
|
||||
scroller.setScrollDirection(Scroller.ScrollDirection.VERTICAL);
|
||||
|
||||
panel.add(title, scroller);
|
||||
if (!readOnly) {
|
||||
panel.add(title, selectionHint, scroller);
|
||||
} else {
|
||||
panel.add(title, scroller);
|
||||
}
|
||||
panel.setFlexGrow(1, scroller);
|
||||
|
||||
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() {
|
||||
VerticalLayout panel = new VerticalLayout();
|
||||
panel.setPadding(false);
|
||||
@@ -411,7 +654,6 @@ public class OrderDetailDialog extends Dialog {
|
||||
|
||||
Div stationCard = new Div();
|
||||
stationCard.addClassNames(
|
||||
LumoUtility.Background.CONTRAST_5,
|
||||
LumoUtility.BorderRadius.MEDIUM,
|
||||
LumoUtility.Padding.SMALL
|
||||
);
|
||||
@@ -419,6 +661,9 @@ public class OrderDetailDialog extends Dialog {
|
||||
.set("flex", "1")
|
||||
.set("position", "relative");
|
||||
|
||||
// Hintergrundfarbe basierend auf Validierungsstatus
|
||||
applyValidationBackground(stationCard, station);
|
||||
|
||||
HorizontalLayout stationContent = new HorizontalLayout();
|
||||
stationContent.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||
stationContent.setSpacing(true);
|
||||
@@ -516,6 +761,109 @@ public class OrderDetailDialog extends Dialog {
|
||||
showTourButton.setWidthFull();
|
||||
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) {
|
||||
@@ -541,7 +889,11 @@ public class OrderDetailDialog extends Dialog {
|
||||
private void openStationEditDialog(Station station, VerticalLayout stationsSection) {
|
||||
Dialog editDialog = new Dialog();
|
||||
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.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
|
||||
@@ -562,14 +914,52 @@ public class OrderDetailDialog extends Dialog {
|
||||
actionComboBox.setWidthFull();
|
||||
|
||||
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 saveButton = new Button("Speichern", e -> {
|
||||
// Prüfen ob Adresse geändert wurde
|
||||
boolean addressChanged = !addressField.getValue().equals(station.getAddress());
|
||||
|
||||
station.setName(nameField.getValue());
|
||||
station.setAddress(addressField.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
|
||||
orderEmail.setSummaryJson(llmService.serializeSummary(summary));
|
||||
onProcessed.accept(orderEmail);
|
||||
|
||||
Reference in New Issue
Block a user