Erweiterungen
This commit is contained in:
@@ -18,9 +18,14 @@ import com.vaadin.flow.router.BeforeEvent;
|
|||||||
import com.vaadin.flow.router.HasUrlParameter;
|
import com.vaadin.flow.router.HasUrlParameter;
|
||||||
import de.assecutor.votianlt.model.Job;
|
import de.assecutor.votianlt.model.Job;
|
||||||
import de.assecutor.votianlt.model.Service;
|
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.JobRepository;
|
||||||
import de.assecutor.votianlt.repository.ServiceRepository;
|
import de.assecutor.votianlt.repository.ServiceRepository;
|
||||||
|
import de.assecutor.votianlt.repository.UserRepository;
|
||||||
import de.assecutor.votianlt.security.SecurityService;
|
import de.assecutor.votianlt.security.SecurityService;
|
||||||
|
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
||||||
|
import de.assecutor.votianlt.service.InvoiceTemplateService;
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.bson.types.ObjectId;
|
import org.bson.types.ObjectId;
|
||||||
@@ -29,7 +34,15 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
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")
|
@PageTitle("Rechnung erstellen")
|
||||||
@Route(value = "create_invoice", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
@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 JobRepository jobRepository;
|
||||||
private final ServiceRepository serviceRepository;
|
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 Job currentJob;
|
||||||
private List<ServiceRow> gridRows = new ArrayList<>();
|
private List<ServiceRow> gridRows = new ArrayList<>();
|
||||||
private Grid<ServiceRow> servicesGrid;
|
private Grid<ServiceRow> servicesGrid;
|
||||||
@@ -73,9 +91,14 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public CreateInvoiceView(JobRepository jobRepository, ServiceRepository serviceRepository,
|
public CreateInvoiceView(JobRepository jobRepository, ServiceRepository serviceRepository,
|
||||||
SecurityService securityService) {
|
UserRepository userRepository, CustomerInvoiceService customerInvoiceService,
|
||||||
|
InvoiceTemplateService invoiceTemplateService, SecurityService securityService) {
|
||||||
this.jobRepository = jobRepository;
|
this.jobRepository = jobRepository;
|
||||||
this.serviceRepository = serviceRepository;
|
this.serviceRepository = serviceRepository;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.customerInvoiceService = customerInvoiceService;
|
||||||
|
this.invoiceTemplateService = invoiceTemplateService;
|
||||||
|
this.securityService = securityService;
|
||||||
setSizeFull();
|
setSizeFull();
|
||||||
setPadding(true);
|
setPadding(true);
|
||||||
setSpacing(true);
|
setSpacing(true);
|
||||||
@@ -376,16 +399,175 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
|||||||
// Save the updated job with kilometers and time
|
// Save the updated job with kilometers and time
|
||||||
jobRepository.save(currentJob);
|
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 netAmount = calculateNetAmount();
|
||||||
BigDecimal vatRate = calculateAverageVatRate();
|
BigDecimal vatRate = calculateAverageVatRate();
|
||||||
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
BigDecimal vatAmount = netAmount.multiply(vatRate);
|
||||||
BigDecimal totalAmount = netAmount.add(vatAmount);
|
BigDecimal totalAmount = netAmount.add(vatAmount);
|
||||||
|
|
||||||
String message = String.format("Rechnung erstellt! Nettosumme: %s €, MwSt: %s €, Gesamt: %s €",
|
// Parse the template and replace variables
|
||||||
netAmount.setScale(2, RoundingMode.HALF_UP), vatAmount.setScale(2, RoundingMode.HALF_UP),
|
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
totalAmount.setScale(2, RoundingMode.HALF_UP));
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import com.itextpdf.html2pdf.HtmlConverter;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
@@ -528,4 +529,281 @@ public class CustomerInvoiceService {
|
|||||||
|
|
||||||
return html.toString();
|
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("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
|
||||||
|
.replace("'", "'");
|
||||||
|
} 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user