From 8b7256232f09f95b1bedfa61e845ebf2f0c7da6c Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Thu, 19 Feb 2026 11:29:11 +0100 Subject: [PATCH] Erweiterungen --- .../java/de/assecutor/votianlt/model/Job.java | 4 + .../votianlt/pages/view/AddJobView.java | 121 ++++++++++++++--- .../pages/view/CreateInvoiceView.java | 23 +++- .../votianlt/pages/view/JobSummaryView.java | 128 ++++++++---------- 4 files changed, 184 insertions(+), 92 deletions(-) diff --git a/src/main/java/de/assecutor/votianlt/model/Job.java b/src/main/java/de/assecutor/votianlt/model/Job.java index f373039..1f2866a 100644 --- a/src/main/java/de/assecutor/votianlt/model/Job.java +++ b/src/main/java/de/assecutor/votianlt/model/Job.java @@ -150,6 +150,10 @@ public class Job { @Field("route_distance_km") private Double routeDistanceKm; + // Fahrtzeit in Sekunden für die Rechnung + @Field("route_duration_seconds") + private Integer routeDurationSeconds; + /** * Returns the ObjectId as string for JSON serialization. This ensures that the * job id is returned as a string when jobs are retrieved via API. diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java index ef58cc5..bfb411e 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -129,6 +129,11 @@ public class AddJobView extends Main { private VerticalLayout routeInfoBox; private Span routeDistanceLabel; private Span routeDurationLabel; + + // Manuelle Eingabe für Route (wenn keine Berechnung erfolgt) + private VerticalLayout manualRouteInputBox; + private com.vaadin.flow.component.textfield.NumberField manualDistanceInput; + private com.vaadin.flow.component.textfield.IntegerField manualDurationInput; // Date picker fields for appointments private DatePicker pickupDate; @@ -652,6 +657,56 @@ public class AddJobView extends Main { routeInfoBox.add(routeTitle, routeRow, durationRow); content.add(routeInfoBox); + // Manuelle Streckeneingabe (wenn keine Route berechnet wurde) + manualRouteInputBox = new VerticalLayout(); + manualRouteInputBox.setPadding(true); + manualRouteInputBox.setSpacing(true); + manualRouteInputBox.getStyle().set("border", "1px solid var(--lumo-contrast-30pct)"); + manualRouteInputBox.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); + manualRouteInputBox.getStyle().set("background-color", "var(--lumo-contrast-5pct)"); + manualRouteInputBox.setWidthFull(); + manualRouteInputBox.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); + manualRouteInputBox.setVisible(true); // Initial sichtbar + + H3 manualRouteTitle = new H3("Streckeninformation (manuelle Eingabe)"); + manualRouteTitle.getStyle().set("margin", "0"); + manualRouteTitle.getStyle().set("color", "var(--lumo-secondary-text-color)"); + + HorizontalLayout manualInputRow = new HorizontalLayout(); + manualInputRow.setWidthFull(); + manualInputRow.setSpacing(true); + + manualDistanceInput = new com.vaadin.flow.component.textfield.NumberField("Entfernung (km)"); + manualDistanceInput.setWidthFull(); + manualDistanceInput.setPlaceholder("z.B. 125,5"); + manualDistanceInput.setMin(0); + manualDistanceInput.setStep(0.1); + manualDistanceInput.setClearButtonVisible(true); + manualDistanceInput.addValueChangeListener(e -> { + // Preise aktualisieren wenn sich die Distanz ändert + updatePriceSummary(); + if (servicesGrid != null) { + servicesGrid.getDataProvider().refreshAll(); + } + }); + + manualDurationInput = new com.vaadin.flow.component.textfield.IntegerField("Fahrtzeit (Minuten)"); + manualDurationInput.setWidthFull(); + manualDurationInput.setPlaceholder("z.B. 90"); + manualDurationInput.setMin(0); + manualDurationInput.setStep(1); + manualDurationInput.setClearButtonVisible(true); + + manualInputRow.add(manualDistanceInput, manualDurationInput); + + Span manualRouteHint = new Span("Bitte geben Sie die Entfernung und Fahrtzeit ein, da keine automatische Routenberechnung durchgeführt wurde."); + manualRouteHint.getStyle().set("font-size", "var(--lumo-font-size-s)"); + manualRouteHint.getStyle().set("color", "var(--lumo-secondary-text-color)"); + manualRouteHint.getStyle().set("font-style", "italic"); + + manualRouteInputBox.add(manualRouteTitle, manualInputRow, manualRouteHint); + content.add(manualRouteInputBox); + // Title H3 servicesTitle = new H3("Leistungen"); servicesTitle.getStyle().set("margin", "0"); @@ -675,10 +730,8 @@ public class AddJobView extends Main { return ""; }).setHeader("Berechnung").setSortable(true); servicesGrid.addColumn(service -> { - // Get route distance for distance-based calculations - Double routeDistance = (routeCalculationResult != null && routeCalculationResult.isValid()) - ? routeCalculationResult.getDistanceKm() - : null; + // Get route distance for distance-based calculations (berechnet oder manuell) + Double routeDistance = getEffectiveRouteDistance(); BigDecimal price = calculateServicePrice(service, routeDistance); if (price.compareTo(BigDecimal.ZERO) > 0) { return price.setScale(2, RoundingMode.HALF_UP) + " €"; @@ -837,10 +890,8 @@ public class AddJobView extends Main { BigDecimal vatTotal = BigDecimal.ZERO; BigDecimal grossTotal = BigDecimal.ZERO; - // Get route distance for distance-based calculations - Double routeDistance = (routeCalculationResult != null && routeCalculationResult.isValid()) - ? routeCalculationResult.getDistanceKm() - : null; + // Get route distance for distance-based calculations (berechnet oder manuell) + Double routeDistance = getEffectiveRouteDistance(); for (Service service : selectedServices) { BigDecimal price = calculateServicePrice(service, routeDistance); @@ -1459,9 +1510,17 @@ public class AddJobView extends Main { // Store selected service IDs in job for invoice creation job.setServiceIds(selectedServices.stream().map(Service::getId).toList()); - // Store route distance in job for invoice creation + // Store route distance and duration in job for invoice creation if (routeCalculationResult != null && routeCalculationResult.isValid()) { + // Berechnete Route verwenden job.setRouteDistanceKm(routeCalculationResult.getDistanceKm()); + job.setRouteDurationSeconds(routeCalculationResult.getDurationSeconds()); + } else if (manualDistanceInput != null && manualDistanceInput.getValue() != null) { + // Manuelle Eingabe verwenden + job.setRouteDistanceKm(manualDistanceInput.getValue()); + if (manualDurationInput != null && manualDurationInput.getValue() != null) { + job.setRouteDurationSeconds(manualDurationInput.getValue() * 60); // Minuten in Sekunden umrechnen + } } // Validate all required fields using the binder @@ -3074,16 +3133,19 @@ public class AddJobView extends Main { /** * Aktualisiert die Route-Info-Box im Preis-Tab mit den aktuellen Routendaten. + * Zeigt entweder die berechnete Route oder die manuelle Eingabe an. */ private void updateRouteInfoBox() { - if (routeInfoBox == null || routeCalculationResult == null) { + if (routeInfoBox == null || manualRouteInputBox == null) { return; } - if (routeCalculationResult.isValid()) { + if (routeCalculationResult != null && routeCalculationResult.isValid()) { + // Berechnete Route anzeigen routeDistanceLabel.setText(String.format("%.1f km", routeCalculationResult.getDistanceKm())); routeDurationLabel.setText(routeCalculationResult.getFormattedDurationLong()); routeInfoBox.setVisible(true); + manualRouteInputBox.setVisible(false); // Update price summary and grid with new route distance updatePriceSummary(); @@ -3091,7 +3153,9 @@ public class AddJobView extends Main { servicesGrid.getDataProvider().refreshAll(); } } else { + // Manuelle Eingabe anzeigen routeInfoBox.setVisible(false); + manualRouteInputBox.setVisible(true); } } @@ -3169,14 +3233,34 @@ public class AddJobView extends Main { } /** - * Gibt die Entfernung zwischen Abhol- und Lieferadresse in Kilometern zurück. + * Gibt die berechnete oder manuell eingegebene Entfernung zurück (für Preisberechnungen). * - * @return Entfernung in km oder 0.0, wenn keine Berechnung durchgeführt wurde + * @return Entfernung in km oder 0.0, wenn keine Berechnung oder Eingabe vorhanden ist */ public double getRouteDistanceKm() { - return routeCalculationResult != null && routeCalculationResult.isValid() - ? routeCalculationResult.getDistanceKm() - : 0.0; + if (routeCalculationResult != null && routeCalculationResult.isValid()) { + return routeCalculationResult.getDistanceKm(); + } + // Manuelle Eingabe verwenden, wenn vorhanden + if (manualDistanceInput != null && manualDistanceInput.getValue() != null) { + return manualDistanceInput.getValue(); + } + return 0.0; + } + + /** + * Gibt die effektive Routendistanz zurück (berechnet oder manuell eingegeben). + * + * @return Distanz als Double oder null, wenn keine Distanz verfügbar + */ + private Double getEffectiveRouteDistance() { + if (routeCalculationResult != null && routeCalculationResult.isValid()) { + return routeCalculationResult.getDistanceKm(); + } + if (manualDistanceInput != null && manualDistanceInput.getValue() != null) { + return manualDistanceInput.getValue(); + } + return null; } /** @@ -3253,10 +3337,13 @@ public class AddJobView extends Main { // Validierungsergebnisse zurücksetzen addressValidationResults.clear(); - // Route-Info-Box im Preis-Tab verstecken + // Route-Info-Box im Preis-Tab verstecken und manuelle Eingabe anzeigen if (routeInfoBox != null) { routeInfoBox.setVisible(false); } + if (manualRouteInputBox != null) { + manualRouteInputBox.setVisible(true); + } log.debug("Streckeninformationen zurückgesetzt aufgrund von Adressänderungen"); } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java index e9609ad..c358ca6 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java @@ -220,6 +220,13 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter new Span(String.format("%.1f km", distance)))); } + Integer durationSeconds = currentJob.getRouteDurationSeconds(); + if (durationSeconds != null && durationSeconds > 0) { + String formattedDuration = formatDuration(durationSeconds); + routeInfo.add(new HorizontalLayout(new Span("Geschätzte Fahrtzeit:"), + new Span(formattedDuration))); + } + section.add(routeInfo); return section; } @@ -450,11 +457,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter BigDecimal vatAmount = netAmount.multiply(vatRate); BigDecimal totalAmount = netAmount.add(vatAmount); - // Parse the template and replace variables - com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); - com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(templateData); - com.fasterxml.jackson.databind.JsonNode elements = rootNode.get("elements"); - // Build variable substitution map Map variables = new HashMap<>(); @@ -570,4 +572,15 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter pdfDialog.getFooter().add(downloadButton, closeButton); pdfDialog.open(); } + + 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); + } else { + return String.format("%d Min.", minutes); + } + } } \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java index c52b17e..53170b7 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java @@ -459,17 +459,32 @@ public class JobSummaryView extends Main implements HasUrlParameter { final String appUserId = showAppUserPosition ? job.getAppUser() : ""; final boolean shouldUpdate = showAppUserPosition; - String js = buildMapJs(origin, destination, hasPosition, position, appUserId, shouldUpdate); + // Gespeicherte Route-Daten aus dem Job holen + Double savedDistance = job.getRouteDistanceKm(); + Integer savedDuration = job.getRouteDurationSeconds(); + boolean hasSavedRouteData = savedDistance != null && savedDuration != null; + + String js = buildMapJs(origin, destination, hasPosition, position, appUserId, shouldUpdate, + hasSavedRouteData, savedDistance != null ? savedDistance : 0.0, savedDuration != null ? savedDuration : 0); map.getElement().executeJs(js, map.getElement(), routeInfo.getElement()); } private String buildMapJs(String origin, String destination, boolean hasPosition, LocationPosition position, - String appUserId, boolean shouldUpdate) { + String appUserId, boolean shouldUpdate, boolean hasSavedRouteData, double savedDistance, int savedDuration) { String apiKey = getGoogleMapsApiKey(); // Explizit mit Punkt als Dezimaltrennzeichen formatieren String lat = hasPosition ? String.format(java.util.Locale.US, "%.6f", position.getLatitude()) : "0"; String lng = hasPosition ? String.format(java.util.Locale.US, "%.6f", position.getLongitude()) : "0"; + // Distanz mit Punkt als Dezimaltrennzeichen für JavaScript + String savedDistanceStr = String.format(java.util.Locale.US, "%.1f", savedDistance); + + // Gespeicherte Dauer formatieren + int hours = savedDuration / 3600; + int minutes = (savedDuration % 3600) / 60; + String savedDurationText = hours > 0 + ? String.format("%d Std. %d Min.", hours, minutes) + : String.format("%d Min.", minutes); return """ (function(){ @@ -483,6 +498,9 @@ public class JobSummaryView extends Main implements HasUrlParameter { var hasAppUserPos = %s; var appUserId = '%s'; var shouldUpdate = %s; + var hasSavedRouteData = %s; + var savedDistance = %s; + var savedDurationText = '%s'; var appUserMarker = null; var updateInterval = null; @@ -503,7 +521,7 @@ public class JobSummaryView extends Main implements HasUrlParameter { origin: origin, destination: destination, travelMode: google.maps.TravelMode.DRIVING, - provideRouteAlternatives: true, + provideRouteAlternatives: false, drivingOptions: { departureTime: new Date(), trafficModel: google.maps.TrafficModel.BEST_GUESS @@ -511,75 +529,44 @@ public class JobSummaryView extends Main implements HasUrlParameter { }, function(res, status){ if(status === 'OK'){ infoEl.innerHTML = ''; + + // Gespeicherte Route-Daten anzeigen (wenn vorhanden) + if(hasSavedRouteData){ + var savedRouteDiv = document.createElement('div'); + savedRouteDiv.style.margin = '4px 0 12px 0'; + savedRouteDiv.style.padding = '8px'; + savedRouteDiv.style.backgroundColor = '#e3f2fd'; + savedRouteDiv.style.borderRadius = '4px'; + savedRouteDiv.style.fontWeight = 'bold'; + savedRouteDiv.textContent = '📍 Geplante Route: ' + savedDistance.toFixed(1) + ' km • Fahrtzeit: ' + savedDurationText; + infoEl.appendChild(savedRouteDiv); + } + var bounds = new google.maps.LatLngBounds(); - var renderers = []; - var polylines = []; - res.routes.forEach(function(route, idx){ - var dr = new google.maps.DirectionsRenderer({ - map: map, - preserveViewport: idx > 0, - suppressMarkers: false, - suppressPolylines: true - }); - dr.setRouteIndex(idx); - dr.setDirections(res); - renderers.push(dr); - - var path = route.overview_path || []; - var poly = new google.maps.Polyline({ - path: path, - strokeColor: idx === 0 ? '#1976d2' : '#90caf9', - strokeOpacity: 0.95, - strokeWeight: idx === 0 ? 6 : 4 - }); - poly.setMap(map); - polylines.push(poly); - - var leg = route.legs && route.legs[0]; - if(leg){ - var dur = leg.duration ? leg.duration.text : ''; - var durT = leg.duration_in_traffic ? leg.duration_in_traffic.text : ''; - var dist = leg.distance ? leg.distance.text : ''; - var alt = (idx === 0 ? 'Schnellste Route' : 'Alternative ' + idx); - - var row = document.createElement('div'); - row.style.margin = '4px 0'; - row.style.cursor = 'pointer'; - row.textContent = alt + ': ' + dist + ' • Dauer: ' + dur + (durT ? ' (mit Verkehr: ' + durT + ')' : ''); - - row.onmouseenter = function(){ - polylines.forEach(function(p, i){ - p.setOptions({ - strokeColor: i === 0 ? '#90caf9' : '#e3f2fd', - strokeOpacity: 0.6, - strokeWeight: 3 - }); - }); - poly.setOptions({ - strokeColor: '#0d47a1', - strokeOpacity: 1, - strokeWeight: 7 - }); - }; - - row.onmouseleave = function(){ - polylines.forEach(function(p, i){ - p.setOptions({ - strokeColor: i === 0 ? '#1976d2' : '#90caf9', - strokeOpacity: 0.95, - strokeWeight: i === 0 ? 6 : 4 - }); - }); - }; - - infoEl.appendChild(row); - - if(path && path.length){ - path.forEach(function(pt){ bounds.extend(pt); }); - } - } + // Nur die erste (beste) Route anzeigen + var route = res.routes[0]; + var dr = new google.maps.DirectionsRenderer({ + map: map, + preserveViewport: false, + suppressMarkers: false, + suppressPolylines: true }); + dr.setDirections(res); + + var path = route.overview_path || []; + var poly = new google.maps.Polyline({ + path: path, + strokeColor: '#1976d2', + strokeOpacity: 0.95, + strokeWeight: 6 + }); + poly.setMap(map); + + // Bounds aus der Route erstellen + if(path && path.length){ + path.forEach(function(pt){ bounds.extend(pt); }); + } // App-Nutzer Position Marker if(hasAppUserPos){ @@ -656,7 +643,8 @@ public class JobSummaryView extends Main implements HasUrlParameter { })(); """ .formatted(escapeJs(origin), escapeJs(destination), escapeJs(apiKey), lat, lng, - Boolean.toString(hasPosition), escapeJs(appUserId), Boolean.toString(shouldUpdate)); + Boolean.toString(hasPosition), escapeJs(appUserId), Boolean.toString(shouldUpdate), + Boolean.toString(hasSavedRouteData), savedDistanceStr, escapeJs(savedDurationText)); } // Hilfsfunktion zum einfachen Escapen von JS-Zeichen in Strings