Erweiterungen
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -130,6 +130,11 @@ public class AddJobView extends Main {
|
||||
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;
|
||||
private DatePicker deliveryDate;
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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<String, String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -459,17 +459,32 @@ public class JobSummaryView extends Main implements HasUrlParameter<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
}, function(res, status){
|
||||
if(status === 'OK'){
|
||||
infoEl.innerHTML = '';
|
||||
var bounds = new google.maps.LatLngBounds();
|
||||
var renderers = [];
|
||||
var polylines = [];
|
||||
|
||||
res.routes.forEach(function(route, idx){
|
||||
// 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();
|
||||
|
||||
// Nur die erste (beste) Route anzeigen
|
||||
var route = res.routes[0];
|
||||
var dr = new google.maps.DirectionsRenderer({
|
||||
map: map,
|
||||
preserveViewport: idx > 0,
|
||||
preserveViewport: false,
|
||||
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',
|
||||
strokeColor: '#1976d2',
|
||||
strokeOpacity: 0.95,
|
||||
strokeWeight: idx === 0 ? 6 : 4
|
||||
strokeWeight: 6
|
||||
});
|
||||
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);
|
||||
|
||||
// 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<String> {
|
||||
})();
|
||||
"""
|
||||
.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
|
||||
|
||||
Reference in New Issue
Block a user