Erweiterungen

This commit is contained in:
2026-02-18 13:30:11 +01:00
parent b4b1685ea6
commit 19ac94e0b8
8 changed files with 572 additions and 213 deletions

View File

@@ -12,6 +12,7 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.math.BigDecimal;
import java.util.List;
@Data
@Document(collection = "jobs")
@@ -141,6 +142,14 @@ public class Job {
@Field("time_in_15min_units")
private Integer timeIn15MinUnits;
// Service-IDs für die Rechnung
@Field("service_ids")
private List<String> serviceIds;
// Streckeninformation für die Rechnung (in km)
@Field("route_distance_km")
private Double routeDistanceKm;
/**
* 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.

View File

@@ -77,7 +77,6 @@ public final class AdminLayout extends AppLayout {
// Only admin-specific menu items
SideNavItem dashboard = new SideNavItem("Dashboard", "admin-dashboard", new Icon(VaadinIcon.DASHBOARD));
SideNavItem pdfTest = new SideNavItem("PDF Test", "pdf-test", new Icon(VaadinIcon.FILE_TEXT_O));
SideNavItem invoiceGenerator = new SideNavItem("Rechnungsgenerator", "invoice-generator",
new Icon(VaadinIcon.FILE_PROCESS));
SideNavItem priceTable = new SideNavItem("Preis-Tabelle", "admin-price-table", new Icon(VaadinIcon.COG));
@@ -89,7 +88,6 @@ public final class AdminLayout extends AppLayout {
// Icon(VaadinIcon.FILE_TEXT));
nav.addItem(dashboard);
nav.addItem(pdfTest);
nav.addItem(invoiceGenerator);
nav.addItem(priceTable);
// nav.addItem(systemSettings);

View File

@@ -123,8 +123,6 @@ public class AddressValidationService {
String locationType = geometry.path("location_type").asText();
boolean isPrecise = "ROOFTOP".equals(locationType) || "RANGE_INTERPOLATED".equals(locationType);
// Adresskomponenten prüfen
boolean hasStreetNumber = false;
boolean hasPostalCode = false;
JsonNode addressComponents = firstResult.path("address_components");
@@ -133,7 +131,6 @@ public class AddressValidationService {
for (JsonNode type : types) {
String typeStr = type.asText();
if ("street_number".equals(typeStr)) {
hasStreetNumber = true;
} else if ("postal_code".equals(typeStr)) {
hasPostalCode = true;
}

View File

@@ -675,11 +675,22 @@ public class AddJobView extends Main {
return "";
}).setHeader("Berechnung").setSortable(true);
servicesGrid.addColumn(service -> {
if (service.getEffectivePrice() != null) {
return service.getEffectivePrice().setScale(2, RoundingMode.HALF_UP) + "";
// Get route distance for distance-based calculations
Double routeDistance = (routeCalculationResult != null && routeCalculationResult.isValid())
? routeCalculationResult.getDistanceKm()
: null;
BigDecimal price = calculateServicePrice(service, routeDistance);
if (price.compareTo(BigDecimal.ZERO) > 0) {
return price.setScale(2, RoundingMode.HALF_UP) + "";
}
return "";
}).setHeader("Preis").setSortable(true);
// Show price info if no route calculated yet
if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE && routeDistance == null) {
return service.getPricePerKilometer().setScale(2, RoundingMode.HALF_UP) + " €/km (Route fehlt)";
}
return service.getEffectivePrice() != null
? service.getEffectivePrice().setScale(2, RoundingMode.HALF_UP) + ""
: "";
}).setHeader("Preis").setSortable(false);
servicesGrid.addColumn(service -> {
if (service.getVatRate() != null) {
return service.getVatRate().multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + " %";
@@ -687,6 +698,10 @@ public class AddJobView extends Main {
return "";
}).setHeader("MwSt").setSortable(true);
servicesGrid.addComponentColumn(service -> {
// Verbindliche Leistungen können nicht gelöscht werden
if (service.isMandatory()) {
return new Span(""); // Leeres Element statt Löschen-Button
}
Button removeButton = new Button(new Icon(VaadinIcon.TRASH));
removeButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY,
ButtonVariant.LUMO_SMALL);
@@ -822,8 +837,13 @@ 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;
for (Service service : selectedServices) {
BigDecimal price = service.getEffectivePrice() != null ? service.getEffectivePrice() : BigDecimal.ZERO;
BigDecimal price = calculateServicePrice(service, routeDistance);
BigDecimal vatRate = service.getVatRate() != null ? service.getVatRate() : BigDecimal.ZERO;
netTotal = netTotal.add(price);
@@ -837,6 +857,35 @@ public class AddJobView extends Main {
grossTotalLabel.setText(grossTotal.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + "");
}
/**
* Calculates the actual price for a service based on its calculation basis and
* route distance (for distance-based services).
*/
private BigDecimal calculateServicePrice(Service service, Double routeDistance) {
if (service.getCalculationBasis() == null) {
return BigDecimal.ZERO;
}
switch (service.getCalculationBasis()) {
case FLAT_RATE:
return service.getPrice() != null ? service.getPrice() : BigDecimal.ZERO;
case DISTANCE:
if (service.getPricePerKilometer() != null && routeDistance != null && routeDistance > 0) {
return service.getPricePerKilometer().multiply(BigDecimal.valueOf(routeDistance));
}
return BigDecimal.ZERO;
case TIME:
// For time-based services, we would need time units
// For now, return the price per 15 minutes as base value
return service.getPricePer15Minutes() != null ? service.getPricePer15Minutes() : BigDecimal.ZERO;
default:
return BigDecimal.ZERO;
}
}
private VerticalLayout createPickupSection() {
VerticalLayout section = new VerticalLayout();
section.setSpacing(true);
@@ -1407,8 +1456,13 @@ public class AddJobView extends Main {
.reduce(BigDecimal.ZERO, BigDecimal::add);
job.setPrice(totalPrice);
// Store selected service IDs in job (optional - if Job has serviceIds field)
// job.setServiceIds(selectedServices.stream().map(Service::getId).toList());
// 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
if (routeCalculationResult != null && routeCalculationResult.isValid()) {
job.setRouteDistanceKm(routeCalculationResult.getDistanceKm());
}
// Validate all required fields using the binder
if (binder.writeBeanIfValid(job)) {
@@ -2802,10 +2856,6 @@ public class AddJobView extends Main {
return field.getValue() != null ? field.getValue().trim() : "";
}
private String getComboValueOrEmpty(ComboBox<String> field) {
return field.getValue() != null ? field.getValue().trim() : "";
}
/**
* Zeigt den Adressvalidierungsdialog an. Die Prüfung erfolgt im Hintergrund und
* der Dialog wird aktualisiert, sobald die Ergebnisse vorliegen.
@@ -3034,6 +3084,12 @@ public class AddJobView extends Main {
routeDistanceLabel.setText(String.format("%.1f km", routeCalculationResult.getDistanceKm()));
routeDurationLabel.setText(routeCalculationResult.getFormattedDurationLong());
routeInfoBox.setVisible(true);
// Update price summary and grid with new route distance
updatePriceSummary();
if (servicesGrid != null) {
servicesGrid.getDataProvider().refreshAll();
}
} else {
routeInfoBox.setVisible(false);
}

View File

@@ -43,10 +43,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
private Job currentJob;
private List<ServiceRow> gridRows = new ArrayList<>();
private List<Service> allUserServices;
private Grid<ServiceRow> servicesGrid;
private IntegerField kilometersField;
private IntegerField timeField;
private Div servicesSection;
/**
@@ -88,6 +85,20 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
setSpacing(true);
}
/**
* Lädt die Services, die beim Job-Erstellen ausgewählt wurden.
*/
private void loadSelectedServicesFromJob() {
if (currentJob.getServiceIds() != null && !currentJob.getServiceIds().isEmpty()) {
gridRows.clear();
for (String serviceId : currentJob.getServiceIds()) {
serviceRepository.findById(serviceId).ifPresent(service -> {
gridRows.add(new ServiceRow(service));
});
}
}
}
@Override
public void setParameter(BeforeEvent event, String jobIdHex) {
try {
@@ -116,13 +127,18 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
H2 title = new H2("Rechnung erstellen für Auftrag " + currentJob.getJobNumber());
add(title);
// Load previously selected services from job
loadSelectedServicesFromJob();
// Job Details Section
Div jobDetailsSection = createJobDetailsSection();
add(jobDetailsSection);
// Performance Data Section
Div performanceDataSection = createPerformanceDataSection();
add(performanceDataSection);
// Route Information Section (if available)
if (currentJob.getRouteDistanceKm() != null && currentJob.getRouteDistanceKm() > 0) {
Div routeInfoSection = createRouteInfoSection();
add(routeInfoSection);
}
// Services Selection Section
Div servicesSection = createServicesSelectionSection();
@@ -164,46 +180,28 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
return section;
}
private Div createPerformanceDataSection() {
private Div createRouteInfoSection() {
Div section = new Div();
section.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
section.getStyle().set("border", "1px solid var(--lumo-primary-color-50pct)")
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box");
.set("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box")
.set("background-color", "var(--lumo-primary-color-10pct)");
H3 sectionTitle = new H3("Leistungsdaten");
H3 sectionTitle = new H3("Streckeninformation");
sectionTitle.getStyle().set("color", "var(--lumo-primary-text-color)");
section.add(sectionTitle);
VerticalLayout performanceLayout = new VerticalLayout();
performanceLayout.setSpacing(true);
performanceLayout.setWidthFull();
VerticalLayout routeInfo = new VerticalLayout();
routeInfo.setSpacing(true);
routeInfo.setWidthFull();
// Kilometers field
HorizontalLayout kilometersLayout = new HorizontalLayout();
kilometersLayout.setWidthFull();
Span kilometersLabel = new Span("Gefahrene Kilometer:");
kilometersLabel.getStyle().set("width", "200px");
kilometersField = new IntegerField();
kilometersField.setWidth("150px");
kilometersField.setMin(0);
kilometersField.setValue(currentJob.getKilometersDriven() != null ? currentJob.getKilometersDriven() : 0);
kilometersField.addValueChangeListener(e -> updateSummarySection());
kilometersLayout.add(kilometersLabel, kilometersField);
performanceLayout.add(kilometersLayout);
Double distance = currentJob.getRouteDistanceKm();
if (distance != null) {
routeInfo.add(new HorizontalLayout(new Span("Berechnete Entfernung:"),
new Span(String.format("%.1f km", distance))));
}
// Time field (in 15-minute units)
HorizontalLayout timeLayout = new HorizontalLayout();
timeLayout.setWidthFull();
Span timeLabel = new Span("Arbeitszeit (15-Minuten-Einheiten):");
timeLabel.getStyle().set("width", "200px");
timeField = new IntegerField();
timeField.setWidth("150px");
timeField.setMin(0);
timeField.setValue(currentJob.getTimeIn15MinUnits() != null ? currentJob.getTimeIn15MinUnits() : 0);
timeField.addValueChangeListener(e -> updateSummarySection());
timeLayout.add(timeLabel, timeField);
performanceLayout.add(timeLayout);
section.add(performanceLayout);
section.add(routeInfo);
return section;
}
@@ -213,46 +211,23 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
.set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)")
.set("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box");
H3 sectionTitle = new H3("Leistungen auswählen");
H3 sectionTitle = new H3("Leistungen");
servicesSection.add(sectionTitle);
// Load services for current user (only once)
if (allUserServices == null) {
String currentUserId = securityService.getCurrentUserId().toHexString();
allUserServices = serviceRepository.findByUserId(currentUserId);
}
// Initialize with 2 empty rows if gridRows is empty
if (gridRows.isEmpty()) {
gridRows.add(new ServiceRow());
gridRows.add(new ServiceRow());
}
// Create grid with editable rows
// Create grid with read-only rows
servicesGrid = new Grid<>();
servicesGrid.setWidthFull();
servicesGrid.setAllRowsVisible(true);
// Service selection column (ComboBox)
servicesGrid.addComponentColumn(row -> {
ComboBox<Service> serviceCombo = new ComboBox<>();
serviceCombo.setItems(allUserServices);
serviceCombo.setItemLabelGenerator(Service::getName);
serviceCombo.setPlaceholder("Leistung auswählen...");
serviceCombo.setWidthFull();
serviceCombo.setValue(row.getService());
serviceCombo.addValueChangeListener(event -> {
row.setService(event.getValue());
// Refresh the grid to show updated calculation basis and price
servicesGrid.getDataProvider().refreshItem(row);
updateSummarySection();
});
return serviceCombo;
// Service name column (read-only)
servicesGrid.addColumn(row -> {
if (row.getService() != null) {
return row.getService().getName();
}
return "";
}).setHeader("Leistung").setAutoWidth(true).setFlexGrow(2);
// Calculation basis column
// Calculation basis column (read-only)
servicesGrid.addColumn(row -> {
if (row.getService() != null && row.getService().getCalculationBasis() != null) {
return switch (row.getService().getCalculationBasis()) {
@@ -264,7 +239,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
return "";
}).setHeader("Berechnungsgrundlage").setAutoWidth(true).setFlexGrow(1);
// Price column
// Price column (read-only)
servicesGrid.addColumn(row -> {
if (row.getService() != null) {
BigDecimal price = calculateServicePrice(row.getService());
@@ -278,15 +253,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
servicesGrid.setItems(gridRows);
servicesSection.add(servicesGrid);
// Add button to add new row
Button addButton = new Button("Leistung hinzufügen", e -> {
ServiceRow newRow = new ServiceRow();
gridRows.add(newRow);
servicesGrid.getDataProvider().refreshAll();
});
addButton.getStyle().set("margin-top", "var(--lumo-space-m)");
servicesSection.add(addButton);
return servicesSection;
}
@@ -342,13 +308,12 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
if (service.getCalculationBasis() == Service.CalculationBasis.FLAT_RATE && service.getPrice() != null) {
return service.getPrice();
} else if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE
&& service.getPricePerKilometer() != null && kilometersField != null
&& kilometersField.getValue() != null) {
BigDecimal kilometers = new BigDecimal(kilometersField.getValue());
&& service.getPricePerKilometer() != null && currentJob.getRouteDistanceKm() != null) {
BigDecimal kilometers = BigDecimal.valueOf(currentJob.getRouteDistanceKm());
return service.getPricePerKilometer().multiply(kilometers);
} else if (service.getCalculationBasis() == Service.CalculationBasis.TIME
&& service.getPricePer15Minutes() != null && timeField != null && timeField.getValue() != null) {
BigDecimal timeUnits = new BigDecimal(timeField.getValue());
&& service.getPricePer15Minutes() != null && currentJob.getTimeIn15MinUnits() != null) {
BigDecimal timeUnits = new BigDecimal(currentJob.getTimeIn15MinUnits());
return service.getPricePer15Minutes().multiply(timeUnits);
}
@@ -362,14 +327,13 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
if (service.getCalculationBasis() == Service.CalculationBasis.FLAT_RATE && service.getPrice() != null) {
total = total.add(service.getPrice());
} else if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE
&& service.getPricePerKilometer() != null && kilometersField != null
&& kilometersField.getValue() != null) {
BigDecimal kilometers = new BigDecimal(kilometersField.getValue());
&& service.getPricePerKilometer() != null && currentJob.getRouteDistanceKm() != null) {
BigDecimal kilometers = BigDecimal.valueOf(currentJob.getRouteDistanceKm());
BigDecimal serviceTotal = service.getPricePerKilometer().multiply(kilometers);
total = total.add(serviceTotal);
} else if (service.getCalculationBasis() == Service.CalculationBasis.TIME
&& service.getPricePer15Minutes() != null && timeField != null && timeField.getValue() != null) {
BigDecimal timeUnits = new BigDecimal(timeField.getValue());
&& service.getPricePer15Minutes() != null && currentJob.getTimeIn15MinUnits() != null) {
BigDecimal timeUnits = new BigDecimal(currentJob.getTimeIn15MinUnits());
BigDecimal serviceTotal = service.getPricePer15Minutes().multiply(timeUnits);
total = total.add(serviceTotal);
}
@@ -402,14 +366,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
}
private void updateSummarySection() {
// Update the job with new values
if (kilometersField != null && kilometersField.getValue() != null) {
currentJob.setKilometersDriven(kilometersField.getValue());
}
if (timeField != null && timeField.getValue() != null) {
currentJob.setTimeIn15MinUnits(timeField.getValue());
}
// Refresh the services grid to update calculated prices
refreshServicesGrid();

View File

@@ -1,98 +0,0 @@
package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.IFrame;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.StreamResource;
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
import de.assecutor.votianlt.service.CustomerInvoiceService;
import de.assecutor.votianlt.service.SystemInvoiceService;
import jakarta.annotation.security.RolesAllowed;
import java.io.ByteArrayInputStream;
@Route(value = "pdf-test", layout = AdminLayout.class)
@PageTitle("PDF Test")
@RolesAllowed("ADMIN")
public class PdfTestView extends VerticalLayout {
private final SystemInvoiceService systemInvoiceService;
private final CustomerInvoiceService customerInvoiceService;
public PdfTestView(SystemInvoiceService systemInvoiceService, CustomerInvoiceService customerInvoiceService) {
this.systemInvoiceService = systemInvoiceService;
this.customerInvoiceService = customerInvoiceService;
setSpacing(false);
setPadding(false);
getStyle().set("margin", "14px");
setWidth("90%");
H2 title = new H2("PDF Test");
add(title);
Button generateHtmlPdfButton = new Button("PDF aus system_invoice.html generieren");
generateHtmlPdfButton.addClickListener(e -> generateHtmlPdf());
Button generateCustomerInvoicePdfButton = new Button("PDF aus customer_invoice.html generieren");
generateCustomerInvoicePdfButton.addClickListener(e -> generateCustomerInvoicePdf());
// Create button layout
HorizontalLayout buttonLayout = new HorizontalLayout();
buttonLayout.add(generateHtmlPdfButton, generateCustomerInvoicePdfButton);
buttonLayout.setSpacing(true);
// Initialize PDF viewer
IFrame pdfViewer = new IFrame();
pdfViewer.setWidth("100%");
pdfViewer.setHeight("800px");
pdfViewer.getStyle().set("border", "1px solid #ccc");
pdfViewer.setVisible(false);
add(buttonLayout);
add(pdfViewer);
}
private void generateHtmlPdf() {
try {
byte[] pdfBytes = systemInvoiceService.generateInvoicePdfFromHtml();
StreamResource resource = new StreamResource("vlt-invoice.pdf", () -> new ByteArrayInputStream(pdfBytes));
resource.setContentType("application/pdf");
getUI().ifPresent(ui -> {
var registration = ui.getSession().getResourceRegistry().registerResource(resource);
ui.getPage().open(registration.getResourceUri().toString(), "_blank");
});
Notification.show("PDF aus HTML erfolgreich generiert!", 3000, Notification.Position.BOTTOM_CENTER);
} catch (Exception ex) {
Notification.show("Fehler beim Generieren des PDFs aus HTML: " + ex.getMessage(), 5000,
Notification.Position.BOTTOM_CENTER);
}
}
private void generateCustomerInvoicePdf() {
try {
byte[] pdfBytes = customerInvoiceService.generateCustomerInvoicePdf();
StreamResource resource = new StreamResource("customer-invoice.pdf",
() -> new ByteArrayInputStream(pdfBytes));
resource.setContentType("application/pdf");
getUI().ifPresent(ui -> {
var registration = ui.getSession().getResourceRegistry().registerResource(resource);
ui.getPage().open(registration.getResourceUri().toString(), "_blank");
});
Notification.show("Customer PDF erfolgreich generiert!", 3000, Notification.Position.BOTTOM_CENTER);
} catch (Exception ex) {
Notification.show("Fehler beim Generieren des Customer PDFs: " + ex.getMessage(), 5000,
Notification.Position.BOTTOM_CENTER);
}
}
}