Erweiterungen

This commit is contained in:
2026-02-18 18:12:44 +01:00
parent 8b6412cb0e
commit b34f8a83cc
2 changed files with 466 additions and 6 deletions

View File

@@ -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<ServiceRow> gridRows = new ArrayList<>();
private Grid<ServiceRow> 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<User> 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<InvoiceTemplate> 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);
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));
// 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");
Notification.show(message, 5000, Notification.Position.BOTTOM_END);
// Build variable substitution map
Map<String, String> 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);
// 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");
// 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();
}
}

View File

@@ -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<String, String> 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("<!DOCTYPE html>");
htmlBuilder.append("<html><head>");
htmlBuilder.append("<meta charset='UTF-8'>");
htmlBuilder.append("<style>");
htmlBuilder.append("@page { size: A4; margin: 0; }");
htmlBuilder.append(
"body { margin: 0; padding: 0; width: 210mm; height: 297mm; position: relative; font-family: Arial, sans-serif; }");
htmlBuilder.append(".element { position: absolute; box-sizing: border-box; overflow: hidden; }");
htmlBuilder.append(".text { white-space: nowrap; overflow: visible; }");
htmlBuilder.append(".line { border-top: 1px solid #333; }");
htmlBuilder.append(".image { overflow: hidden; }");
htmlBuilder.append(".image img { width: 100%; height: 100%; object-fit: contain; display: block; }");
htmlBuilder.append("</style>");
htmlBuilder.append("</head><body>");
// 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<com.fasterxml.jackson.databind.JsonNode> 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("<div class='element ").append(type).append("' ");
htmlBuilder.append("style='");
htmlBuilder.append("left:").append(String.format(java.util.Locale.US, "%.2f", mmX)).append("mm;");
htmlBuilder.append("top:").append(String.format(java.util.Locale.US, "%.2f", mmY)).append("mm;");
htmlBuilder.append("width:").append(String.format(java.util.Locale.US, "%.2f", mmWidth)).append("mm;");
htmlBuilder.append("height:").append(String.format(java.util.Locale.US, "%.2f", mmHeight))
.append("mm;");
htmlBuilder.append("font-size:").append(fontSize).append("pt;");
htmlBuilder.append("line-height:").append(String.format(java.util.Locale.US, "%.2f", fontSize * 1.2))
.append("pt;");
htmlBuilder.append("color:").append(color).append(";");
if (!fontStyle.isEmpty()) {
if (fontStyle.contains("bold"))
htmlBuilder.append("font-weight:bold;");
}
// For services.list use block display
if ("services.list".equals(variable)) {
htmlBuilder.append("display:block;overflow:visible;padding:0;");
} else {
htmlBuilder.append("display:flex;align-items:center;");
}
htmlBuilder.append("'");
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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
.replace("'", "&#x27;");
} else {
text = "";
}
if ("line".equals(type)) {
htmlBuilder.append(
"<hr style='margin:0;border:none;border-top:1px solid #333;height:0;width:100%;'/>");
} 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(
"<div style='width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:hidden;'>");
htmlBuilder.append("<img src=\"").append(imageData.replace("\"", "%22"))
.append("\" style='max-width:100%;max-height:100%;object-fit:contain;' alt='Bild' />");
htmlBuilder.append("</div>");
} else {
htmlBuilder.append(
"<div style='width:100%;height:100%;background:#f0f0f0;display:flex;align-items:center;justify-content:center;font-size:10pt;color:#666;'>[Bild]</div>");
}
} else if ("services.list".equals(variable)) {
// Render services list as a table with actual data
htmlBuilder.append(generateServicesTableHtmlWithData(mmWidth, variables));
} else {
htmlBuilder.append("<span style='white-space:nowrap;'>").append(text).append("</span>");
}
htmlBuilder.append("</div>");
}
System.out.println("DEBUG: Processed " + elementCount + " elements");
} else {
System.out.println("DEBUG: No elements found in template!");
}
htmlBuilder.append("</body></html>");
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<String, String> 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("<div style='width:100%;box-sizing:border-box;'>");
// Table
html.append("<table style='width:100%;border-collapse:collapse;font-size:inherit;table-layout:fixed;'>");
// Header row
html.append("<tr style='background-color:#f5f5f5;border-bottom:1px solid #cccccc;'>");
html.append("<th style='text-align:left;padding:4px 8px;font-weight:bold;width:55%;white-space:nowrap;'>Name</th>");
html.append("<th style='text-align:right;padding:4px 8px;font-weight:bold;width:20%;white-space:nowrap;'>Steuersatz</th>");
html.append("<th style='text-align:right;padding:4px 8px;font-weight:bold;width:25%;white-space:nowrap;'>Nettobetrag</th>");
html.append("</tr>");
// 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("<tr style='").append(bgColor).append("border-bottom:1px solid #eeeeee;'>");
html.append("<td style='text-align:left;padding:4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'>").append(serviceData[i][0]).append("</td>");
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(serviceData[i][1]).append("</td>");
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(serviceData[i][2]).append("</td>");
html.append("</tr>");
}
html.append("</table>");
// Summary section with actual totals from variables
html.append("<div style='margin-top:8px;width:100%;'>");
html.append("<table style='width:100%;border-collapse:collapse;font-size:inherit;table-layout:fixed;'>");
// Extract numeric value from VAT rate for display
String vatPercent = vatRate.replace(" %", "").replace("%", "");
// Nettosumme
html.append("<tr>");
html.append("<td style='width:55%;padding:2px 0;'></td>");
html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>Nettosumme:</td>");
html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>").append(netTotal).append("</td>");
html.append("</tr>");
// Umsatzsteuer
html.append("<tr>");
html.append("<td style='width:55%;padding:2px 0;'></td>");
html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>zzgl. ").append(vatPercent).append("% USt:</td>");
html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>").append(vatTotal).append("</td>");
html.append("</tr>");
// Gesamtsumme
html.append("<tr>");
html.append("<td style='width:55%;padding:2px 0;'></td>");
html.append("<td style='width:20%;text-align:left;padding:4px 8px;white-space:nowrap;font-weight:bold;font-size:1.05em;'>Gesamtsumme:</td>");
html.append("<td style='width:25%;text-align:right;padding:4px 8px;white-space:nowrap;font-weight:bold;font-size:1.05em;'>").append(grossTotal).append("</td>");
html.append("</tr>");
html.append("</table>");
html.append("</div>");
html.append("</div>");
return html.toString();
}
}