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.color || '#333333',
el.width || 100, el.width || 100,
el.height || 30, el.height || 30,
el.isStatic || false el.isStatic || false,
el.variable || null
); );
} }
} }
@@ -102,8 +103,10 @@ window.initProfileInvoiceGenerator = function() {
// Draw function // Draw function
function draw() { function draw() {
console.log('draw() called, elements:', elements.length);
var w = canvas.width; var w = canvas.width;
var h = canvas.height; var h = canvas.height;
console.log('Canvas size:', w, 'x', h);
// Clear background // Clear background
ctx.fillStyle = '#e8e8e8'; ctx.fillStyle = '#e8e8e8';
@@ -141,7 +144,9 @@ window.initProfileInvoiceGenerator = function() {
} }
// Draw elements // Draw elements
console.log('Drawing', elements.length, 'elements');
elements.forEach(function(el) { elements.forEach(function(el) {
console.log('Drawing element:', el.id, 'at', el.x, el.y, 'text:', el.text);
drawElement(el); drawElement(el);
}); });
@@ -236,9 +241,6 @@ window.initProfileInvoiceGenerator = function() {
} }
} else { } else {
// Text elements // Text elements
ctx.font = (el.fontStyle || '') + ' ' + fontSize + 'px Arial';
ctx.fillStyle = el.color || '#333333';
var lines = (el.text || '').split('\n'); var lines = (el.text || '').split('\n');
var lineHeight = fontSize * 1.2; var lineHeight = fontSize * 1.2;
var totalTextHeight = lines.length * lineHeight; var totalTextHeight = lines.length * lineHeight;
@@ -249,11 +251,17 @@ window.initProfileInvoiceGenerator = function() {
// Draw background highlight for static elements // Draw background highlight for static elements
if (el.isStatic) { if (el.isStatic) {
var textWidth = ctx.measureText(el.text || '').width + (10 * zoomFactor); 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.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.font = (el.fontStyle || '') + ' ' + fontSize + 'px Arial';
ctx.textBaseline = 'top'; ctx.textBaseline = 'top';
@@ -612,7 +620,7 @@ window.initProfileInvoiceGenerator = function() {
}); });
// Add element 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++; elementCounter++;
var id = 'element-' + elementCounter; var id = 'element-' + elementCounter;
@@ -633,7 +641,9 @@ window.initProfileInvoiceGenerator = function() {
height: 30, height: 30,
fontSize: 14, fontSize: 14,
color: '#333333', 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 // Helper function to calculate height based on font size and lines
@@ -642,10 +652,10 @@ window.initProfileInvoiceGenerator = function() {
return Math.round(lineCount * lineHeight + 6); return Math.round(lineCount * lineHeight + 6);
} }
// Handle static elements (user data) // Handle static elements (user data or customer data)
if (isStatic && staticText) { if (isStatic && staticText) {
el.text = staticText; el.text = staticText;
el.color = '#1976d2'; // Blue color for static elements el.color = '#000000'; // Black text for static elements
el.fontStyle = 'bold'; el.fontStyle = 'bold';
el.height = calculateHeight(el.fontSize, 1); el.height = calculateHeight(el.fontSize, 1);
} else { } else {
@@ -843,10 +853,22 @@ window.initProfileInvoiceGenerator = function() {
saveState(); saveState();
// Convert pixel values to percentages for storage // Convert pixel values to percentages for storage
var elementsWithPercent = elements.map(function(el) { 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 { return {
id: el.id, id: el.id,
type: el.type, type: el.type,
text: el.text, text: textToStore,
xPercent: toPercentX(el.x), xPercent: toPercentX(el.x),
yPercent: toPercentY(el.y), yPercent: toPercentY(el.y),
widthPercent: toPercentX(el.width), widthPercent: toPercentX(el.width),
@@ -855,6 +877,8 @@ window.initProfileInvoiceGenerator = function() {
fontStyle: el.fontStyle, fontStyle: el.fontStyle,
color: el.color, color: el.color,
isStatic: el.isStatic, isStatic: el.isStatic,
isCustomer: el.isCustomer,
variable: el.variable,
imageData: el.imageData imageData: el.imageData
}; };
}); });
@@ -899,27 +923,37 @@ window.initProfileInvoiceGenerator = function() {
var templateLabel = e.dataTransfer.getData('template-label'); var templateLabel = e.dataTransfer.getData('template-label');
var isStatic = e.dataTransfer.getData('is-static') === 'true'; var isStatic = e.dataTransfer.getData('is-static') === 'true';
var staticText = e.dataTransfer.getData('static-text'); 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) { if (templateType && window.addProfileElement) {
var rect = container.getBoundingClientRect(); var rect = container.getBoundingClientRect();
var x = e.clientX - rect.left; var x = e.clientX - rect.left;
var y = e.clientY - rect.top; 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) { window.loadProfileTemplate = function(templateData) {
try { 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)) { if (data.elements && Array.isArray(data.elements)) {
// Clear existing elements console.log('Loading ' + data.elements.length + ' elements');
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; 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 // Load new elements
data.elements.forEach(function(el) { data.elements.forEach(function(el) {
console.log('Loading element:', el.id, 'type:', el.type, 'variable:', el.variable);
// Ensure element has an ID // Ensure element has an ID
if (!el.id) { if (!el.id) {
elementCounter++; elementCounter++;
@@ -940,7 +974,69 @@ window.initProfileInvoiceGenerator = function() {
el.height = fromPercentY(el.heightPercent); 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); elements.push(el);
console.log('Element pushed, elements count now:', elements.length);
}); });
// Update counter to be higher than any loaded element // Update counter to be higher than any loaded element
@@ -956,9 +1052,14 @@ window.initProfileInvoiceGenerator = function() {
// Save to global state // Save to global state
saveState(); saveState();
console.log('Calling draw(), elements count:', elements.length);
console.log('Canvas dimensions:', canvas.width, 'x', canvas.height);
draw(); draw();
console.log('draw() completed');
notifyElementDeselected(); notifyElementDeselected();
console.log('Template loaded with ' + elements.length + ' elements'); console.log('Template loaded with ' + elements.length + ' elements');
} else {
console.log('No elements array found in template data');
} }
} catch (e) { } catch (e) {
console.error('Error loading template:', 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("display", "flex");
featuresGrid.getStyle().set("flex-wrap", "wrap"); featuresGrid.getStyle().set("flex-wrap", "wrap");
featuresGrid.getStyle().set("justify-content", "center"); 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("gap", "var(--lumo-space-l)");
featuresGrid.getStyle().set("width", "100%"); featuresGrid.getStyle().set("width", "100%");
@@ -128,6 +128,7 @@ public class AuthenticatedStartView extends VerticalLayout {
card.setWidth("300px"); card.setWidth("300px");
card.setMinWidth("300px"); card.setMinWidth("300px");
card.setMaxWidth("300px"); card.setMaxWidth("300px");
card.setHeight("100%");
card.getStyle().set("flex-grow", "0"); card.getStyle().set("flex-grow", "0");
card.getStyle().set("flex-shrink", "0"); card.getStyle().set("flex-shrink", "0");

View File

@@ -800,7 +800,7 @@ public class EditProfileView extends HorizontalLayout {
} else { } else {
templateData = result.toString(); templateData = result.toString();
} }
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData); byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData, currentUser);
showPdfInDialog(pdfBytes); showPdfInDialog(pdfBytes);
} catch (Exception ex) { } catch (Exception ex) {
Notification.show("Fehler beim Generieren der Vorschau: " + ex.getMessage(), 3000, Notification.Position.BOTTOM_CENTER); Notification.show("Fehler beim Generieren der Vorschau: " + ex.getMessage(), 3000, Notification.Position.BOTTOM_CENTER);
@@ -868,14 +868,14 @@ public class EditProfileView extends HorizontalLayout {
panel.setSpacing(true); panel.setSpacing(true);
panel.setHeightFull(); panel.setHeightFull();
// Bereich 1: Rechnungselemente (Stammdaten) // Bereich 1: Meine Stammdaten (Variablen)
Span invoiceHeader = new Span("Rechnungselemente"); Span invoiceHeader = new Span("Meine Stammdaten (Variablen)");
invoiceHeader.getStyle() invoiceHeader.getStyle()
.set("font-weight", "bold") .set("font-weight", "bold")
.set("font-size", "var(--lumo-font-size-m)") .set("font-size", "var(--lumo-font-size-m)")
.set("margin-top", "var(--lumo-space-s)"); .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 company = safe(currentUser.getCompany());
String fullName = safe(currentUser.getFirstname()) + " " + safe(currentUser.getName()); String fullName = safe(currentUser.getFirstname()) + " " + safe(currentUser.getName());
String street = safe(currentUser.getStreet()) + " " + safe(currentUser.getHouseNumber()); String street = safe(currentUser.getStreet()) + " " + safe(currentUser.getHouseNumber());
@@ -883,19 +883,34 @@ public class EditProfileView extends HorizontalLayout {
String email = safe(currentUser.getEmail()); String email = safe(currentUser.getEmail());
String phone = safe(currentUser.getPhone()); 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); 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()); 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()); 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()); 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); 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); 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 // Bereich 2: Freie Elemente
Span freeHeader = new Span("Freie Elemente"); Span freeHeader = new Span("Freie Elemente");
freeHeader.getStyle() freeHeader.getStyle()
@@ -916,6 +931,8 @@ public class EditProfileView extends HorizontalLayout {
panel.add( panel.add(
invoiceHeader, invoiceHeader,
senderCompany, senderName, senderAddress, senderCity, senderEmail, senderPhone, senderCompany, senderName, senderAddress, senderCity, senderEmail, senderPhone,
customerHeader,
customerCompany, customerName, customerAddress, customerCity, customerEmail, customerPhone,
freeHeader, freeHeader,
textBlock, headerBlock, dateBlock, customerBlock, companyBlock, amountBlock, lineBlock, imageBlock textBlock, headerBlock, dateBlock, customerBlock, companyBlock, amountBlock, lineBlock, imageBlock
); );
@@ -923,37 +940,87 @@ public class EditProfileView extends HorizontalLayout {
return panel; 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(); Div template = new Div();
template.setText(label); template.setText(label);
template.getStyle() template.getStyle()
.set("padding", "var(--lumo-space-s)") .set("padding", "var(--lumo-space-s)")
.set("margin", "var(--lumo-space-xs) 0") .set("margin", "var(--lumo-space-xs) 0")
.set("background-color", "var(--lumo-contrast-10pct)") .set("background-color", "rgba(25, 118, 210, 0.1)")
.set("border", "1px solid var(--lumo-contrast-30pct)") .set("border", "1px solid rgba(25, 118, 210, 0.3)")
.set("border-radius", "var(--lumo-border-radius-m)") .set("border-radius", "var(--lumo-border-radius-m)")
.set("cursor", "grab") .set("cursor", "grab")
.set("display", "flex") .set("display", "flex")
.set("align-items", "center") .set("align-items", "center")
.set("gap", "var(--lumo-space-s)") .set("gap", "var(--lumo-space-s)")
.set("user-select", "none") .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(); Icon templateIcon = icon.create();
templateIcon.setSize("var(--lumo-icon-size-s)"); templateIcon.setSize("var(--lumo-icon-size-s)");
template.getElement().insertChild(0, templateIcon.getElement()); template.getElement().insertChild(0, templateIcon.getElement());
template.getElement().setAttribute("draggable", "true"); 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-template-label", label);
template.getElement().setAttribute("data-variable", variable);
template.getElement().setAttribute("data-static-text", defaultText); template.getElement().setAttribute("data-static-text", defaultText);
template.getElement().setAttribute("data-is-customer", "false");
template.getElement().executeJs( template.getElement().executeJs(
"this.addEventListener('dragstart', function(e) {" + "this.addEventListener('dragstart', function(e) {" +
" e.dataTransfer.setData('template-type', this.getAttribute('data-template-type'));" + " e.dataTransfer.setData('template-type', this.getAttribute('data-template-type'));" +
" e.dataTransfer.setData('template-label', this.getAttribute('data-template-label'));" + " 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('static-text', this.getAttribute('data-static-text'));" +
" e.dataTransfer.setData('is-static', 'true');" + " 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.style.opacity = '0.5';" +
"});" + "});" +
"this.addEventListener('dragend', function(e) {" + "this.addEventListener('dragend', function(e) {" +
@@ -1024,7 +1091,7 @@ public class EditProfileView extends HorizontalLayout {
@ClientCallable @ClientCallable
public void updatePropertiesPanel(String elementId, String elementType, String text, Double x, Double y, 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(() -> { getUI().ifPresent(ui -> ui.access(() -> {
propertiesPanelProfile.removeAll(); propertiesPanelProfile.removeAll();
@@ -1034,11 +1101,31 @@ public class EditProfileView extends HorizontalLayout {
.set("font-size", "var(--lumo-font-size-l)"); .set("font-size", "var(--lumo-font-size-l)");
// Element Typ Anzeige // 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)"); typeLabel.getStyle().set("font-size", "var(--lumo-font-size-s)");
propertiesPanelProfile.add(header, typeLabel); 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 // Für Bildelemente: Upload-Button anzeigen
if ("image".equals(elementType)) { if ("image".equals(elementType)) {
MemoryBuffer buffer = new MemoryBuffer(); MemoryBuffer buffer = new MemoryBuffer();
@@ -1250,15 +1337,38 @@ public class EditProfileView extends HorizontalLayout {
String templateData = optionalTemplate.get().getTemplateData(); String templateData = optionalTemplate.get().getTemplateData();
if (templateData != null && !templateData.isEmpty()) { if (templateData != null && !templateData.isEmpty()) {
System.out.println("Loading template data: " + templateData.substring(0, Math.min(100, templateData.length())) + "..."); 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( getElement().executeJs(
"setTimeout(function() { " + "setTimeout(function() { " +
" if (window.loadProfileTemplate && document.getElementById('invoice-canvas-container-profile')) { " + " if (window.loadProfileTemplate && document.getElementById('invoice-canvas-container-profile')) { " +
" console.log('Loading template into canvas...'); " + " console.log('Loading template into canvas...'); " +
" window.loadProfileTemplate($0); " + " window.masterdataValues = " + masterdataJson + "; " +
" var templateData = JSON.parse('" + escapedJson + "'); " +
" window.loadProfileTemplate(templateData); " +
" } else { " + " } else { " +
" console.error('loadProfileTemplate or canvas not available'); " + " console.error('loadProfileTemplate or canvas not available'); " +
" } " + " } " +
"}, 300);", templateData); "}, 300);");
} }
} else { } else {
System.out.println("No template found for user: " + currentUser.getId()); 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. * Creates an HTML representation of the canvas elements and converts it to PDF.
*/ */
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData) throws Exception { 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 // Parse the JSON template data
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonTemplateData); com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonTemplateData);
@@ -269,10 +273,48 @@ public class CustomerInvoiceService {
htmlBuilder.append("</style>"); htmlBuilder.append("</style>");
htmlBuilder.append("</head><body>"); 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()) { if (elements != null && elements.isArray()) {
for (com.fasterxml.jackson.databind.JsonNode element : elements) { for (com.fasterxml.jackson.databind.JsonNode element : elements) {
String type = element.has("type") ? element.get("type").asText("text") : "text"; 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 // Use percentage values if available, otherwise fall back to legacy pixel values
double xPercent, yPercent, widthPercent, heightPercent; double xPercent, yPercent, widthPercent, heightPercent;
@@ -329,12 +371,36 @@ public class CustomerInvoiceService {
htmlBuilder.append("'"); htmlBuilder.append("'");
htmlBuilder.append(">"); htmlBuilder.append(">");
// Escape HTML special characters in text // 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;") text = text.replace("&", "&amp;")
.replace("<", "&lt;") .replace("<", "&lt;")
.replace(">", "&gt;") .replace(">", "&gt;")
.replace("\"", "&quot;") .replace("\"", "&quot;")
.replace("'", "&#x27;"); .replace("'", "&#x27;");
} else {
text = "";
}
if ("line".equals(type)) { if ("line".equals(type)) {
htmlBuilder.append("<hr style='margin:0;border:none;border-top:1px solid #333;height:0;width:100%;'/>"); 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 // Generate PDF from HTML
return generatePdfFromHtmlString(htmlBuilder.toString()); return generatePdfFromHtmlString(htmlBuilder.toString());
} }
private String safe(String value) {
return value != null ? value : "";
}
} }