diff --git a/src/main/bundles/dev.bundle b/src/main/bundles/dev.bundle index 0fc7d9c..44e56d7 100644 Binary files a/src/main/bundles/dev.bundle and b/src/main/bundles/dev.bundle differ diff --git a/src/main/frontend/invoice-generator/profile-invoice-generator.js b/src/main/frontend/invoice-generator/profile-invoice-generator.js index a27fb6c..3f18533 100644 --- a/src/main/frontend/invoice-generator/profile-invoice-generator.js +++ b/src/main/frontend/invoice-generator/profile-invoice-generator.js @@ -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); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java b/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java index 83379db..8dade16 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java @@ -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"); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java index 4b549a6..c6961fb 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java @@ -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; + }; + } + } diff --git a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java index 3e30e14..429c86f 100644 --- a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java +++ b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java @@ -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(""); htmlBuilder.append(""); + // Prepare variable substitution map + java.util.Map 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("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'"); + // 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("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } else { + text = ""; + } if ("line".equals(type)) { htmlBuilder.append("
"); @@ -366,4 +432,8 @@ public class CustomerInvoiceService { // Generate PDF from HTML return generatePdfFromHtmlString(htmlBuilder.toString()); } + + private String safe(String value) { + return value != null ? value : ""; + } }