Erweiterungen

This commit is contained in:
2026-02-13 16:38:57 +01:00
parent ceffef2332
commit d5cf0f53ba
5 changed files with 348 additions and 45 deletions

Binary file not shown.

View File

@@ -89,7 +89,8 @@ window.initProfileInvoiceGenerator = function() {
el.color || '#333333',
el.width || 100,
el.height || 30,
el.isStatic || false
el.isStatic || false,
el.variable || null
);
}
}
@@ -102,8 +103,10 @@ window.initProfileInvoiceGenerator = function() {
// Draw function
function draw() {
console.log('draw() called, elements:', elements.length);
var w = canvas.width;
var h = canvas.height;
console.log('Canvas size:', w, 'x', h);
// Clear background
ctx.fillStyle = '#e8e8e8';
@@ -141,7 +144,9 @@ window.initProfileInvoiceGenerator = function() {
}
// Draw elements
console.log('Drawing', elements.length, 'elements');
elements.forEach(function(el) {
console.log('Drawing element:', el.id, 'at', el.x, el.y, 'text:', el.text);
drawElement(el);
});
@@ -236,9 +241,6 @@ window.initProfileInvoiceGenerator = function() {
}
} else {
// Text elements
ctx.font = (el.fontStyle || '') + ' ' + fontSize + 'px Arial';
ctx.fillStyle = el.color || '#333333';
var lines = (el.text || '').split('\n');
var lineHeight = fontSize * 1.2;
var totalTextHeight = lines.length * lineHeight;
@@ -249,11 +251,17 @@ window.initProfileInvoiceGenerator = function() {
// Draw background highlight for static elements
if (el.isStatic) {
var textWidth = ctx.measureText(el.text || '').width + (10 * zoomFactor);
ctx.fillStyle = 'rgba(25, 118, 210, 0.1)'; // Light blue background
// Different background colors: green for customer, blue for masterdata
if (el.isCustomer) {
ctx.fillStyle = 'rgba(46, 204, 113, 0.15)'; // Light green for customer
} else {
ctx.fillStyle = 'rgba(25, 118, 210, 0.1)'; // Light blue for masterdata
}
ctx.fillRect(x - (3 * zoomFactor), y - (2 * zoomFactor), Math.max(w, textWidth), h);
}
ctx.fillStyle = el.color || '#333333';
// Always use black text for static elements, otherwise use element color
ctx.fillStyle = el.isStatic ? '#000000' : (el.color || '#333333');
ctx.font = (el.fontStyle || '') + ' ' + fontSize + 'px Arial';
ctx.textBaseline = 'top';
@@ -612,7 +620,7 @@ window.initProfileInvoiceGenerator = function() {
});
// Add element function
window.addProfileElement = function(type, label, dropX, dropY, isStatic, staticText) {
window.addProfileElement = function(type, label, dropX, dropY, isStatic, staticText, variable, isCustomer) {
elementCounter++;
var id = 'element-' + elementCounter;
@@ -633,7 +641,9 @@ window.initProfileInvoiceGenerator = function() {
height: 30,
fontSize: 14,
color: '#333333',
isStatic: isStatic || false
isStatic: isStatic || false,
variable: variable || null,
isCustomer: isCustomer || false
};
// Helper function to calculate height based on font size and lines
@@ -642,10 +652,10 @@ window.initProfileInvoiceGenerator = function() {
return Math.round(lineCount * lineHeight + 6);
}
// Handle static elements (user data)
// Handle static elements (user data or customer data)
if (isStatic && staticText) {
el.text = staticText;
el.color = '#1976d2'; // Blue color for static elements
el.color = '#000000'; // Black text for static elements
el.fontStyle = 'bold';
el.height = calculateHeight(el.fontSize, 1);
} else {
@@ -843,10 +853,22 @@ window.initProfileInvoiceGenerator = function() {
saveState();
// Convert pixel values to percentages for storage
var elementsWithPercent = elements.map(function(el) {
// For static elements with variables, handle text storage differently:
// - masterdata variables: text is resolved from user data, don't store
// - customer variables: store the placeholder text from canvas
var textToStore = el.text;
if (el.isStatic && el.variable) {
if (el.variable.startsWith('masterdata.')) {
// Don't store the text for masterdata variables - will be resolved from user data
textToStore = null;
}
// For customer variables, keep the text (placeholder) as is
}
return {
id: el.id,
type: el.type,
text: el.text,
text: textToStore,
xPercent: toPercentX(el.x),
yPercent: toPercentY(el.y),
widthPercent: toPercentX(el.width),
@@ -855,6 +877,8 @@ window.initProfileInvoiceGenerator = function() {
fontStyle: el.fontStyle,
color: el.color,
isStatic: el.isStatic,
isCustomer: el.isCustomer,
variable: el.variable,
imageData: el.imageData
};
});
@@ -899,27 +923,37 @@ window.initProfileInvoiceGenerator = function() {
var templateLabel = e.dataTransfer.getData('template-label');
var isStatic = e.dataTransfer.getData('is-static') === 'true';
var staticText = e.dataTransfer.getData('static-text');
var variable = e.dataTransfer.getData('variable');
var isCustomer = e.dataTransfer.getData('is-customer') === 'true';
if (templateType && window.addProfileElement) {
var rect = container.getBoundingClientRect();
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
window.addProfileElement(templateType, templateLabel, x, y, isStatic, staticText);
window.addProfileElement(templateType, templateLabel, x, y, isStatic, staticText, variable, isCustomer);
}
});
}
// Load template function
// Load template function - defined INSIDE initProfileInvoiceGenerator to access closure variables
window.loadProfileTemplate = function(templateData) {
try {
var data = JSON.parse(templateData);
console.log('loadProfileTemplate called with data:', JSON.stringify(templateData).substring(0, 200));
var data = (typeof templateData === 'string') ? JSON.parse(templateData) : templateData;
if (data.elements && Array.isArray(data.elements)) {
// Clear existing elements
elements = [];
console.log('Loading ' + data.elements.length + ' elements');
console.log('Current elements array before clear:', elements.length);
// Clear existing elements - use length = 0 to keep the same array reference
elements.length = 0;
selectedElement = null;
console.log('Elements array after clear:', elements.length);
// Master data mapping - these should be filled from user data
var masterdata = window.masterdataValues || {};
// Load new elements
data.elements.forEach(function(el) {
console.log('Loading element:', el.id, 'type:', el.type, 'variable:', el.variable);
// Ensure element has an ID
if (!el.id) {
elementCounter++;
@@ -940,7 +974,69 @@ window.initProfileInvoiceGenerator = function() {
el.height = fromPercentY(el.heightPercent);
}
// Handle elements with variables
if (el.variable) {
el.isStatic = true;
if (el.variable.startsWith('customer.')) {
el.isCustomer = true;
// Customer variables - use saved text if available, otherwise use placeholder
if (!el.text || el.text.trim() === '') {
el.text = '[' + el.variable.replace('customer.', '') + ']';
}
// If text is already set (from saved template), keep it
} else if (el.variable.startsWith('masterdata.')) {
// Masterdata variables - use actual value from masterdata or placeholder
var value = masterdata[el.variable];
if (value && value.trim() !== '') {
el.text = value;
} else {
el.text = '[' + el.variable.replace('masterdata.', '') + ']';
}
}
}
// Legacy: If text contains a variable placeholder {{variable}}|Display Text, extract it
else if (el.text && el.text.startsWith('{{')) {
var pipeIndex = el.text.indexOf('|');
if (pipeIndex > -1) {
// Format: {{variable}}|Display Text
var placeholderPart = el.text.substring(0, pipeIndex);
var displayText = el.text.substring(pipeIndex + 1);
if (placeholderPart.startsWith('{{') && placeholderPart.endsWith('}}')) {
var varName = placeholderPart.substring(2, placeholderPart.length - 2);
el.variable = varName;
if (varName.startsWith('masterdata.')) {
el.isStatic = true;
// Use masterdata value or fallback to display text
var value = masterdata[varName];
el.text = (value && value.trim() !== '') ? value : displayText;
} else if (varName.startsWith('customer.')) {
el.isStatic = true;
el.isCustomer = true;
// Use saved text if available, otherwise use placeholder
if (!el.text || el.text.trim() === '' || el.text.startsWith('{{')) {
el.text = '[' + varName.replace('customer.', '') + ']';
}
}
}
} else if (el.text.endsWith('}}')) {
// Legacy format: {{variable}} without display text
var varName = el.text.substring(2, el.text.length - 2);
el.variable = varName;
if (varName.startsWith('masterdata.')) {
el.isStatic = true;
var value = masterdata[varName];
el.text = (value && value.trim() !== '') ? value : '[' + varName.replace('masterdata.', '') + ']';
} else if (varName.startsWith('customer.')) {
el.isStatic = true;
el.isCustomer = true;
el.text = '[' + varName.replace('customer.', '') + ']';
}
}
}
console.log('Element processed:', el.id, 'x:', el.x, 'y:', el.y, 'text:', el.text);
elements.push(el);
console.log('Element pushed, elements count now:', elements.length);
});
// Update counter to be higher than any loaded element
@@ -956,9 +1052,14 @@ window.initProfileInvoiceGenerator = function() {
// Save to global state
saveState();
console.log('Calling draw(), elements count:', elements.length);
console.log('Canvas dimensions:', canvas.width, 'x', canvas.height);
draw();
console.log('draw() completed');
notifyElementDeselected();
console.log('Template loaded with ' + elements.length + ' elements');
} else {
console.log('No elements array found in template data');
}
} catch (e) {
console.error('Error loading template:', e);

View File

@@ -100,7 +100,7 @@ public class AuthenticatedStartView extends VerticalLayout {
featuresGrid.getStyle().set("display", "flex");
featuresGrid.getStyle().set("flex-wrap", "wrap");
featuresGrid.getStyle().set("justify-content", "center");
featuresGrid.getStyle().set("align-items", "flex-start");
featuresGrid.getStyle().set("align-items", "stretch");
featuresGrid.getStyle().set("gap", "var(--lumo-space-l)");
featuresGrid.getStyle().set("width", "100%");
@@ -128,6 +128,7 @@ public class AuthenticatedStartView extends VerticalLayout {
card.setWidth("300px");
card.setMinWidth("300px");
card.setMaxWidth("300px");
card.setHeight("100%");
card.getStyle().set("flex-grow", "0");
card.getStyle().set("flex-shrink", "0");

View File

@@ -800,7 +800,7 @@ public class EditProfileView extends HorizontalLayout {
} else {
templateData = result.toString();
}
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData);
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData, currentUser);
showPdfInDialog(pdfBytes);
} catch (Exception ex) {
Notification.show("Fehler beim Generieren der Vorschau: " + ex.getMessage(), 3000, Notification.Position.BOTTOM_CENTER);
@@ -868,34 +868,49 @@ public class EditProfileView extends HorizontalLayout {
panel.setSpacing(true);
panel.setHeightFull();
// Bereich 1: Rechnungselemente (Stammdaten)
Span invoiceHeader = new Span("Rechnungselemente");
// Bereich 1: Meine Stammdaten (Variablen)
Span invoiceHeader = new Span("Meine Stammdaten (Variablen)");
invoiceHeader.getStyle()
.set("font-weight", "bold")
.set("font-size", "var(--lumo-font-size-m)")
.set("margin-top", "var(--lumo-space-s)");
// Stammdaten des Benutzers als unveränderbare Elemente
// Stammdaten als Variablen - mit tatsächlichen Werten aus currentUser
String company = safe(currentUser.getCompany());
String fullName = safe(currentUser.getFirstname()) + " " + safe(currentUser.getName());
String street = safe(currentUser.getStreet()) + " " + safe(currentUser.getHouseNumber());
String city = safe(currentUser.getZip()) + " " + safe(currentUser.getCity());
String email = safe(currentUser.getEmail());
String phone = safe(currentUser.getPhone());
Div senderCompany = createStaticTemplate("Firma", VaadinIcon.OFFICE, "sender_company",
Div senderCompany = createVariableTemplate("Firma", VaadinIcon.OFFICE, "masterdata.company_name",
company.isEmpty() ? "Ihre Firma" : company);
Div senderName = createStaticTemplate("Name", VaadinIcon.USER, "sender_name",
Div senderName = createVariableTemplate("Name", VaadinIcon.USER, "masterdata.contact_name",
fullName.trim().isEmpty() ? "Ihr Name" : fullName.trim());
Div senderAddress = createStaticTemplate("Adresse", VaadinIcon.MAP_MARKER, "sender_address",
Div senderAddress = createVariableTemplate("Straße", VaadinIcon.MAP_MARKER, "masterdata.street",
street.trim().isEmpty() ? "Ihre Straße" : street.trim());
Div senderCity = createStaticTemplate("Ort", VaadinIcon.BUILDING, "sender_city",
Div senderCity = createVariableTemplate("Ort", VaadinIcon.BUILDING, "masterdata.city",
city.trim().isEmpty() ? "PLZ Ort" : city.trim());
Div senderEmail = createStaticTemplate("E-Mail", VaadinIcon.ENVELOPE, "sender_email",
Div senderEmail = createVariableTemplate("E-Mail", VaadinIcon.ENVELOPE, "masterdata.email",
email.isEmpty() ? "ihre@email.de" : email);
Div senderPhone = createStaticTemplate("Telefon", VaadinIcon.PHONE, "sender_phone",
Div senderPhone = createVariableTemplate("Telefon", VaadinIcon.PHONE, "masterdata.phone",
phone.isEmpty() ? "Ihre Telefonnummer" : phone);
// Bereich 2: Kundendaten (Variablen)
Span customerHeader = new Span("Kundendaten (Variablen)");
customerHeader.getStyle()
.set("font-weight", "bold")
.set("font-size", "var(--lumo-font-size-m)")
.set("margin-top", "var(--lumo-space-m)");
// Kundendaten als Variablen (grün hinterlegt)
Div customerCompany = createCustomerVariableTemplate("Kunde Firma", VaadinIcon.OFFICE, "customer.company_name", "Kundenfirma GmbH");
Div customerName = createCustomerVariableTemplate("Kunde Name", VaadinIcon.USER, "customer.contact_name", "Erika Mustermann");
Div customerAddress = createCustomerVariableTemplate("Kunde Straße", VaadinIcon.MAP_MARKER, "customer.street", "Kundenstraße 456");
Div customerCity = createCustomerVariableTemplate("Kunde Ort", VaadinIcon.BUILDING, "customer.city", "54321 Kundenstadt");
Div customerEmail = createCustomerVariableTemplate("Kunde E-Mail", VaadinIcon.ENVELOPE, "customer.email", "kunde@beispiel.de");
Div customerPhone = createCustomerVariableTemplate("Kunde Telefon", VaadinIcon.PHONE, "customer.phone", "0987 654321");
// Bereich 2: Freie Elemente
Span freeHeader = new Span("Freie Elemente");
freeHeader.getStyle()
@@ -916,6 +931,8 @@ public class EditProfileView extends HorizontalLayout {
panel.add(
invoiceHeader,
senderCompany, senderName, senderAddress, senderCity, senderEmail, senderPhone,
customerHeader,
customerCompany, customerName, customerAddress, customerCity, customerEmail, customerPhone,
freeHeader,
textBlock, headerBlock, dateBlock, customerBlock, companyBlock, amountBlock, lineBlock, imageBlock
);
@@ -923,37 +940,87 @@ public class EditProfileView extends HorizontalLayout {
return panel;
}
private Div createStaticTemplate(String label, VaadinIcon icon, String type, String defaultText) {
private Div createVariableTemplate(String label, VaadinIcon icon, String variable, String defaultText) {
Div template = new Div();
template.setText(label);
template.getStyle()
.set("padding", "var(--lumo-space-s)")
.set("margin", "var(--lumo-space-xs) 0")
.set("background-color", "var(--lumo-contrast-10pct)")
.set("border", "1px solid var(--lumo-contrast-30pct)")
.set("background-color", "rgba(25, 118, 210, 0.1)")
.set("border", "1px solid rgba(25, 118, 210, 0.3)")
.set("border-radius", "var(--lumo-border-radius-m)")
.set("cursor", "grab")
.set("display", "flex")
.set("align-items", "center")
.set("gap", "var(--lumo-space-s)")
.set("user-select", "none")
.set("font-size", "var(--lumo-font-size-s)");
.set("font-size", "var(--lumo-font-size-s)")
.set("color", "#1976d2");
Icon templateIcon = icon.create();
templateIcon.setSize("var(--lumo-icon-size-s)");
template.getElement().insertChild(0, templateIcon.getElement());
template.getElement().setAttribute("draggable", "true");
template.getElement().setAttribute("data-template-type", type);
template.getElement().setAttribute("data-template-type", "variable");
template.getElement().setAttribute("data-template-label", label);
template.getElement().setAttribute("data-variable", variable);
template.getElement().setAttribute("data-static-text", defaultText);
template.getElement().setAttribute("data-is-customer", "false");
template.getElement().executeJs(
"this.addEventListener('dragstart', function(e) {" +
" e.dataTransfer.setData('template-type', this.getAttribute('data-template-type'));" +
" e.dataTransfer.setData('template-label', this.getAttribute('data-template-label'));" +
" e.dataTransfer.setData('variable', this.getAttribute('data-variable'));" +
" e.dataTransfer.setData('static-text', this.getAttribute('data-static-text'));" +
" e.dataTransfer.setData('is-static', 'true');" +
" e.dataTransfer.setData('is-customer', this.getAttribute('data-is-customer'));" +
" this.style.opacity = '0.5';" +
"});" +
"this.addEventListener('dragend', function(e) {" +
" this.style.opacity = '1';" +
"});");
return template;
}
private Div createCustomerVariableTemplate(String label, VaadinIcon icon, String variable, String defaultText) {
Div template = new Div();
template.setText(label);
template.getStyle()
.set("padding", "var(--lumo-space-s)")
.set("margin", "var(--lumo-space-xs) 0")
.set("background-color", "rgba(46, 204, 113, 0.15)") // Friendly green
.set("border", "1px solid rgba(46, 204, 113, 0.4)")
.set("border-radius", "var(--lumo-border-radius-m)")
.set("cursor", "grab")
.set("display", "flex")
.set("align-items", "center")
.set("gap", "var(--lumo-space-s)")
.set("user-select", "none")
.set("font-size", "var(--lumo-font-size-s)")
.set("color", "#27ae60"); // Dark green text
Icon templateIcon = icon.create();
templateIcon.setSize("var(--lumo-icon-size-s)");
template.getElement().insertChild(0, templateIcon.getElement());
template.getElement().setAttribute("draggable", "true");
template.getElement().setAttribute("data-template-type", "variable");
template.getElement().setAttribute("data-template-label", label);
template.getElement().setAttribute("data-variable", variable);
template.getElement().setAttribute("data-static-text", defaultText);
template.getElement().setAttribute("data-is-customer", "true");
template.getElement().executeJs(
"this.addEventListener('dragstart', function(e) {" +
" e.dataTransfer.setData('template-type', this.getAttribute('data-template-type'));" +
" e.dataTransfer.setData('template-label', this.getAttribute('data-template-label'));" +
" e.dataTransfer.setData('variable', this.getAttribute('data-variable'));" +
" e.dataTransfer.setData('static-text', this.getAttribute('data-static-text'));" +
" e.dataTransfer.setData('is-static', 'true');" +
" e.dataTransfer.setData('is-customer', this.getAttribute('data-is-customer'));" +
" this.style.opacity = '0.5';" +
"});" +
"this.addEventListener('dragend', function(e) {" +
@@ -1024,7 +1091,7 @@ public class EditProfileView extends HorizontalLayout {
@ClientCallable
public void updatePropertiesPanel(String elementId, String elementType, String text, Double x, Double y,
Integer fontSize, String color, Double width, Double height, Boolean isStatic) {
Integer fontSize, String color, Double width, Double height, Boolean isStatic, String variable) {
getUI().ifPresent(ui -> ui.access(() -> {
propertiesPanelProfile.removeAll();
@@ -1034,10 +1101,30 @@ public class EditProfileView extends HorizontalLayout {
.set("font-size", "var(--lumo-font-size-l)");
// Element Typ Anzeige
Span typeLabel = new Span("Typ: " + elementType + (Boolean.TRUE.equals(isStatic) ? " (Stammdaten)" : ""));
String typeDisplay = "Typ: " + elementType;
if (variable != null && !variable.isEmpty()) {
typeDisplay += " (Variable)";
} else if (Boolean.TRUE.equals(isStatic)) {
typeDisplay += " (Stammdaten)";
}
Span typeLabel = new Span(typeDisplay);
typeLabel.getStyle().set("font-size", "var(--lumo-font-size-s)");
propertiesPanelProfile.add(header, typeLabel);
// Variable anzeigen wenn vorhanden
if (variable != null && !variable.isEmpty()) {
TextField variableField = new TextField("Variable");
variableField.setValue(variable);
variableField.setReadOnly(true);
variableField.setWidthFull();
// Beschreibung basierend auf der Variable
String description = getVariableDescription(variable);
variableField.setHelperText(description);
propertiesPanelProfile.add(variableField);
}
// Für Bildelemente: Upload-Button anzeigen
if ("image".equals(elementType)) {
@@ -1250,15 +1337,38 @@ public class EditProfileView extends HorizontalLayout {
String templateData = optionalTemplate.get().getTemplateData();
if (templateData != null && !templateData.isEmpty()) {
System.out.println("Loading template data: " + templateData.substring(0, Math.min(100, templateData.length())) + "...");
// Escape the JSON string for JavaScript
String escapedJson = templateData
.replace("\\", "\\\\")
.replace("'", "\\'")
.replace("\n", "\\n")
.replace("\r", "\\r");
// Build masterdata values JSON
String company = safe(currentUser.getCompany());
String fullName = safe(currentUser.getFirstname()) + " " + safe(currentUser.getName());
String street = safe(currentUser.getStreet()) + " " + safe(currentUser.getHouseNumber());
String city = safe(currentUser.getZip()) + " " + safe(currentUser.getCity());
String email = safe(currentUser.getEmail());
String phone = safe(currentUser.getPhone());
String masterdataJson = "{" +
"'masterdata.company_name': '" + company.replace("'", "\\'") + "'," +
"'masterdata.contact_name': '" + fullName.replace("'", "\\'") + "'," +
"'masterdata.street': '" + street.replace("'", "\\'") + "'," +
"'masterdata.city': '" + city.replace("'", "\\'") + "'," +
"'masterdata.email': '" + email.replace("'", "\\'") + "'," +
"'masterdata.phone': '" + phone.replace("'", "\\'") + "'" +
"}";
getElement().executeJs(
"setTimeout(function() { " +
" if (window.loadProfileTemplate && document.getElementById('invoice-canvas-container-profile')) { " +
" console.log('Loading template into canvas...'); " +
" window.loadProfileTemplate($0); " +
" window.masterdataValues = " + masterdataJson + "; " +
" var templateData = JSON.parse('" + escapedJson + "'); " +
" window.loadProfileTemplate(templateData); " +
" } else { " +
" console.error('loadProfileTemplate or canvas not available'); " +
" } " +
"}, 300);", templateData);
"}, 300);");
}
} else {
System.out.println("No template found for user: " + currentUser.getId());
@@ -1269,4 +1379,25 @@ public class EditProfileView extends HorizontalLayout {
}
}
/**
* Get a human-readable description for a variable
*/
private String getVariableDescription(String variable) {
return switch (variable) {
case "masterdata.company_name" -> "Name Ihrer Firma";
case "masterdata.contact_name" -> "Name des Ansprechpartners";
case "masterdata.street" -> "Straße und Hausnummer";
case "masterdata.city" -> "PLZ und Ort";
case "masterdata.email" -> "E-Mail-Adresse";
case "masterdata.phone" -> "Telefonnummer";
case "customer.company_name" -> "Firmenname des Kunden";
case "customer.contact_name" -> "Name des Kundenansprechpartners";
case "customer.street" -> "Straße des Kunden";
case "customer.city" -> "PLZ und Ort des Kunden";
case "customer.email" -> "E-Mail des Kunden";
case "customer.phone" -> "Telefon des Kunden";
default -> "Variable: " + variable;
};
}
}

View File

@@ -248,6 +248,10 @@ public class CustomerInvoiceService {
* Creates an HTML representation of the canvas elements and converts it to PDF.
*/
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData) throws Exception {
return generatePdfFromCanvasTemplate(jsonTemplateData, null);
}
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData, de.assecutor.votianlt.model.User user) throws Exception {
// Parse the JSON template data
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonTemplateData);
@@ -269,10 +273,48 @@ public class CustomerInvoiceService {
htmlBuilder.append("</style>");
htmlBuilder.append("</head><body>");
// Prepare variable substitution map
java.util.Map<String, String> variables = new java.util.HashMap<>();
if (user != null) {
// Use actual user data
String company = user.getCompany();
variables.put("masterdata.company_name", (company != null && !company.isEmpty()) ? company : "Ihre Firma");
variables.put("masterdata.contact_name", safe(user.getFirstname()) + " " + safe(user.getName()));
variables.put("masterdata.street", safe(user.getStreet()) + " " + safe(user.getHouseNumber()));
variables.put("masterdata.city", safe(user.getZip()) + " " + safe(user.getCity()));
variables.put("masterdata.email", safe(user.getEmail()));
variables.put("masterdata.phone", safe(user.getPhone()));
} else {
// Default values for preview without user
variables.put("masterdata.company_name", "Meine Firma GmbH");
variables.put("masterdata.contact_name", "Max Mustermann");
variables.put("masterdata.street", "Musterstraße 123");
variables.put("masterdata.city", "12345 Musterstadt");
variables.put("masterdata.email", "kontakt@firma.de");
variables.put("masterdata.phone", "0123 456789");
}
// Customer data (placeholder for now - would come from job/customer selection)
variables.put("customer.company_name", "Kundenfirma GmbH");
variables.put("customer.contact_name", "Erika Mustermann");
variables.put("customer.street", "Kundenstraße 456");
variables.put("customer.city", "54321 Kundenstadt");
variables.put("customer.email", "kunde@beispiel.de");
variables.put("customer.phone", "0987 654321");
if (elements != null && elements.isArray()) {
for (com.fasterxml.jackson.databind.JsonNode element : elements) {
String type = element.has("type") ? element.get("type").asText("text") : "text";
String text = element.has("text") ? element.get("text").asText("") : "";
String variable = element.has("variable") ? element.get("variable").asText(null) : null;
// For static elements with variables, use the variable to get the text
// Otherwise use the stored text
String text;
if (variable != null && !variable.isEmpty()) {
text = ""; // Will be replaced by variable value below
} else {
text = element.has("text") ? element.get("text").asText("") : "";
}
// Use percentage values if available, otherwise fall back to legacy pixel values
double xPercent, yPercent, widthPercent, heightPercent;
@@ -329,12 +371,36 @@ public class CustomerInvoiceService {
htmlBuilder.append("'");
htmlBuilder.append(">");
// Escape HTML special characters in text
text = text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#x27;");
// Replace variables with actual values FIRST
if (variable != null && variables.containsKey(variable)) {
text = variables.get(variable);
} else if (text != null && text.startsWith("{{")) {
// Handle variable placeholder format {{variable}}|Display Text or {{variable}}
int pipeIndex = text.indexOf('|');
String placeholderPart;
if (pipeIndex > -1) {
placeholderPart = text.substring(0, pipeIndex);
} else {
placeholderPart = text;
}
if (placeholderPart.startsWith("{{") && placeholderPart.endsWith("}}")) {
String varName = placeholderPart.substring(2, placeholderPart.length() - 2);
if (variables.containsKey(varName)) {
text = variables.get(varName);
}
}
}
// Escape HTML special characters in text AFTER variable replacement
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%;'/>");
@@ -366,4 +432,8 @@ public class CustomerInvoiceService {
// Generate PDF from HTML
return generatePdfFromHtmlString(htmlBuilder.toString());
}
private String safe(String value) {
return value != null ? value : "";
}
}