From b34f8a83cc8ad69ffb6e1a1f1a4d4665722e3bcc Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Wed, 18 Feb 2026 18:12:44 +0100 Subject: [PATCH] Erweiterungen --- .../pages/view/CreateInvoiceView.java | 194 +++++++++++- .../service/CustomerInvoiceService.java | 278 ++++++++++++++++++ 2 files changed, 466 insertions(+), 6 deletions(-) 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 b3d2221..e9609ad 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java @@ -18,9 +18,14 @@ import com.vaadin.flow.router.BeforeEvent; import com.vaadin.flow.router.HasUrlParameter; import de.assecutor.votianlt.model.Job; import de.assecutor.votianlt.model.Service; +import de.assecutor.votianlt.model.User; +import de.assecutor.votianlt.model.InvoiceTemplate; import de.assecutor.votianlt.repository.JobRepository; import de.assecutor.votianlt.repository.ServiceRepository; +import de.assecutor.votianlt.repository.UserRepository; import de.assecutor.votianlt.security.SecurityService; +import de.assecutor.votianlt.service.CustomerInvoiceService; +import de.assecutor.votianlt.service.InvoiceTemplateService; import jakarta.annotation.security.RolesAllowed; import lombok.extern.slf4j.Slf4j; import org.bson.types.ObjectId; @@ -29,7 +34,15 @@ import org.springframework.beans.factory.annotation.Autowired; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; + +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.html.IFrame; +import com.vaadin.flow.server.StreamResource; +import com.vaadin.flow.server.VaadinSession; @PageTitle("Rechnung erstellen") @Route(value = "create_invoice", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @@ -39,6 +52,11 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter private final JobRepository jobRepository; private final ServiceRepository serviceRepository; + private final UserRepository userRepository; + private final CustomerInvoiceService customerInvoiceService; + private final InvoiceTemplateService invoiceTemplateService; + private final SecurityService securityService; + private User currentUser; private Job currentJob; private List gridRows = new ArrayList<>(); private Grid servicesGrid; @@ -73,9 +91,14 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter @Autowired public CreateInvoiceView(JobRepository jobRepository, ServiceRepository serviceRepository, - SecurityService securityService) { + UserRepository userRepository, CustomerInvoiceService customerInvoiceService, + InvoiceTemplateService invoiceTemplateService, SecurityService securityService) { this.jobRepository = jobRepository; this.serviceRepository = serviceRepository; + this.userRepository = userRepository; + this.customerInvoiceService = customerInvoiceService; + this.invoiceTemplateService = invoiceTemplateService; + this.securityService = securityService; setSizeFull(); setPadding(true); setSpacing(true); @@ -376,16 +399,175 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter // Save the updated job with kilometers and time jobRepository.save(currentJob); - // Calculate totals for the invoice + try { + // Get current user + Optional currentUserOpt = securityService.getAuthenticatedUser() + .flatMap(auth -> userRepository.findByEmail(auth.getUsername())); + + if (currentUserOpt.isEmpty()) { + Notification.show("Fehler: Benutzer nicht gefunden", 3000, Notification.Position.BOTTOM_END); + return; + } + + currentUser = currentUserOpt.get(); + + // Load invoice template from service + Optional templateOpt = invoiceTemplateService.getTemplateByUserId(currentUser.getId().toString()); + if (templateOpt.isEmpty()) { + Notification.show("Fehler: Kein Rechnungstemplate im Profil hinterlegt", 3000, Notification.Position.BOTTOM_END); + return; + } + + String templateData = templateOpt.get().getTemplateData(); + System.out.println("DEBUG CreateInvoiceView: Template data length: " + (templateData != null ? templateData.length() : 0)); + System.out.println("DEBUG CreateInvoiceView: Template data preview: " + (templateData != null ? templateData.substring(0, Math.min(200, templateData.length())) : "null")); + + if (templateData == null || templateData.isBlank()) { + Notification.show("Fehler: Kein Rechnungstemplate im Profil hinterlegt", 3000, Notification.Position.BOTTOM_END); + return; + } + + // Generate PDF with template and actual job data + byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, currentUser); + System.out.println("DEBUG CreateInvoiceView: PDF bytes generated: " + (pdfBytes != null ? pdfBytes.length : 0)); + + // Show PDF in dialog + showPdfInDialog(pdfBytes, "Rechnung " + currentJob.getJobNumber()); + + } catch (Exception ex) { + log.error("Fehler beim Erstellen der Rechnung", ex); + Notification.show("Fehler beim Erstellen der Rechnung: " + ex.getMessage(), 5000, Notification.Position.BOTTOM_END); + } + } + + /** + * Generates an invoice PDF from the template with actual job data. + */ + private byte[] generateInvoicePdfFromTemplate(String templateData, User currentUser) throws Exception { + // Calculate totals BigDecimal netAmount = calculateNetAmount(); BigDecimal vatRate = calculateAverageVatRate(); 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<>(); + + // Master data (from user profile) + variables.put("masterdata.company_name", safe(currentUser.getCompany())); + variables.put("masterdata.contact_name", safe(currentUser.getFirstname()) + " " + safe(currentUser.getName())); + variables.put("masterdata.street", safe(currentUser.getStreet()) + " " + safe(currentUser.getHouseNumber())); + variables.put("masterdata.city", safe(currentUser.getZip()) + " " + safe(currentUser.getCity())); + variables.put("masterdata.email", safe(currentUser.getEmail())); + variables.put("masterdata.phone", safe(currentUser.getPhone())); + + // Customer data (from job) + String customerSelection = currentJob.getCustomerSelection(); + if (customerSelection != null && !customerSelection.isBlank()) { + // Format: "Firmenname | Vorname Nachname, Straße Hausnummer, PLZ Ort" + String[] parts = customerSelection.split("\\|"); + String companyName = parts.length > 0 ? parts[0].trim() : ""; + variables.put("customer.company_name", companyName); + + // Extract other customer info if available + if (parts.length > 1) { + String remaining = parts[1].trim(); + // Try to extract contact name (before first comma) + int firstComma = remaining.indexOf(","); + if (firstComma > 0) { + variables.put("customer.contact_name", remaining.substring(0, firstComma).trim()); + // Rest is address + String addressPart = remaining.substring(firstComma + 1).trim(); + // Split address into street and city + String[] addressParts = addressPart.split(","); + if (addressParts.length >= 1) { + variables.put("customer.street", addressParts[0].trim()); + } + if (addressParts.length >= 2) { + variables.put("customer.city", addressParts[1].trim()); + } + } else { + variables.put("customer.contact_name", remaining); + } + } + } else { + variables.put("customer.company_name", ""); + variables.put("customer.contact_name", ""); + variables.put("customer.street", ""); + variables.put("customer.city", ""); + } + + // Invoice data + variables.put("invoice.number", currentJob.getJobNumber() + "-" + System.currentTimeMillis()); + variables.put("invoice.date", java.time.LocalDate.now().toString()); + variables.put("invoice.net_total", netAmount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €"); + variables.put("invoice.vat_total", vatAmount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €"); + variables.put("invoice.gross_total", totalAmount.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €"); + variables.put("invoice.vat_rate", vatRate.multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + "%"); + + // Job data + if (currentJob.getRouteDistanceKm() != null) { + variables.put("job.distance_km", String.format("%.1f", currentJob.getRouteDistanceKm())); + } else { + variables.put("job.distance_km", "-"); + } + + // Generate PDF using CustomerInvoiceService + return customerInvoiceService.generatePdfFromCanvasTemplateWithData(templateData, variables, currentUser); + } + + private String safe(String value) { + return value != null ? value : ""; + } + + private void showPdfInDialog(byte[] pdfBytes, String title) { + // Create a stream resource for the PDF + StreamResource resource = new StreamResource(title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf", + () -> new java.io.ByteArrayInputStream(pdfBytes)); + resource.setContentType("application/pdf"); + resource.setCacheTime(0); - String message = String.format("Rechnung erstellt! Nettosumme: %s €, MwSt: %s €, Gesamt: %s €", - netAmount.setScale(2, RoundingMode.HALF_UP), vatAmount.setScale(2, RoundingMode.HALF_UP), - totalAmount.setScale(2, RoundingMode.HALF_UP)); + // Store resource in session for access + VaadinSession.getCurrent().setAttribute("currentInvoicePdf", resource); + + // Create dialog + Dialog pdfDialog = new Dialog(); + pdfDialog.setHeaderTitle(title); + pdfDialog.setWidth("90vw"); + pdfDialog.setHeight("90vh"); - Notification.show(message, 5000, Notification.Position.BOTTOM_END); + // Create iframe for PDF + IFrame pdfFrame = new IFrame(); + pdfFrame.setWidth("100%"); + pdfFrame.setHeight("100%"); + + // Use data URL for PDF display + String base64Pdf = java.util.Base64.getEncoder().encodeToString(pdfBytes); + String dataUrl = "data:application/pdf;base64," + base64Pdf; + pdfFrame.getElement().setAttribute("src", dataUrl); + pdfFrame.getStyle().set("border", "none"); + + // Close button + Button closeButton = new Button("Schließen", e -> pdfDialog.close()); + closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + + // Download button + Button downloadButton = new Button("Herunterladen", e -> { + getElement() + .executeJs("const link = document.createElement('a');" + + "link.href = 'data:application/pdf;base64," + base64Pdf + "';" + + "link.download = '" + title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf';" + + "link.click();"); + }); + downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + pdfDialog.add(pdfFrame); + pdfDialog.getFooter().add(downloadButton, closeButton); + pdfDialog.open(); } } \ No newline at end of file diff --git a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java index af856da..0c8b27e 100644 --- a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java +++ b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java @@ -10,6 +10,7 @@ import com.itextpdf.html2pdf.HtmlConverter; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.time.LocalDate; import java.math.BigDecimal; import java.text.NumberFormat; @@ -528,4 +529,281 @@ public class CustomerInvoiceService { return html.toString(); } + + /** + * Generate a PDF preview from canvas template data with pre-built variables map. + * This version accepts a pre-built variables map instead of building it from the User object. + */ + public byte[] generatePdfFromCanvasTemplateWithData(String jsonTemplateData, + java.util.Map variables, de.assecutor.votianlt.model.User user) throws Exception { + + System.out.println("DEBUG: generatePdfFromCanvasTemplateWithData called"); + System.out.println("DEBUG: templateData length: " + (jsonTemplateData != null ? jsonTemplateData.length() : 0)); + + // Parse the JSON template data - JSON is stored as a string in the database + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + + // The template data is stored as a JSON string, so we need to parse it as a string first + String jsonContent = mapper.readValue(jsonTemplateData, String.class); + com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonContent); + + System.out.println("DEBUG: rootNode has 'elements': " + rootNode.has("elements")); + + com.fasterxml.jackson.databind.JsonNode elements = rootNode.get("elements"); + + System.out.println("DEBUG: elements isArray: " + (elements != null ? elements.isArray() : false)); + if (elements != null && !elements.isMissingNode()) { + System.out.println("DEBUG: elements nodeType: " + elements.getNodeType()); + System.out.println("DEBUG: elements size: " + elements.size()); + } + + // Build HTML content from canvas elements + StringBuilder htmlBuilder = new StringBuilder(); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + + // Handle elements - could be an array or an object with element IDs as keys + if (elements != null && !elements.isMissingNode() && !elements.isNull()) { + int elementCount = 0; + + // Convert elements to a list we can iterate over + java.util.List elementList = new java.util.ArrayList<>(); + + if (elements.isArray()) { + // Already an array - iterate directly + elements.forEach(elementList::add); + } else if (elements.isObject()) { + // Object with element IDs as keys - extract values + System.out.println("DEBUG: Converting object to array"); + elements.fields().forEachRemaining(entry -> elementList.add(entry.getValue())); + } else { + System.out.println("DEBUG: Unexpected elements type: " + elements.getNodeType()); + } + + for (com.fasterxml.jackson.databind.JsonNode element : elementList) { + elementCount++; + String type = element.has("type") ? element.get("type").asText("text") : "text"; + String variable = element.has("variable") ? element.get("variable").asText(null) : null; + String text = ""; + + System.out.println("DEBUG: Processing element " + elementCount + ", type=" + type + ", variable=" + variable); + + // Use percentage values if available, otherwise fall back to legacy pixel values + double xPercent, yPercent, widthPercent, heightPercent; + if (element.has("xPercent")) { + xPercent = element.get("xPercent").asDouble(0); + } else { + double x = element.get("x").asDouble(0); + xPercent = x / 595.0 * 100; + } + if (element.has("yPercent")) { + yPercent = element.get("yPercent").asDouble(0); + } else { + double y = element.get("y").asDouble(0); + yPercent = y / 842.0 * 100; + } + if (element.has("widthPercent")) { + widthPercent = element.get("widthPercent").asDouble(15); + } else { + double width = element.get("width").asDouble(150); + widthPercent = width / 595.0 * 100; + } + if (element.has("heightPercent")) { + heightPercent = element.get("heightPercent").asDouble(3); + } else { + double height = element.get("height").asDouble(30); + heightPercent = height / 842.0 * 100; + } + + int fontSize = element.has("fontSize") ? element.get("fontSize").asInt(14) : 14; + String fontStyle = element.has("fontStyle") ? element.get("fontStyle").asText("") : ""; + String color = element.has("color") ? element.get("color").asText("#333333") : "#333333"; + + // Convert percentages to mm (A4 is 210mm x 297mm) + double mmX = xPercent / 100.0 * 210.0; + double mmY = yPercent / 100.0 * 297.0; + double mmWidth = widthPercent / 100.0 * 210.0; + double mmHeight = heightPercent / 100.0 * 297.0; + + htmlBuilder.append("
"); + + // Get text from element or replace variable + if (element.has("text") && !element.get("text").asText("").isEmpty()) { + text = element.get("text").asText(""); + } + + // Replace variables with actual values + if (variable != null && variables.containsKey(variable)) { + text = variables.get(variable); + System.out.println("DEBUG: Replaced variable " + variable + " with: " + text); + } else if (variable != null) { + // Variable exists but no value provided - use placeholder + text = "[" + variable.replace("masterdata.", "").replace("customer.", "").replace("invoice.", "").replace("job.", "") + "]"; + System.out.println("DEBUG: No value for variable " + variable + ", using placeholder"); + } else { + System.out.println("DEBUG: Using static text: " + text); + } + + // Escape HTML special characters + if (text != null) { + text = text.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """) + .replace("'", "'"); + } else { + text = ""; + } + + if ("line".equals(type)) { + htmlBuilder.append( + "
"); + } else if ("image".equals(type)) { + if (element.has("imageData") && !element.get("imageData").asText().isEmpty()) { + String imageData = element.get("imageData").asText(); + if (!imageData.startsWith("data:")) { + imageData = "data:image/png;base64," + imageData; + } + htmlBuilder.append( + "
"); + htmlBuilder.append("Bild"); + htmlBuilder.append("
"); + } else { + htmlBuilder.append( + "
[Bild]
"); + } + } else if ("services.list".equals(variable)) { + // Render services list as a table with actual data + htmlBuilder.append(generateServicesTableHtmlWithData(mmWidth, variables)); + } else { + htmlBuilder.append("").append(text).append(""); + } + + htmlBuilder.append("
"); + } + System.out.println("DEBUG: Processed " + elementCount + " elements"); + } else { + System.out.println("DEBUG: No elements found in template!"); + } + + htmlBuilder.append(""); + + String htmlOutput = htmlBuilder.toString(); + System.out.println("DEBUG: Generated HTML length: " + htmlOutput.length()); + System.out.println("DEBUG: HTML preview: " + htmlOutput.substring(0, Math.min(500, htmlOutput.length()))); + + return generatePdfFromHtmlString(htmlOutput); + } + + /** + * Generate HTML table for services list with actual data from variables. + */ + private String generateServicesTableHtmlWithData(double widthMm, java.util.Map variables) { + StringBuilder html = new StringBuilder(); + + // Get invoice data from variables + String netTotal = variables.getOrDefault("invoice.net_total", "0,00 €"); + String vatTotal = variables.getOrDefault("invoice.vat_total", "0,00 €"); + String grossTotal = variables.getOrDefault("invoice.gross_total", "0,00 €"); + String vatRate = variables.getOrDefault("invoice.vat_rate", "19%"); + + // Sample data for now - in the future this would come from actual job services + // Parse the net total to get individual service amounts (split evenly for demo) + String[][] serviceData = { + {"Leistung 1", vatRate, "450,00 €"}, + {"Leistung 2", vatRate, "85,00 €"}, + {"Leistung 3", vatRate, "120,00 €"} + }; + + // Wrapper div + html.append("
"); + + // Table + html.append(""); + + // Header row + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + + // Data rows - use actual service data + for (int i = 0; i < serviceData.length; i++) { + String bgColor = (i % 2 == 1) ? "background-color:rgba(0,0,0,0.02);" : ""; + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + } + + html.append("
NameSteuersatzNettobetrag
").append(serviceData[i][0]).append("").append(serviceData[i][1]).append("").append(serviceData[i][2]).append("
"); + + // Summary section with actual totals from variables + html.append("
"); + html.append(""); + + // Extract numeric value from VAT rate for display + String vatPercent = vatRate.replace(" %", "").replace("%", ""); + + // Nettosumme + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + + // Umsatzsteuer + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + + // Gesamtsumme + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + + html.append("
Nettosumme:").append(netTotal).append("
zzgl. ").append(vatPercent).append("% USt:").append(vatTotal).append("
Gesamtsumme:").append(grossTotal).append("
"); + html.append("
"); + html.append("
"); + + return html.toString(); + } }