Erweiterungen

This commit is contained in:
2026-02-13 11:27:06 +01:00
parent 120a8e0571
commit 2da3d13431
6 changed files with 1491 additions and 141 deletions

Binary file not shown.

View File

@@ -4,7 +4,7 @@
'use strict'; 'use strict';
class InvoiceGenerator { class InvoiceGenerator {
constructor() { constructor(containerId) {
this.canvas = null; this.canvas = null;
this.ctx = null; this.ctx = null;
this.elements = new Map(); this.elements = new Map();
@@ -16,22 +16,33 @@
this.elementStart = { x: 0, y: 0, w: 0, h: 0 }; this.elementStart = { x: 0, y: 0, w: 0, h: 0 };
this.activeHandle = null; this.activeHandle = null;
this.container = null; this.container = null;
this.containerId = containerId || 'invoice-canvas-container';
// Page dimensions (A4 at 96 DPI) // Page dimensions (A4 at 96 DPI)
this.pageX = 50; this.pageX = 50;
this.pageY = 30; this.pageY = 30;
this.pageWidth = 794; this.pageWidth = 794;
this.pageHeight = 1123; this.pageHeight = 1123;
this.scale = 1;
// Handle configuration // Handle configuration
this.handleSize = 10; this.handleSize = 10;
this.handlePadding = 3; this.handlePadding = 3;
// Grid configuration (Snap to Grid)
this.gridSize = 5;
this.showGrid = true;
}
// Snap value to grid
snapToGrid(value) {
return Math.round(value / this.gridSize) * this.gridSize;
} }
init() { init() {
this.container = document.getElementById('invoice-canvas-container'); this.container = document.getElementById(this.containerId);
if (!this.container) { if (!this.container) {
console.error('Canvas container not found'); console.error('Canvas container not found: ' + this.containerId);
return; return;
} }
@@ -50,6 +61,10 @@
this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this)); this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this));
this.canvas.addEventListener('mouseleave', this.onMouseUp.bind(this)); this.canvas.addEventListener('mouseleave', this.onMouseUp.bind(this));
// Keyboard navigation for selected element
this.canvas.setAttribute('tabindex', '0');
this.canvas.addEventListener('keydown', this.onKeyDown.bind(this));
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
this.updateCanvasSize(); this.updateCanvasSize();
this.draw(); this.draw();
@@ -63,14 +78,32 @@
const rect = this.container.getBoundingClientRect(); const rect = this.container.getBoundingClientRect();
this.canvas.width = rect.width; this.canvas.width = rect.width;
this.canvas.height = rect.height; this.canvas.height = rect.height;
this.pageX = Math.max(20, (this.canvas.width - this.pageWidth) / 2);
// Calculate scale to maximize page size while maintaining aspect ratio
// Use full available space with minimal padding
const padding = 10;
const availableWidth = this.canvas.width - padding * 2;
const availableHeight = this.canvas.height - padding * 2;
// Scale to fit the available space (can be larger than 1 if container is bigger than A4)
this.scale = Math.min(availableWidth / this.pageWidth, availableHeight / this.pageHeight);
// Center the scaled page
const scaledPageWidth = this.pageWidth * this.scale;
const scaledPageHeight = this.pageHeight * this.scale;
this.pageX = (this.canvas.width - scaledPageWidth) / 2;
this.pageY = (this.canvas.height - scaledPageHeight) / 2;
} }
getMousePos(e) { getMousePos(e) {
const rect = this.canvas.getBoundingClientRect(); const rect = this.canvas.getBoundingClientRect();
// Convert screen coordinates to canvas coordinates
const canvasX = e.clientX - rect.left;
const canvasY = e.clientY - rect.top;
// Convert to page coordinates (accounting for scale and offset)
return { return {
x: e.clientX - rect.left, x: (canvasX - this.pageX) / this.scale,
y: e.clientY - rect.top y: (canvasY - this.pageY) / this.scale
}; };
} }
@@ -166,14 +199,18 @@
const dx = pos.x - this.dragStart.x; const dx = pos.x - this.dragStart.x;
const dy = pos.y - this.dragStart.y; const dy = pos.y - this.dragStart.y;
this.selectedElement.x = this.elementStart.x + dx; let newX = this.elementStart.x + dx;
this.selectedElement.y = this.elementStart.y + dy; let newY = this.elementStart.y + dy;
// Constrain to page // Constrain to page (in page coordinates, page starts at 0,0)
this.selectedElement.x = Math.max(this.pageX, Math.min(this.selectedElement.x, newX = Math.max(0, Math.min(newX,
this.pageX + this.pageWidth - (this.selectedElement.width || 100))); this.pageWidth - (this.selectedElement.width || 100)));
this.selectedElement.y = Math.max(this.pageY, Math.min(this.selectedElement.y, newY = Math.max(0, Math.min(newY,
this.pageY + this.pageHeight - (this.selectedElement.height || 30))); this.pageHeight - (this.selectedElement.height || 30)));
// Snap to grid
this.selectedElement.x = this.snapToGrid(newX);
this.selectedElement.y = this.snapToGrid(newY);
this.draw(); this.draw();
this.notifyChange(); this.notifyChange();
@@ -211,6 +248,47 @@
this.canvas.style.cursor = 'default'; this.canvas.style.cursor = 'default';
} }
onKeyDown(e) {
if (!this.selectedElement) return;
const step = this.gridSize; // Move by grid size (5px)
let moved = false;
switch(e.key) {
case 'ArrowUp':
this.selectedElement.y = Math.max(0, this.selectedElement.y - step);
moved = true;
e.preventDefault();
break;
case 'ArrowDown':
this.selectedElement.y = Math.min(
this.pageHeight - (this.selectedElement.height || 30),
this.selectedElement.y + step
);
moved = true;
e.preventDefault();
break;
case 'ArrowLeft':
this.selectedElement.x = Math.max(0, this.selectedElement.x - step);
moved = true;
e.preventDefault();
break;
case 'ArrowRight':
this.selectedElement.x = Math.min(
this.pageWidth - (this.selectedElement.width || 100),
this.selectedElement.x + step
);
moved = true;
e.preventDefault();
break;
}
if (moved) {
this.draw();
this.notifyChange();
}
}
doResize(mouseX, mouseY) { doResize(mouseX, mouseY) {
if (!this.selectedElement || !this.activeHandle) return; if (!this.selectedElement || !this.activeHandle) return;
@@ -308,17 +386,31 @@
ctx.fillStyle = '#e8e8e8'; ctx.fillStyle = '#e8e8e8';
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Page shadow ctx.save();
// Apply scale and offset
ctx.translate(this.pageX, this.pageY);
ctx.scale(this.scale, this.scale);
// Page shadow (drawn before scaling offset)
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = 'rgba(0,0,0,0.2)'; ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.fillRect(this.pageX + 4, this.pageY + this.pageHeight, this.pageWidth, 4); ctx.fillRect(this.pageX + 4 * this.scale, this.pageY + this.pageHeight * this.scale, this.pageWidth * this.scale, 4);
ctx.fillRect(this.pageX + this.pageWidth, this.pageY + 4, 4, this.pageHeight); ctx.fillRect(this.pageX + this.pageWidth * this.scale, this.pageY + 4 * this.scale, 4, this.pageHeight * this.scale);
ctx.restore();
// Page // Page
ctx.fillStyle = '#ffffff'; ctx.fillStyle = '#ffffff';
ctx.fillRect(this.pageX, this.pageY, this.pageWidth, this.pageHeight); ctx.fillRect(0, 0, this.pageWidth, this.pageHeight);
ctx.strokeStyle = '#cccccc'; ctx.strokeStyle = '#cccccc';
ctx.lineWidth = 1; ctx.lineWidth = 1 / this.scale;
ctx.strokeRect(this.pageX, this.pageY, this.pageWidth, this.pageHeight); ctx.strokeRect(0, 0, this.pageWidth, this.pageHeight);
// Draw grid if enabled
if (this.showGrid) {
this.drawGrid(ctx);
}
// Draw elements // Draw elements
this.elements.forEach(el => this.drawElement(el)); this.elements.forEach(el => this.drawElement(el));
@@ -327,6 +419,8 @@
if (this.selectedElement) { if (this.selectedElement) {
this.drawSelection(this.selectedElement); this.drawSelection(this.selectedElement);
} }
ctx.restore();
} }
drawImagePlaceholder(ctx, el) { drawImagePlaceholder(ctx, el) {
@@ -491,7 +585,7 @@
const y = el.y; const y = el.y;
const w = el.width || 150; const w = el.width || 150;
const h = el.height || 30; const h = el.height || 30;
const hs = this.handleSize; const hs = this.handleSize / this.scale;
// Selection border // Selection border
ctx.strokeStyle = '#1976d2'; ctx.strokeStyle = '#1976d2';
@@ -522,13 +616,43 @@
}); });
} }
drawGrid(ctx) {
ctx.save();
ctx.strokeStyle = 'rgba(200, 200, 200, 0.3)';
ctx.lineWidth = 0.5 / this.scale;
// Vertical lines (draw in page coordinates)
for (let x = 0; x <= this.pageWidth; x += this.gridSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, this.pageHeight);
ctx.stroke();
}
// Horizontal lines
for (let y = 0; y <= this.pageHeight; y += this.gridSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(this.pageWidth, y);
ctx.stroke();
}
ctx.restore();
}
addElement(type, label, dropX, dropY) { addElement(type, label, dropX, dropY) {
this.elementCounter++; this.elementCounter++;
const id = `element-${this.elementCounter}`; const id = `element-${this.elementCounter}`;
const rect = this.container.getBoundingClientRect(); // dropX and dropY are in container coordinates, convert to page coordinates
const x = Math.max(this.pageX + 10, dropX - rect.left - 50); const pageCoordX = (dropX - this.pageX) / this.scale;
const y = Math.max(this.pageY + 10, dropY - rect.top - 15); const pageCoordY = (dropY - this.pageY) / this.scale;
// Snap initial position to grid (relative to page origin)
const rawX = Math.max(10, pageCoordX - 50);
const rawY = Math.max(10, pageCoordY - 15);
const x = this.snapToGrid(rawX);
const y = this.snapToGrid(rawY);
let el = { let el = {
id, type, x, y, id, type, x, y,
@@ -595,6 +719,10 @@
this.draw(); this.draw();
if (this.selectedElement) { if (this.selectedElement) {
this.notifyChange(); this.notifyChange();
// Focus canvas for keyboard navigation
if (this.canvas) {
this.canvas.focus();
}
} }
} }
@@ -617,8 +745,8 @@
updateElementPosition(id, x, y) { updateElementPosition(id, x, y) {
const el = this.elements.get(id); const el = this.elements.get(id);
if (el) { if (el) {
if (x !== null) el.x = x; if (x !== null) el.x = this.snapToGrid(x);
if (y !== null) el.y = y; if (y !== null) el.y = this.snapToGrid(y);
this.draw(); this.draw();
} }
} }
@@ -631,6 +759,14 @@
} }
} }
updateElementColor(id, color) {
const el = this.elements.get(id);
if (el) {
el.color = color;
this.draw();
}
}
updateElementImage(id, imageDataUrl) { updateElementImage(id, imageDataUrl) {
const el = this.elements.get(id); const el = this.elements.get(id);
if (el && el.type === 'image') { if (el && el.type === 'image') {
@@ -683,6 +819,12 @@
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
exportTemplateJson() {
// Return template data as JSON string for Java processing
const data = this.getCanvasData();
return JSON.stringify(data);
}
generatePreview() { generatePreview() {
const temp = document.createElement('canvas'); const temp = document.createElement('canvas');
temp.width = this.pageWidth; temp.width = this.pageWidth;
@@ -718,4 +860,5 @@
} }
window.invoiceGenerator = new InvoiceGenerator(); window.invoiceGenerator = new InvoiceGenerator();
window.InvoiceGenerator = InvoiceGenerator;
})(); })();

View File

@@ -0,0 +1,534 @@
// Profile Invoice Generator - Initializes canvas on the edit-profile page
window.initProfileInvoiceGenerator = function() {
var containerId = 'invoice-canvas-container-profile';
var container = document.getElementById(containerId);
if (!container) {
console.error('Canvas container not found:', containerId);
return;
}
// Check if canvas already exists
var existingCanvas = container.querySelector('canvas');
if (existingCanvas) {
console.log('Canvas already exists');
return;
}
// Get container dimensions
var rect = container.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
console.error('Container has no size');
return;
}
// Create canvas element
var canvas = document.createElement('canvas');
canvas.style.display = 'block';
canvas.width = rect.width;
canvas.height = rect.height;
container.appendChild(canvas);
var ctx = canvas.getContext('2d');
// State
var elements = [];
var selectedElement = null;
var isDragging = false;
var dragStart = { x: 0, y: 0 };
var elementStart = { x: 0, y: 0 };
var elementCounter = 0;
var gridSize = 5;
// Page dimensions
var padding = 10;
var pageX, pageY, pageWidth, pageHeight;
function updatePageDimensions() {
var w = canvas.width;
var h = canvas.height;
var availableWidth = w - padding * 2;
var availableHeight = h - padding * 2;
pageWidth = Math.min(availableWidth, availableHeight * 0.707);
pageHeight = pageWidth / 0.707;
pageX = (w - pageWidth) / 2;
pageY = (h - pageHeight) / 2;
}
function snapToGrid(value) {
return Math.round(value / gridSize) * gridSize;
}
// Notify Java about element selection
function notifyElementSelected(el) {
if (window.invoiceGeneratorViewProfile && window.invoiceGeneratorViewProfile.$server) {
window.invoiceGeneratorViewProfile.$server.updatePropertiesPanel(
el.id,
el.type,
el.text || '',
el.x,
el.y,
el.fontSize || 14,
el.color || '#333333',
el.width || 100,
el.height || 30
);
}
}
function notifyElementDeselected() {
if (window.invoiceGeneratorViewProfile && window.invoiceGeneratorViewProfile.$server) {
window.invoiceGeneratorViewProfile.$server.resetPropertiesPanel();
}
}
// Draw function
function draw() {
var w = canvas.width;
var h = canvas.height;
// Clear background
ctx.fillStyle = '#e8e8e8';
ctx.fillRect(0, 0, w, h);
updatePageDimensions();
// Page shadow
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.fillRect(pageX + 4, pageY + pageHeight, pageWidth, 4);
ctx.fillRect(pageX + pageWidth, pageY + 4, 4, pageHeight);
// White page
ctx.fillStyle = '#ffffff';
ctx.fillRect(pageX, pageY, pageWidth, pageHeight);
ctx.strokeStyle = '#cccccc';
ctx.lineWidth = 1;
ctx.strokeRect(pageX, pageY, pageWidth, pageHeight);
// Grid
ctx.strokeStyle = 'rgba(200, 200, 200, 0.3)';
ctx.lineWidth = 0.5;
for (var x = pageX; x <= pageX + pageWidth; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x, pageY);
ctx.lineTo(x, pageY + pageHeight);
ctx.stroke();
}
for (var y = pageY; y <= pageY + pageHeight; y += gridSize) {
ctx.beginPath();
ctx.moveTo(pageX, y);
ctx.lineTo(pageX + pageWidth, y);
ctx.stroke();
}
// Draw elements
elements.forEach(function(el) {
drawElement(el);
});
// Draw selection
if (selectedElement) {
drawSelection(selectedElement);
}
}
function drawElement(el) {
ctx.save();
if (el.type === 'line') {
ctx.strokeStyle = el.color || '#333333';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pageX + el.x, pageY + el.y);
ctx.lineTo(pageX + el.x + el.width, pageY + el.y);
ctx.stroke();
} else if (el.type === 'image') {
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(pageX + el.x, pageY + el.y, el.width, el.height);
ctx.strokeStyle = '#999999';
ctx.strokeRect(pageX + el.x, pageY + el.y, el.width, el.height);
ctx.fillStyle = '#666666';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Bild', pageX + el.x + el.width / 2, pageY + el.y + el.height / 2);
} else {
// Text elements
ctx.font = (el.fontStyle || '') + ' ' + (el.fontSize || 14) + 'px Arial';
ctx.fillStyle = el.color || '#333333';
ctx.textBaseline = 'top';
var lines = (el.text || '').split('\n');
var lineHeight = (el.fontSize || 14) * 1.2;
var y = pageY + el.y;
lines.forEach(function(line) {
ctx.fillText(line, pageX + el.x, y);
y += lineHeight;
});
}
ctx.restore();
}
function drawSelection(el) {
var x = pageX + el.x;
var y = pageY + el.y;
var w = el.width || 100;
var h = el.height || 30;
var hs = 8; // handle size
ctx.strokeStyle = '#1976d2';
ctx.lineWidth = 2;
ctx.setLineDash([5, 3]);
ctx.strokeRect(x, y, w, h);
ctx.setLineDash([]);
// Handles
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#1976d2';
ctx.lineWidth = 2;
var positions = [
[x - hs/2, y - hs/2],
[x + w/2 - hs/2, y - hs/2],
[x + w - hs/2, y - hs/2],
[x - hs/2, y + h/2 - hs/2],
[x + w - hs/2, y + h/2 - hs/2],
[x - hs/2, y + h - hs/2],
[x + w/2 - hs/2, y + h - hs/2],
[x + w - hs/2, y + h - hs/2]
];
positions.forEach(function(pos) {
ctx.fillRect(pos[0], pos[1], hs, hs);
ctx.strokeRect(pos[0], pos[1], hs, hs);
});
}
function hitTest(x, y, el) {
var ex = pageX + el.x;
var ey = pageY + el.y;
var ew = el.width || 100;
var eh = el.height || 30;
return x >= ex && x <= ex + ew && y >= ey && y <= ey + eh;
}
// Mouse events
canvas.addEventListener('mousedown', function(e) {
var rect = canvas.getBoundingClientRect();
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
// Check if clicking on an element
var clickedElement = null;
for (var i = elements.length - 1; i >= 0; i--) {
if (hitTest(x, y, elements[i])) {
clickedElement = elements[i];
break;
}
}
if (clickedElement) {
selectedElement = clickedElement;
isDragging = true;
dragStart = { x: x, y: y };
elementStart = { x: clickedElement.x, y: clickedElement.y };
canvas.style.cursor = 'move';
notifyElementSelected(selectedElement);
} else {
selectedElement = null;
notifyElementDeselected();
}
draw();
});
canvas.addEventListener('mousemove', function(e) {
var rect = canvas.getBoundingClientRect();
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
if (isDragging && selectedElement) {
var dx = x - dragStart.x;
var dy = y - dragStart.y;
var newX = elementStart.x + dx;
var newY = elementStart.y + dy;
// Constrain to page
newX = Math.max(0, Math.min(newX, pageWidth - (selectedElement.width || 100)));
newY = Math.max(0, Math.min(newY, pageHeight - (selectedElement.height || 30)));
// Snap to grid
selectedElement.x = snapToGrid(newX);
selectedElement.y = snapToGrid(newY);
draw();
notifyElementSelected(selectedElement);
} else {
// Update cursor
var hovering = false;
for (var i = elements.length - 1; i >= 0; i--) {
if (hitTest(x, y, elements[i])) {
hovering = true;
break;
}
}
canvas.style.cursor = hovering ? 'move' : 'default';
}
});
canvas.addEventListener('mouseup', function() {
isDragging = false;
canvas.style.cursor = 'default';
});
canvas.addEventListener('mouseleave', function() {
isDragging = false;
canvas.style.cursor = 'default';
});
// Keyboard navigation
canvas.setAttribute('tabindex', '0');
canvas.addEventListener('keydown', function(e) {
if (!selectedElement) return;
var step = gridSize;
var moved = false;
switch(e.key) {
case 'ArrowUp':
selectedElement.y = Math.max(0, selectedElement.y - step);
moved = true;
e.preventDefault();
break;
case 'ArrowDown':
selectedElement.y = Math.min(pageHeight - (selectedElement.height || 30), selectedElement.y + step);
moved = true;
e.preventDefault();
break;
case 'ArrowLeft':
selectedElement.x = Math.max(0, selectedElement.x - step);
moved = true;
e.preventDefault();
break;
case 'ArrowRight':
selectedElement.x = Math.min(pageWidth - (selectedElement.width || 100), selectedElement.x + step);
moved = true;
e.preventDefault();
break;
case 'Delete':
var index = elements.indexOf(selectedElement);
if (index > -1) {
elements.splice(index, 1);
selectedElement = null;
notifyElementDeselected();
draw();
}
e.preventDefault();
break;
}
if (moved) {
draw();
notifyElementSelected(selectedElement);
}
});
// Add element function
window.addProfileElement = function(type, label, dropX, dropY) {
elementCounter++;
var id = 'element-' + elementCounter;
// Convert to page coordinates
var x = dropX - pageX - 50;
var y = dropY - pageY - 15;
x = Math.max(10, x);
y = Math.max(10, y);
x = snapToGrid(x);
y = snapToGrid(y);
var el = {
id: id,
type: type,
x: x,
y: y,
width: 150,
height: 30,
fontSize: 14,
color: '#333333'
};
switch (type) {
case 'text':
el.text = 'Text eingeben...';
el.height = 20;
break;
case 'header':
el.text = 'Überschrift';
el.fontSize = 24;
el.fontStyle = 'bold';
el.color = '#000000';
el.width = 200;
el.height = 30;
break;
case 'date':
el.text = 'Datum: ' + new Date().toLocaleDateString('de-DE');
el.fontSize = 12;
el.color = '#666666';
el.height = 16;
break;
case 'customer':
el.text = 'Kundenname\nStraße Nr.\nPLZ Ort';
el.height = 50;
el.fontSize = 12;
break;
case 'company':
el.text = 'Ihr Unternehmen\nIhre Straße\nIhre PLZ Ort';
el.height = 50;
el.fontSize = 12;
break;
case 'amount':
el.text = 'Gesamtbetrag: 0,00 €';
el.fontStyle = 'bold';
el.width = 180;
el.height = 20;
break;
case 'line':
el.text = '';
el.width = 200;
el.height = 2;
break;
case 'image':
el.text = 'Bild';
el.width = 100;
el.height = 100;
break;
default:
el.text = label || 'Neues Element';
el.height = 20;
}
elements.push(el);
selectedElement = el;
draw();
notifyElementSelected(el);
// Focus canvas for keyboard navigation
canvas.focus();
};
// Update element functions
window.updateProfileElementText = function(id, text) {
var el = elements.find(function(e) { return e.id === id; });
if (el) {
el.text = text;
draw();
}
};
window.updateProfileElementPosition = function(id, x, y) {
var el = elements.find(function(e) { return e.id === id; });
if (el) {
if (x !== null) el.x = snapToGrid(x);
if (y !== null) el.y = snapToGrid(y);
draw();
}
};
window.updateProfileElementFontSize = function(id, size) {
var el = elements.find(function(e) { return e.id === id; });
if (el) {
el.fontSize = size;
draw();
}
};
window.updateProfileElementColor = function(id, color) {
var el = elements.find(function(e) { return e.id === id; });
if (el) {
el.color = color;
draw();
}
};
window.updateProfileElementSize = function(id, width, height) {
var el = elements.find(function(e) { return e.id === id; });
if (el) {
if (width !== null) el.width = width;
if (height !== null) el.height = height;
draw();
}
};
window.deleteProfileElement = function(id) {
var index = elements.findIndex(function(e) { return e.id === id; });
if (index > -1) {
elements.splice(index, 1);
if (selectedElement && selectedElement.id === id) {
selectedElement = null;
notifyElementDeselected();
}
draw();
}
};
// Clear canvas function
window.clearProfileCanvas = function() {
elements = [];
selectedElement = null;
notifyElementDeselected();
draw();
};
// Get canvas data
window.getProfileCanvasData = function() {
return {
elements: elements
};
};
draw();
console.log('Profile canvas initialized');
// Handle window resize
window.addEventListener('resize', function() {
var newRect = container.getBoundingClientRect();
if (newRect.width > 0 && newRect.height > 0) {
canvas.width = newRect.width;
canvas.height = newRect.height;
draw();
}
});
// Setup drop zone
if (!container._dropSetup) {
container._dropSetup = true;
container.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
container.style.borderColor = 'var(--lumo-primary-color)';
});
container.addEventListener('dragleave', function(e) {
container.style.borderColor = 'var(--lumo-contrast-20pct)';
});
container.addEventListener('drop', function(e) {
e.preventDefault();
container.style.borderColor = 'var(--lumo-contrast-20pct)';
var templateType = e.dataTransfer.getData('template-type');
var templateLabel = e.dataTransfer.getData('template-label');
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);
}
});
}
};

View File

@@ -6,8 +6,9 @@ import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.html.IFrame; import com.vaadin.flow.component.html.IFrame;
import com.vaadin.flow.component.html.Input;
import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.Notification;
@@ -26,7 +27,6 @@ import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.tabs.TabSheet; import com.vaadin.flow.component.tabs.TabSheet;
import com.vaadin.flow.data.binder.Binder; import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.validator.EmailValidator; import com.vaadin.flow.data.validator.EmailValidator;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.model.User; import de.assecutor.votianlt.model.User;
@@ -37,11 +37,14 @@ import de.assecutor.votianlt.pages.service.UserService;
import de.assecutor.votianlt.pages.service.UserInvoiceDataService; import de.assecutor.votianlt.pages.service.UserInvoiceDataService;
import de.assecutor.votianlt.security.SecurityService; import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.service.CustomerInvoiceService; import de.assecutor.votianlt.service.CustomerInvoiceService;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.ClientCallable;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
@PageTitle("Profil bearbeiten") @PageTitle("Profil bearbeiten")
@Route(value = "edit-profile", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class) @Route(value = "edit-profile", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({ "USER", "ADMIN" }) @RolesAllowed({ "USER", "ADMIN" })
@JsModule("./invoice-generator/profile-invoice-generator.js")
public class EditProfileView extends HorizontalLayout { public class EditProfileView extends HorizontalLayout {
private final TextField prefixField; private final TextField prefixField;
private final TextField ustIdField; private final TextField ustIdField;
@@ -59,6 +62,7 @@ public class EditProfileView extends HorizontalLayout {
private final CustomerInvoiceService customerInvoiceService; private final CustomerInvoiceService customerInvoiceService;
private UserInvoiceData currentInvoiceData; private UserInvoiceData currentInvoiceData;
private Checkbox billingEnabled; private Checkbox billingEnabled;
private VerticalLayout propertiesPanelProfile;
public EditProfileView(UserService userService, UserInvoiceDataService userInvoiceDataService, public EditProfileView(UserService userService, UserInvoiceDataService userInvoiceDataService,
CustomerInvoiceService customerInvoiceService, SecurityService securityService) { CustomerInvoiceService customerInvoiceService, SecurityService securityService) {
@@ -78,7 +82,7 @@ public class EditProfileView extends HorizontalLayout {
// Linke Spalte: Formular // Linke Spalte: Formular
VerticalLayout formColumn = new VerticalLayout(); VerticalLayout formColumn = new VerticalLayout();
formColumn.setWidth("68%"); formColumn.setWidthFull();
formColumn.setHeightFull(); formColumn.setHeightFull();
formColumn.setPadding(false); formColumn.setPadding(false);
formColumn.setSpacing(false); formColumn.setSpacing(false);
@@ -267,93 +271,148 @@ public class EditProfileView extends HorizontalLayout {
mapTab.setSpacing(true); mapTab.setSpacing(true);
mapTab.add(coordsLabel, mapDiv); mapTab.add(coordsLabel, mapDiv);
tabSheet.add("Karte", mapTab); tabSheet.add("Karte", mapTab);
// Dritter Tab: Rechnungsstellung // Dritter Tab: Rechnungserstellung
HorizontalLayout billingTab = new HorizontalLayout(); VerticalLayout billingTab = new VerticalLayout();
billingTab.setWidthFull(); billingTab.setWidthFull();
billingTab.setSpacing(true); billingTab.setSpacing(true);
billingTab.setPadding(false); billingTab.setPadding(false);
billingTab.setHeightFull();
// Linke Spalte: Rechnungsbestandteile // Rechnungsfelder initialisieren (für Speicherung, aber nicht im UI)
VerticalLayout billingLeft = new VerticalLayout(); prefixField = new TextField();
billingLeft.setWidth("45%"); ustIdField = new TextField();
billingLeft.setPadding(false); taxNumberField = new TextField();
billingLeft.setSpacing(true); bankNameField = new TextField();
H3 partsTitle = new H3("Rechnungsbestandteile"); ibanField = new TextField();
partsTitle.getStyle().set("margin", "0 0 var(--lumo-space-s) 0"); taxRateField = new TextField();
introTextArea = new TextArea();
// Felder für Rechnungsstellung (für Live-Update) termsTextArea = new TextArea();
pdfFrame = new IFrame();
// Nur die Checkbox "Rechnungslegung über votianLT"
billingEnabled = new Checkbox("Rechnungslegung über votianLT"); billingEnabled = new Checkbox("Rechnungslegung über votianLT");
billingEnabled.setValue(false); billingEnabled.setValue(false);
billingEnabled.addValueChangeListener(e -> toggleBilling(e.getValue())); billingEnabled.addValueChangeListener(e -> {
// Nur noch für das Speichern des Status, keine PDF-Vorschau mehr
});
billingTab.add(billingEnabled);
prefixField = new TextField("Rechnungs Präfix"); // Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften)
ustIdField = new TextField("USt-IdNr."); HorizontalLayout mainLayout = new HorizontalLayout();
taxNumberField = new TextField("Steuernummer"); mainLayout.setWidthFull();
bankNameField = new TextField("Bankname"); mainLayout.setHeight("500px");
ibanField = new TextField("IBAN"); mainLayout.setSpacing(true);
taxRateField = new TextField("Steuersatz"); mainLayout.getStyle().set("overflow", "hidden");
introTextArea = new TextArea("Einleitungstext");
termsTextArea = new TextArea("Zahlungsbedingungen");
introTextArea.setWidthFull();
termsTextArea.setWidthFull();
// Anfangszustand: Felder deaktiviert bis Checkbox aktiv // Linke Seite: Templates/Bausteine
setBillingFieldsEnabled(false); VerticalLayout leftPanel = createTemplatesPanelForProfile();
leftPanel.setWidth("220px");
leftPanel.setHeightFull();
leftPanel.getStyle()
.set("flex-shrink", "0")
.set("min-width", "220px")
.set("overflow", "auto")
.set("background-color", "var(--lumo-contrast-5pct)")
.set("border-radius", "var(--lumo-border-radius-m)")
.set("padding", "var(--lumo-space-m)");
// Live-Update: EAGER und Listener // Mitte: Canvas
prefixField.setValueChangeMode(ValueChangeMode.EAGER); Div canvasContainer = new Div();
ustIdField.setValueChangeMode(ValueChangeMode.EAGER); canvasContainer.setId("invoice-canvas-container-profile");
taxNumberField.setValueChangeMode(ValueChangeMode.EAGER); canvasContainer.setWidth("100%");
bankNameField.setValueChangeMode(ValueChangeMode.EAGER); canvasContainer.setHeight("100%");
ibanField.setValueChangeMode(ValueChangeMode.EAGER); canvasContainer.getStyle()
taxRateField.setValueChangeMode(ValueChangeMode.EAGER); .set("background-color", "#e8e8e8")
introTextArea.setValueChangeMode(ValueChangeMode.EAGER); .set("border", "1px solid var(--lumo-contrast-20pct)")
termsTextArea.setValueChangeMode(ValueChangeMode.EAGER); .set("border-radius", "var(--lumo-border-radius-m)")
prefixField.addValueChangeListener(e -> refreshPdf()); .set("overflow", "hidden")
ustIdField.addValueChangeListener(e -> refreshPdf()); .set("position", "relative");
taxNumberField.addValueChangeListener(e -> refreshPdf());
bankNameField.addValueChangeListener(e -> refreshPdf()); VerticalLayout centerPanel = new VerticalLayout();
ibanField.addValueChangeListener(e -> refreshPdf()); centerPanel.setWidth("60%");
taxRateField.addValueChangeListener(e -> refreshPdf()); centerPanel.setHeightFull();
introTextArea.addValueChangeListener(e -> refreshPdf()); centerPanel.setPadding(false);
termsTextArea.addValueChangeListener(e -> refreshPdf()); centerPanel.setSpacing(false);
centerPanel.getStyle()
.set("flex-grow", "1")
.set("min-width", "0");
centerPanel.add(canvasContainer);
centerPanel.expand(canvasContainer);
billingLeft.add(partsTitle, billingEnabled, prefixField, ustIdField, taxNumberField, bankNameField, ibanField, // Rechte Seite: Eigenschaften
taxRateField, introTextArea, termsTextArea); propertiesPanelProfile = createPropertiesPanelForProfile();
propertiesPanelProfile.setWidth("280px");
propertiesPanelProfile.setHeightFull();
propertiesPanelProfile.getStyle()
.set("flex-shrink", "0")
.set("min-width", "280px")
.set("overflow", "auto")
.set("background-color", "var(--lumo-contrast-5pct)")
.set("border-radius", "var(--lumo-border-radius-m)")
.set("padding", "var(--lumo-space-m)");
// Rechte Spalte: Vorschau mainLayout.add(leftPanel, centerPanel, propertiesPanelProfile);
VerticalLayout billingRight = new VerticalLayout(); mainLayout.expand(centerPanel);
billingRight.setWidth("55%"); billingTab.add(mainLayout);
billingRight.setPadding(false); billingTab.expand(mainLayout);
billingRight.setSpacing(false);
billingRight.setHeightFull(); // Action Buttons für den Rechnungsgenerator
billingTab.setFlexGrow(1, billingRight); HorizontalLayout actionLayout = new HorizontalLayout();
H3 previewTitle = new H3("Rechnungsvorschau"); actionLayout.setWidthFull();
previewTitle.getStyle().set("margin", "0 0 var(--lumo-space-s) 0"); actionLayout.setJustifyContentMode(JustifyContentMode.END);
actionLayout.setSpacing(true);
actionLayout.getStyle().set("margin-top", "var(--lumo-space-s)");
Button clearButton = new Button("Leeren", new Icon(VaadinIcon.TRASH));
clearButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
clearButton.addClickListener(e -> {
getElement().executeJs("if (window.clearProfileCanvas) { window.clearProfileCanvas(); }");
Notification.show("Canvas wurde geleert", 2000, Notification.Position.BOTTOM_CENTER);
});
Button previewPdfButton = new Button("Vorschau", new Icon(VaadinIcon.EYE));
previewPdfButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
previewPdfButton.addClickListener(e -> generatePreviewPdfFromProfile());
Button saveTemplateButton = new Button("Template speichern", new Icon(VaadinIcon.DOWNLOAD));
saveTemplateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
saveTemplateButton.addClickListener(e -> {
getElement().executeJs(
"if (window.getProfileCanvasData) {" +
" var data = JSON.stringify(window.getProfileCanvasData(), null, 2);" +
" var blob = new Blob([data], { type: 'application/json' });" +
" var url = URL.createObjectURL(blob);" +
" var a = document.createElement('a');" +
" a.href = url;" +
" a.download = 'template.json';" +
" a.click();" +
" URL.revokeObjectURL(url);" +
"}");
});
actionLayout.add(clearButton, previewPdfButton, saveTemplateButton);
billingTab.add(actionLayout);
tabSheet.add("Rechnungserstellung", billingTab);
// Echte PDF-Vorschau mittels StreamResource und iframe // Bestehende Rechnungsdaten laden (nur für die Checkbox)
Div previewWrapper = new Div();
previewWrapper.setWidth("100%");
previewWrapper.setHeight("650px");
previewWrapper.getStyle().set("overflow", "hidden").set("background", "var(--lumo-contrast-10pct)")
.set("padding", "0");
// Initial noch keine PDF laden (erst bei aktiver Checkbox)
pdfFrame = new IFrame();
pdfFrame.setWidth("99%");
pdfFrame.setHeight("98%");
pdfFrame.getStyle().set("border", "none");
pdfFrame.getStyle().set("background", "var(--lumo-contrast-10pct)");
previewWrapper.removeAll();
previewWrapper.add(pdfFrame);
billingRight.add(previewTitle, previewWrapper);
billingTab.add(billingLeft, billingRight);
tabSheet.add("Rechnungsstellung", billingTab);
// Bestehende Rechnungsdaten laden (nach Erstellung aller Felder)
loadInvoiceData(); loadInvoiceData();
// Initialize invoice generator when the billing tab is selected
// Also register this view instance for JavaScript callbacks
tabSheet.addSelectedChangeListener(e -> {
if ("Rechnungserstellung".equals(e.getSelectedTab().getLabel())) {
getElement().executeJs(
"window.invoiceGeneratorViewProfile = $0;" +
"setTimeout(function() { " +
" if (window.initProfileInvoiceGenerator) { " +
" window.initProfileInvoiceGenerator(); " +
" } else { " +
" console.error('initProfileInvoiceGenerator not found'); " +
" } " +
"}, 300);", getElement());
}
});
// Zweiter Tab: Einstellungen (Beispiel mit Schaltern) // Zweiter Tab: Einstellungen (Beispiel mit Schaltern)
VerticalLayout switches = new VerticalLayout(); VerticalLayout switches = new VerticalLayout();
@@ -494,25 +553,6 @@ public class EditProfileView extends HorizontalLayout {
termsTextArea.setEnabled(enabled); termsTextArea.setEnabled(enabled);
} }
// Checkbox steuert Aktivierung und PDF
private void toggleBilling(boolean enabled) {
setBillingFieldsEnabled(enabled);
if (enabled) {
refreshPdf();
} else {
if (pdfFrame != null) {
pdfFrame.setSrc("about:blank");
}
}
// Sicherstellen, dass das IFrame den verfügbaren Raum nutzt
if (pdfFrame != null) {
pdfFrame.setWidth("100%");
pdfFrame.setHeight("100%");
pdfFrame.getStyle().set("border", "none");
pdfFrame.getStyle().set("display", "block");
}
}
private void refreshPdf() { private void refreshPdf() {
try { try {
byte[] bytes = generatePreviewPdf(); byte[] bytes = generatePreviewPdf();
@@ -712,4 +752,336 @@ public class EditProfileView extends HorizontalLayout {
&& !houseNumberField.isInvalid() && !zipField.isInvalid() && !cityField.isInvalid(); && !houseNumberField.isInvalid() && !zipField.isInvalid() && !cityField.isInvalid();
} }
// Methoden für den Rechnungsgenerator im Profil
private void generatePreviewPdfFromProfile() {
try {
getElement().executeJs(
"if (window.getProfileCanvasData) { return window.getProfileCanvasData(); } else { return null; }")
.then(result -> {
if (result == null) {
Notification.show("Fehler: Canvas-Daten konnten nicht gelesen werden", 3000, Notification.Position.BOTTOM_CENTER);
return;
}
try {
String templateData;
if (result instanceof elemental.json.JsonValue) {
elemental.json.JsonValue jsonValue = (elemental.json.JsonValue) result;
templateData = jsonValue.toJson();
} else {
templateData = result.toString();
}
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData);
showPdfInDialog(pdfBytes);
} catch (Exception ex) {
Notification.show("Fehler beim Generieren der Vorschau: " + ex.getMessage(), 3000, Notification.Position.BOTTOM_CENTER);
}
});
} catch (Exception ex) {
Notification.show("Fehler: " + ex.getMessage(), 3000, Notification.Position.BOTTOM_CENTER);
}
}
private void showPdfInDialog(byte[] pdfBytes) {
// Create a stream resource for the PDF
String base64Pdf = Base64.getEncoder().encodeToString(pdfBytes);
String dataUrl = "data:application/pdf;base64," + base64Pdf;
// Create dialog
Dialog pdfDialog = new Dialog();
pdfDialog.setHeaderTitle("PDF Vorschau");
pdfDialog.setWidth("90vw");
pdfDialog.setHeight("90vh");
// Create a Div to hold the PDF viewer
Div pdfContainer = new Div();
pdfContainer.setWidth("100%");
pdfContainer.setHeight("100%");
pdfContainer.getStyle()
.set("display", "flex")
.set("flex-direction", "column")
.set("overflow", "hidden");
// Use an iframe with data URL for PDF display
IFrame pdfFrame = new IFrame();
pdfFrame.setWidth("100%");
pdfFrame.setHeight("100%");
pdfFrame.getElement().setAttribute("src", dataUrl);
pdfFrame.getStyle()
.set("border", "none")
.set("flex-grow", "1");
pdfContainer.add(pdfFrame);
// 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 = 'vorschau.pdf';" +
"link.click();");
});
downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
pdfDialog.add(pdfContainer);
pdfDialog.getFooter().add(downloadButton, closeButton);
pdfDialog.open();
}
// Panel-Methoden für den Rechnungsgenerator
private VerticalLayout createTemplatesPanelForProfile() {
VerticalLayout panel = new VerticalLayout();
panel.setPadding(false);
panel.setSpacing(true);
panel.setHeightFull();
Span header = new Span("Textbausteine");
header.getStyle()
.set("font-weight", "bold")
.set("font-size", "var(--lumo-font-size-l)");
// Draggable Templates
Div textBlock = createDraggableTemplate("Textfeld", VaadinIcon.TEXT_LABEL, "text");
Div headerBlock = createDraggableTemplate("Überschrift", VaadinIcon.HEADER, "header");
Div dateBlock = createDraggableTemplate("Datum", VaadinIcon.CALENDAR, "date");
Div customerBlock = createDraggableTemplate("Kundeninfo", VaadinIcon.USER, "customer");
Div companyBlock = createDraggableTemplate("Firmeninfo", VaadinIcon.OFFICE, "company");
Div amountBlock = createDraggableTemplate("Betrag", VaadinIcon.COIN_PILES, "amount");
Div lineBlock = createDraggableTemplate("Linie", VaadinIcon.LINE_V, "line");
Div imageBlock = createDraggableTemplate("Bild", VaadinIcon.PICTURE, "image");
panel.add(header, textBlock, headerBlock, dateBlock, customerBlock, companyBlock, amountBlock, lineBlock,
imageBlock);
return panel;
}
private Div createDraggableTemplate(String label, VaadinIcon icon, String type) {
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-base-color)")
.set("border", "1px solid var(--lumo-contrast-20pct)")
.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");
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-label", label);
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'));" +
" this.style.opacity = '0.5';" +
"});" +
"this.addEventListener('dragend', function(e) {" +
" this.style.opacity = '1';" +
"});");
return template;
}
private VerticalLayout createPropertiesPanelForProfile() {
VerticalLayout panel = new VerticalLayout();
panel.setPadding(false);
panel.setSpacing(true);
panel.setHeightFull();
Span header = new Span("Eigenschaften");
header.getStyle()
.set("font-weight", "bold")
.set("font-size", "var(--lumo-font-size-l)");
Div infoText = new Div();
infoText.setText("Klicken Sie auf ein Element im Canvas, um dessen Eigenschaften zu bearbeiten.");
infoText.getStyle()
.set("color", "var(--lumo-secondary-text-color)")
.set("font-size", "var(--lumo-font-size-s)");
panel.add(header, infoText);
return panel;
}
@ClientCallable
public void updatePropertiesPanel(String elementId, String elementType, String text, Double x, Double y,
Integer fontSize, String color, Double width, Double height) {
getUI().ifPresent(ui -> ui.access(() -> {
propertiesPanelProfile.removeAll();
Span header = new Span("Eigenschaften");
header.getStyle()
.set("font-weight", "bold")
.set("font-size", "var(--lumo-font-size-l)");
// Element Typ Anzeige
Span typeLabel = new Span("Typ: " + elementType);
typeLabel.getStyle().set("font-size", "var(--lumo-font-size-s)");
propertiesPanelProfile.add(header, typeLabel);
// Text Feld (nur für Text-Elemente)
if (!"line".equals(elementType) && !"image".equals(elementType)) {
TextField textField = new TextField("Text");
textField.setValue(text != null ? text : "");
textField.setWidthFull();
textField.addValueChangeListener(e -> {
getElement().executeJs(
"if (window.updateProfileElementText) { window.updateProfileElementText('" + elementId
+ "', $0); }",
e.getValue());
});
propertiesPanelProfile.add(textField);
}
// X Position
TextField xField = new TextField("X Position");
xField.setValue(x != null ? String.valueOf(Math.round(x)) : "0");
xField.setWidthFull();
xField.addValueChangeListener(e -> {
try {
double newX = Double.parseDouble(e.getValue());
getElement().executeJs(
"if (window.updateProfileElementPosition) { window.updateProfileElementPosition('" + elementId
+ "', $0, null); }",
newX);
} catch (NumberFormatException ignored) {
}
});
propertiesPanelProfile.add(xField);
// Y Position
TextField yField = new TextField("Y Position");
yField.setValue(y != null ? String.valueOf(Math.round(y)) : "0");
yField.setWidthFull();
yField.addValueChangeListener(e -> {
try {
double newY = Double.parseDouble(e.getValue());
getElement().executeJs(
"if (window.updateProfileElementPosition) { window.updateProfileElementPosition('" + elementId
+ "', null, $0); }",
newY);
} catch (NumberFormatException ignored) {
}
});
propertiesPanelProfile.add(yField);
// Font Size (nur für Text-Elemente)
if (!"line".equals(elementType) && !"image".equals(elementType)) {
TextField fontSizeField = new TextField("Schriftgröße");
fontSizeField.setValue(fontSize != null ? String.valueOf(fontSize) : "16");
fontSizeField.setWidthFull();
fontSizeField.addValueChangeListener(e -> {
try {
int newSize = Integer.parseInt(e.getValue());
getElement().executeJs(
"if (window.updateProfileElementFontSize) { window.updateProfileElementFontSize('"
+ elementId + "', $0); }",
newSize);
} catch (NumberFormatException ignored) {
}
});
propertiesPanelProfile.add(fontSizeField);
// Farbe
Div colorContainer = new Div();
colorContainer.getStyle()
.set("display", "flex")
.set("align-items", "center")
.set("gap", "var(--lumo-space-s)")
.set("margin-top", "var(--lumo-space-s)");
Span colorLabel = new Span("Farbe");
colorLabel.getStyle().set("font-size", "var(--lumo-font-size-s)");
Input colorPicker = new Input();
colorPicker.setType("color");
colorPicker.setValue(color != null ? color : "#333333");
colorPicker.getStyle()
.set("width", "50px")
.set("height", "32px")
.set("padding", "0")
.set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-m)")
.set("cursor", "pointer");
TextField colorHexField = new TextField();
colorHexField.setValue(color != null ? color : "#333333");
colorHexField.setWidth("100px");
colorHexField.getStyle().set("margin", "0");
colorPicker.addValueChangeListener(e -> {
String newColor = e.getValue();
colorHexField.setValue(newColor);
getElement().executeJs(
"if (window.updateProfileElementColor) { window.updateProfileElementColor('"
+ elementId + "', $0); }",
newColor);
});
colorHexField.addValueChangeListener(e -> {
String newColor = e.getValue();
if (newColor.matches("^#[0-9A-Fa-f]{6}$")) {
colorPicker.setValue(newColor);
getElement().executeJs(
"if (window.updateProfileElementColor) { window.updateProfileElementColor('"
+ elementId + "', $0); }",
newColor);
}
});
colorContainer.add(colorLabel, colorPicker, colorHexField);
propertiesPanelProfile.add(colorContainer);
}
// Löschen Button
Button deleteButton = new Button("Element löschen", new Icon(VaadinIcon.TRASH));
deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
deleteButton.setWidthFull();
deleteButton.addClickListener(e -> {
getElement().executeJs(
"if (window.deleteProfileElement) { window.deleteProfileElement('" + elementId
+ "'); }");
resetPropertiesPanel();
});
propertiesPanelProfile.add(deleteButton);
}));
}
@ClientCallable
public void resetPropertiesPanel() {
getUI().ifPresent(ui -> ui.access(() -> {
if (propertiesPanelProfile == null) return;
propertiesPanelProfile.removeAll();
Span header = new Span("Eigenschaften");
header.getStyle()
.set("font-weight", "bold")
.set("font-size", "var(--lumo-font-size-l)");
Div infoText = new Div();
infoText.setText("Klicken Sie auf ein Element im Canvas, um dessen Eigenschaften zu bearbeiten.");
infoText.getStyle()
.set("color", "var(--lumo-secondary-text-color)")
.set("font-size", "var(--lumo-font-size-s)");
propertiesPanelProfile.add(header, infoText);
}));
}
} }

View File

@@ -2,15 +2,10 @@ package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.AttachEvent; import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.ClientCallable; import com.vaadin.flow.component.ClientCallable;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dependency.JsModule; import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.page.PendingJavaScriptResult;
import com.vaadin.flow.server.Command;
import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.icon.VaadinIcon;
@@ -18,17 +13,20 @@ import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.html.Input;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.html.IFrame;
import com.vaadin.flow.server.StreamResource;
import elemental.json.JsonValue;
import elemental.json.JsonType;
import com.vaadin.flow.component.upload.Upload; import com.vaadin.flow.component.upload.Upload;
import com.vaadin.flow.component.upload.receivers.MemoryBuffer; import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.StreamResource;
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout; import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
import de.assecutor.votianlt.service.CustomerInvoiceService; import de.assecutor.votianlt.service.CustomerInvoiceService;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
import java.io.ByteArrayInputStream;
@Route(value = "invoice-generator", layout = AdminLayout.class) @Route(value = "invoice-generator", layout = AdminLayout.class)
@PageTitle("Rechnungsgenerator") @PageTitle("Rechnungsgenerator")
@RolesAllowed("ADMIN") @RolesAllowed("ADMIN")
@@ -48,41 +46,59 @@ public class InvoiceGeneratorView extends VerticalLayout {
setSpacing(false); setSpacing(false);
setPadding(false); setPadding(false);
getStyle().set("margin", "14px"); setMargin(false);
setWidth("100%"); setWidth("100%");
setHeight("100%"); setHeight("100%");
getStyle()
H2 title = new H2("Rechnungsgenerator"); .set("overflow", "hidden")
add(title); .set("box-sizing", "border-box")
.set("display", "flex")
.set("flex-direction", "column");
// Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften) // Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften)
HorizontalLayout mainLayout = new HorizontalLayout(); HorizontalLayout mainLayout = new HorizontalLayout();
mainLayout.setWidth("100%"); mainLayout.setWidth("100%");
mainLayout.setHeight("calc(100vh - 150px)"); mainLayout.setHeight("calc(100vh - 60px)"); // Abzug für Action-Buttons
mainLayout.setSpacing(true); mainLayout.setSpacing(true);
mainLayout.getStyle().set("overflow", "hidden");
// Linke Seite: Templates/Bausteine // Linke Seite: Templates/Bausteine
VerticalLayout leftPanel = createTemplatesPanel(); VerticalLayout leftPanel = createTemplatesPanel();
leftPanel.setWidth("250px"); leftPanel.setWidth("250px");
leftPanel.setHeightFull(); leftPanel.setHeightFull();
leftPanel.getStyle()
.set("flex-shrink", "0")
.set("min-width", "250px")
.set("overflow", "auto");
// Mitte: Canvas mit Konva.js // Mitte: Canvas mit Konva.js
VerticalLayout centerPanel = createCanvasPanel(); VerticalLayout centerPanel = createCanvasPanel();
centerPanel.setWidth("60%"); centerPanel.setWidth("60%");
centerPanel.setHeightFull(); centerPanel.setHeightFull();
centerPanel.getStyle()
.set("flex-grow", "1")
.set("min-width", "0");
// Rechte Seite: Eigenschaften // Rechte Seite: Eigenschaften
propertiesPanel = createPropertiesPanel(); propertiesPanel = createPropertiesPanel();
propertiesPanel.setWidth("300px"); propertiesPanel.setWidth("300px");
propertiesPanel.setHeightFull(); propertiesPanel.setHeightFull();
propertiesPanel.getStyle()
.set("flex-shrink", "0")
.set("min-width", "300px")
.set("overflow", "auto");
mainLayout.add(leftPanel, centerPanel, propertiesPanel); mainLayout.add(leftPanel, centerPanel, propertiesPanel);
mainLayout.expand(centerPanel); mainLayout.expand(centerPanel);
add(mainLayout); add(mainLayout);
// Aktions-Buttons unter dem Canvas // Aktions-Buttons unter dem Canvas (fixe Höhe)
HorizontalLayout actionLayout = createActionButtons(); HorizontalLayout actionLayout = createActionButtons();
actionLayout.setHeight("60px");
actionLayout.getStyle()
.set("flex-shrink", "0")
.set("padding", "0 var(--lumo-space-m)");
add(actionLayout); add(actionLayout);
} }
@@ -104,9 +120,11 @@ public class InvoiceGeneratorView extends VerticalLayout {
VerticalLayout panel = new VerticalLayout(); VerticalLayout panel = new VerticalLayout();
panel.setPadding(true); panel.setPadding(true);
panel.setSpacing(true); panel.setSpacing(true);
panel.setHeightFull();
panel.getStyle() panel.getStyle()
.set("background-color", "var(--lumo-contrast-5pct)") .set("background-color", "var(--lumo-contrast-5pct)")
.set("border-radius", "var(--lumo-border-radius-m)"); .set("border-radius", "var(--lumo-border-radius-m)")
.set("overflow", "auto");
Span header = new Span("Textbausteine"); Span header = new Span("Textbausteine");
header.getStyle() header.getStyle()
@@ -221,9 +239,11 @@ public class InvoiceGeneratorView extends VerticalLayout {
VerticalLayout panel = new VerticalLayout(); VerticalLayout panel = new VerticalLayout();
panel.setPadding(true); panel.setPadding(true);
panel.setSpacing(true); panel.setSpacing(true);
panel.setHeightFull();
panel.getStyle() panel.getStyle()
.set("background-color", "var(--lumo-contrast-5pct)") .set("background-color", "var(--lumo-contrast-5pct)")
.set("border-radius", "var(--lumo-border-radius-m)"); .set("border-radius", "var(--lumo-border-radius-m)")
.set("overflow", "auto");
Span header = new Span("Eigenschaften"); Span header = new Span("Eigenschaften");
header.getStyle() header.getStyle()
@@ -246,7 +266,9 @@ public class InvoiceGeneratorView extends VerticalLayout {
HorizontalLayout layout = new HorizontalLayout(); HorizontalLayout layout = new HorizontalLayout();
layout.setWidth("100%"); layout.setWidth("100%");
layout.setJustifyContentMode(JustifyContentMode.END); layout.setJustifyContentMode(JustifyContentMode.END);
layout.setPadding(true); layout.setPadding(false);
layout.setSpacing(true);
layout.setAlignItems(Alignment.CENTER);
Button clearButton = new Button("Canvas leeren", new Icon(VaadinIcon.TRASH)); Button clearButton = new Button("Canvas leeren", new Icon(VaadinIcon.TRASH));
clearButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); clearButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
@@ -257,9 +279,7 @@ public class InvoiceGeneratorView extends VerticalLayout {
Button previewButton = new Button("Vorschau", new Icon(VaadinIcon.EYE)); Button previewButton = new Button("Vorschau", new Icon(VaadinIcon.EYE));
previewButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST); previewButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST);
previewButton.addClickListener(e -> { previewButton.addClickListener(e -> generatePreviewPdf());
getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.generatePreview(); }");
});
Button saveTemplateButton = new Button("Template speichern", new Icon(VaadinIcon.DOWNLOAD)); Button saveTemplateButton = new Button("Template speichern", new Icon(VaadinIcon.DOWNLOAD));
saveTemplateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); saveTemplateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
@@ -294,6 +314,92 @@ public class InvoiceGeneratorView extends VerticalLayout {
} }
} }
private void generatePreviewPdf() {
try {
getElement().executeJs(
"if (window.invoiceGenerator) { return window.invoiceGenerator.exportTemplateJson(); } else { return null; }")
.then(result -> {
if (result == null) {
showNotification("Fehler: Canvas-Daten konnten nicht gelesen werden");
return;
}
try {
String templateData;
if (result instanceof JsonValue) {
JsonValue jsonValue = (JsonValue) result;
if (jsonValue.getType() == JsonType.STRING) {
templateData = jsonValue.asString();
} else {
templateData = jsonValue.toJson();
}
} else {
templateData = result.toString();
}
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData);
showPdfInDialog(pdfBytes);
} catch (Exception ex) {
showNotification("Fehler beim Generieren der Vorschau: " + ex.getMessage());
}
});
} catch (Exception ex) {
showNotification("Fehler: " + ex.getMessage());
}
}
private void showPdfInDialog(byte[] pdfBytes) {
// Create a stream resource for the PDF
StreamResource resource = new StreamResource("preview.pdf", () -> new java.io.ByteArrayInputStream(pdfBytes));
resource.setContentType("application/pdf");
resource.setCacheTime(0);
// Create dialog
Dialog pdfDialog = new Dialog();
pdfDialog.setHeaderTitle("PDF Vorschau");
pdfDialog.setWidth("90vw");
pdfDialog.setHeight("90vh");
// Create a Div to hold the PDF viewer
Div pdfContainer = new Div();
pdfContainer.setWidth("100%");
pdfContainer.setHeight("100%");
pdfContainer.getStyle()
.set("display", "flex")
.set("flex-direction", "column")
.set("overflow", "hidden");
// Use an iframe with data URL for PDF display
String base64Pdf = java.util.Base64.getEncoder().encodeToString(pdfBytes);
String dataUrl = "data:application/pdf;base64," + base64Pdf;
IFrame pdfFrame = new IFrame();
pdfFrame.setWidth("100%");
pdfFrame.setHeight("100%");
pdfFrame.getElement().setAttribute("src", dataUrl);
pdfFrame.getStyle()
.set("border", "none")
.set("flex-grow", "1");
pdfContainer.add(pdfFrame);
// 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 = 'vorschau.pdf';" +
"link.click();");
});
downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
pdfDialog.add(pdfContainer);
pdfDialog.getFooter().add(downloadButton, closeButton);
pdfDialog.open();
}
private void showNotification(String message) { private void showNotification(String message) {
Notification.show(message, 3000, Notification.Position.BOTTOM_CENTER); Notification.show(message, 3000, Notification.Position.BOTTOM_CENTER);
} }
@@ -413,6 +519,114 @@ public class InvoiceGeneratorView extends VerticalLayout {
} }
}); });
propertiesPanel.add(fontSizeField); propertiesPanel.add(fontSizeField);
// Schriftfarbe mit Dialog
Span colorLabel = new Span("Schriftfarbe");
colorLabel.getStyle().set("font-size", "var(--lumo-font-size-s)");
propertiesPanel.add(colorLabel);
// Aktuelle Farbe anzeigen und klickbar machen
String currentColor = color != null ? color : "#333333";
HorizontalLayout colorPreviewLayout = new HorizontalLayout();
colorPreviewLayout.setSpacing(true);
colorPreviewLayout.setAlignItems(Alignment.CENTER);
colorPreviewLayout.setWidthFull();
colorPreviewLayout.getStyle().set("cursor", "pointer");
// Farbvorschau-Box
Div colorPreview = new Div();
colorPreview.getStyle()
.set("width", "40px")
.set("height", "30px")
.set("background-color", currentColor)
.set("border", "1px solid var(--lumo-contrast-30pct)")
.set("border-radius", "var(--lumo-border-radius-m)");
Span colorHexLabel = new Span(currentColor);
colorHexLabel.getStyle()
.set("font-family", "monospace")
.set("font-size", "var(--lumo-font-size-s)");
colorPreviewLayout.add(colorPreview, colorHexLabel);
// Color Picker Dialog
Dialog colorDialog = new Dialog();
colorDialog.setHeaderTitle("Schriftfarbe wählen");
VerticalLayout dialogLayout = new VerticalLayout();
dialogLayout.setSpacing(true);
dialogLayout.setPadding(true);
// Color Picker im Dialog
Input dialogColorPicker = new Input();
dialogColorPicker.setType("color");
dialogColorPicker.setValue(currentColor);
dialogColorPicker.getStyle()
.set("width", "100%")
.set("height", "50px")
.set("padding", "0");
// Hex-Eingabe im Dialog
TextField dialogHexField = new TextField("Hex-Farbwert");
dialogHexField.setValue(currentColor);
dialogHexField.setWidthFull();
// Sync zwischen Color Picker und Hex-Feld
dialogColorPicker.addValueChangeListener(e -> {
dialogHexField.setValue(e.getValue());
});
dialogHexField.addValueChangeListener(e -> {
String newColor = e.getValue();
if (newColor.matches("^#[0-9A-Fa-f]{6}$")) {
dialogColorPicker.setValue(newColor);
}
});
dialogLayout.add(dialogColorPicker, dialogHexField);
colorDialog.add(dialogLayout);
// Dialog Buttons
Button dialogCancelButton = new Button("Abbrechen", e -> {
colorDialog.close();
// Reset to original values
dialogColorPicker.setValue(currentColor);
dialogHexField.setValue(currentColor);
});
dialogCancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
Button dialogApplyButton = new Button("Übernehmen", e -> {
String newColor = dialogColorPicker.getValue();
// Update preview
colorPreview.getStyle().set("background-color", newColor);
colorHexLabel.setText(newColor);
// Apply to element
getElement().executeJs(
"if (window.invoiceGenerator) { window.invoiceGenerator.updateElementColor('"
+ elementId + "', $0); }",
newColor);
colorDialog.close();
showNotification("Farbe übernommen");
});
dialogApplyButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
colorDialog.getFooter().add(dialogCancelButton, dialogApplyButton);
// Öffne Dialog beim Klick auf die Vorschau
Runnable openColorDialog = () -> {
// Aktualisiere Dialog mit aktuellem Wert
String actualColor = colorHexLabel.getText();
dialogColorPicker.setValue(actualColor);
dialogHexField.setValue(actualColor);
colorDialog.open();
};
colorPreviewLayout.addClickListener(e -> openColorDialog.run());
colorPreview.addClickListener(e -> openColorDialog.run());
colorHexLabel.addClickListener(e -> openColorDialog.run());
propertiesPanel.add(colorPreviewLayout);
} }
// Löschen Button // Löschen Button

View File

@@ -242,4 +242,91 @@ public class CustomerInvoiceService {
return baos.toByteArray(); return baos.toByteArray();
} }
/**
* Generate a PDF preview from canvas template data.
* Creates an HTML representation of the canvas elements and converts it to PDF.
*/
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData) 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);
com.fasterxml.jackson.databind.JsonNode elements = rootNode.get("elements");
// 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; }");
htmlBuilder.append(".text { white-space: pre-wrap; word-wrap: break-word; }");
htmlBuilder.append(".line { border-top: 1px solid #333; }");
htmlBuilder.append(".image { max-width: 100%; max-height: 100%; }");
htmlBuilder.append("</style>");
htmlBuilder.append("</head><body>");
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("") : "";
double x = element.has("x") ? element.get("x").asDouble(0) : 0;
double y = element.has("y") ? element.get("y").asDouble(0) : 0;
double width = element.has("width") ? element.get("width").asDouble(150) : 150;
double height = element.has("height") ? element.get("height").asDouble(30) : 30;
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 canvas coordinates to mm (assuming 96 DPI)
double mmX = x * 25.4 / 96;
double mmY = y * 25.4 / 96;
double mmWidth = width * 25.4 / 96;
double mmHeight = height * 25.4 / 96;
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("color:").append(color).append(";");
if (!fontStyle.isEmpty()) {
if (fontStyle.contains("bold")) htmlBuilder.append("font-weight:bold;");
}
htmlBuilder.append("'");
htmlBuilder.append(">");
// Escape HTML special characters in text
text = text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#x27;");
if ("line".equals(type)) {
htmlBuilder.append("<hr style='margin:0;border:none;border-top:1px solid #333;height:0;'/>");
} else if ("image".equals(type)) {
if (element.has("imageData")) {
String imageData = element.get("imageData").asText();
htmlBuilder.append("<img src='").append(imageData).append("' style='max-width:100%;max-height:100%;' />");
} else {
htmlBuilder.append("[Bild]");
}
} else {
htmlBuilder.append(text);
}
htmlBuilder.append("</div>");
}
}
htmlBuilder.append("</body></html>");
// Generate PDF from HTML
return generatePdfFromHtmlString(htmlBuilder.toString());
}
} }