Erweiterungen
This commit is contained in:
Binary file not shown.
Binary file not shown.
721
src/main/frontend/invoice-generator/invoice-generator.js
Normal file
721
src/main/frontend/invoice-generator/invoice-generator.js
Normal file
@@ -0,0 +1,721 @@
|
||||
// Invoice Generator - Native HTML5 Canvas Implementation
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
class InvoiceGenerator {
|
||||
constructor() {
|
||||
this.canvas = null;
|
||||
this.ctx = null;
|
||||
this.elements = new Map();
|
||||
this.selectedElement = null;
|
||||
this.elementCounter = 0;
|
||||
this.isDragging = false;
|
||||
this.isResizing = false;
|
||||
this.dragStart = { x: 0, y: 0 };
|
||||
this.elementStart = { x: 0, y: 0, w: 0, h: 0 };
|
||||
this.activeHandle = null;
|
||||
this.container = null;
|
||||
|
||||
// Page dimensions (A4 at 96 DPI)
|
||||
this.pageX = 50;
|
||||
this.pageY = 30;
|
||||
this.pageWidth = 794;
|
||||
this.pageHeight = 1123;
|
||||
|
||||
// Handle configuration
|
||||
this.handleSize = 10;
|
||||
this.handlePadding = 3;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.container = document.getElementById('invoice-canvas-container');
|
||||
if (!this.container) {
|
||||
console.error('Canvas container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create canvas
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.style.display = 'block';
|
||||
this.updateCanvasSize();
|
||||
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.container.innerHTML = '';
|
||||
this.container.appendChild(this.canvas);
|
||||
|
||||
// Bind events
|
||||
this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
|
||||
this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this));
|
||||
this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this));
|
||||
this.canvas.addEventListener('mouseleave', this.onMouseUp.bind(this));
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.updateCanvasSize();
|
||||
this.draw();
|
||||
});
|
||||
|
||||
this.draw();
|
||||
console.log('Invoice generator initialized');
|
||||
}
|
||||
|
||||
updateCanvasSize() {
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.canvas.width = rect.width;
|
||||
this.canvas.height = rect.height;
|
||||
this.pageX = Math.max(20, (this.canvas.width - this.pageWidth) / 2);
|
||||
}
|
||||
|
||||
getMousePos(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
return {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top
|
||||
};
|
||||
}
|
||||
|
||||
// Check if point is inside element
|
||||
hitTest(x, y, el) {
|
||||
const w = el.width || 150;
|
||||
const h = el.height || 30;
|
||||
return x >= el.x && x <= el.x + w && y >= el.y && y <= el.y + h;
|
||||
}
|
||||
|
||||
// Check if point is on a resize handle
|
||||
getHandleAt(x, y, el) {
|
||||
if (!el) return null;
|
||||
|
||||
const w = el.width || 150;
|
||||
const h = el.height || 30;
|
||||
const hs = this.handleSize + this.handlePadding;
|
||||
|
||||
// Define handle positions (center points)
|
||||
const handles = {
|
||||
'nw': { x: el.x, y: el.y },
|
||||
'n': { x: el.x + w / 2, y: el.y },
|
||||
'ne': { x: el.x + w, y: el.y },
|
||||
'w': { x: el.x, y: el.y + h / 2 },
|
||||
'e': { x: el.x + w, y: el.y + h / 2 },
|
||||
'sw': { x: el.x, y: el.y + h },
|
||||
's': { x: el.x + w / 2, y: el.y + h },
|
||||
'se': { x: el.x + w, y: el.y + h }
|
||||
};
|
||||
|
||||
for (const [name, pos] of Object.entries(handles)) {
|
||||
const dx = x - pos.x;
|
||||
const dy = y - pos.y;
|
||||
if (Math.abs(dx) <= hs && Math.abs(dy) <= hs) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onMouseDown(e) {
|
||||
const pos = this.getMousePos(e);
|
||||
|
||||
// First check if clicking on a handle of selected element
|
||||
if (this.selectedElement) {
|
||||
const handle = this.getHandleAt(pos.x, pos.y, this.selectedElement);
|
||||
if (handle) {
|
||||
console.log('Resize handle clicked:', handle);
|
||||
this.isResizing = true;
|
||||
this.activeHandle = handle;
|
||||
this.dragStart = { x: pos.x, y: pos.y };
|
||||
this.elementStart = {
|
||||
x: this.selectedElement.x,
|
||||
y: this.selectedElement.y,
|
||||
w: this.selectedElement.width || 100,
|
||||
h: this.selectedElement.height || 30
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if clicking on an element
|
||||
let clickedElement = null;
|
||||
for (const el of Array.from(this.elements.values()).reverse()) {
|
||||
if (this.hitTest(pos.x, pos.y, el)) {
|
||||
clickedElement = el;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (clickedElement) {
|
||||
this.selectElement(clickedElement.id);
|
||||
this.isDragging = true;
|
||||
this.dragStart = { x: pos.x, y: pos.y };
|
||||
this.elementStart = { x: clickedElement.x, y: clickedElement.y };
|
||||
this.canvas.style.cursor = 'move';
|
||||
} else {
|
||||
this.deselectAll();
|
||||
}
|
||||
|
||||
this.draw();
|
||||
}
|
||||
|
||||
onMouseMove(e) {
|
||||
const pos = this.getMousePos(e);
|
||||
|
||||
if (this.isResizing && this.selectedElement) {
|
||||
this.doResize(pos.x, pos.y);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isDragging && this.selectedElement) {
|
||||
const dx = pos.x - this.dragStart.x;
|
||||
const dy = pos.y - this.dragStart.y;
|
||||
|
||||
this.selectedElement.x = this.elementStart.x + dx;
|
||||
this.selectedElement.y = this.elementStart.y + dy;
|
||||
|
||||
// Constrain to page
|
||||
this.selectedElement.x = Math.max(this.pageX, Math.min(this.selectedElement.x,
|
||||
this.pageX + this.pageWidth - (this.selectedElement.width || 100)));
|
||||
this.selectedElement.y = Math.max(this.pageY, Math.min(this.selectedElement.y,
|
||||
this.pageY + this.pageHeight - (this.selectedElement.height || 30)));
|
||||
|
||||
this.draw();
|
||||
this.notifyChange();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update cursor
|
||||
if (this.selectedElement) {
|
||||
const handle = this.getHandleAt(pos.x, pos.y, this.selectedElement);
|
||||
if (handle) {
|
||||
const cursors = {
|
||||
'nw': 'nw-resize', 'ne': 'ne-resize', 'sw': 'sw-resize', 'se': 'se-resize',
|
||||
'n': 'ns-resize', 's': 'ns-resize', 'e': 'ew-resize', 'w': 'ew-resize'
|
||||
};
|
||||
this.canvas.style.cursor = cursors[handle] || 'default';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check hover over elements
|
||||
let hovering = false;
|
||||
for (const el of Array.from(this.elements.values()).reverse()) {
|
||||
if (this.hitTest(pos.x, pos.y, el)) {
|
||||
hovering = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.canvas.style.cursor = hovering ? 'move' : 'default';
|
||||
}
|
||||
|
||||
onMouseUp(e) {
|
||||
this.isDragging = false;
|
||||
this.isResizing = false;
|
||||
this.activeHandle = null;
|
||||
this.canvas.style.cursor = 'default';
|
||||
}
|
||||
|
||||
doResize(mouseX, mouseY) {
|
||||
if (!this.selectedElement || !this.activeHandle) return;
|
||||
|
||||
const el = this.selectedElement;
|
||||
el.manuallyResized = true; // Mark as manually resized
|
||||
|
||||
const start = this.elementStart;
|
||||
const minSize = 20;
|
||||
|
||||
// Calculate new dimensions based on handle
|
||||
switch (this.activeHandle) {
|
||||
case 'se':
|
||||
el.width = Math.max(minSize, mouseX - start.x);
|
||||
el.height = Math.max(minSize, mouseY - start.y);
|
||||
break;
|
||||
|
||||
case 'sw':
|
||||
const newWsw = start.w + (start.x - mouseX);
|
||||
if (newWsw >= minSize) {
|
||||
el.x = mouseX;
|
||||
el.width = newWsw;
|
||||
}
|
||||
el.height = Math.max(minSize, mouseY - start.y);
|
||||
break;
|
||||
|
||||
case 'ne':
|
||||
el.width = Math.max(minSize, mouseX - start.x);
|
||||
const newHne = start.h + (start.y - mouseY);
|
||||
if (newHne >= minSize) {
|
||||
el.y = mouseY;
|
||||
el.height = newHne;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'nw':
|
||||
const newWnw = start.w + (start.x - mouseX);
|
||||
const newHnw = start.h + (start.y - mouseY);
|
||||
if (newWnw >= minSize) {
|
||||
el.x = mouseX;
|
||||
el.width = newWnw;
|
||||
}
|
||||
if (newHnw >= minSize) {
|
||||
el.y = mouseY;
|
||||
el.height = newHnw;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'e':
|
||||
el.width = Math.max(minSize, mouseX - start.x);
|
||||
break;
|
||||
|
||||
case 'w':
|
||||
const newWw = start.w + (start.x - mouseX);
|
||||
if (newWw >= minSize) {
|
||||
el.x = mouseX;
|
||||
el.width = newWw;
|
||||
}
|
||||
break;
|
||||
|
||||
case 's':
|
||||
el.height = Math.max(minSize, mouseY - start.y);
|
||||
break;
|
||||
|
||||
case 'n':
|
||||
const newHn = start.h + (start.y - mouseY);
|
||||
if (newHn >= minSize) {
|
||||
el.y = mouseY;
|
||||
el.height = newHn;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.draw();
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
notifyChange() {
|
||||
if (this.selectedElement && window.invoiceGeneratorView?.$server) {
|
||||
const el = this.selectedElement;
|
||||
window.invoiceGeneratorView.$server.updatePropertiesPanel(
|
||||
el.id, el.type, el.text || '', el.x, el.y, el.fontSize || 14, el.color || '#000000'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (!this.ctx) return;
|
||||
|
||||
const ctx = this.ctx;
|
||||
|
||||
// Clear
|
||||
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Background
|
||||
ctx.fillStyle = '#e8e8e8';
|
||||
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Page shadow
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.2)';
|
||||
ctx.fillRect(this.pageX + 4, this.pageY + this.pageHeight, this.pageWidth, 4);
|
||||
ctx.fillRect(this.pageX + this.pageWidth, this.pageY + 4, 4, this.pageHeight);
|
||||
|
||||
// Page
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(this.pageX, this.pageY, this.pageWidth, this.pageHeight);
|
||||
ctx.strokeStyle = '#cccccc';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(this.pageX, this.pageY, this.pageWidth, this.pageHeight);
|
||||
|
||||
// Draw elements
|
||||
this.elements.forEach(el => this.drawElement(el));
|
||||
|
||||
// Draw selection
|
||||
if (this.selectedElement) {
|
||||
this.drawSelection(this.selectedElement);
|
||||
}
|
||||
}
|
||||
|
||||
drawImagePlaceholder(ctx, el) {
|
||||
ctx.fillStyle = '#f0f0f0';
|
||||
ctx.fillRect(el.x, el.y, el.width || 100, el.height || 100);
|
||||
ctx.strokeStyle = '#999999';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(el.x, el.y, el.width || 100, el.height || 100);
|
||||
ctx.fillStyle = '#666666';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('Bild', el.x + (el.width || 100) / 2, el.y + (el.height || 100) / 2);
|
||||
}
|
||||
|
||||
drawElement(el) {
|
||||
const ctx = this.ctx;
|
||||
ctx.save();
|
||||
|
||||
switch (el.type) {
|
||||
case 'line':
|
||||
ctx.strokeStyle = el.color || '#333333';
|
||||
ctx.lineWidth = el.strokeWidth || 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(el.x, el.y);
|
||||
ctx.lineTo(el.x + (el.width || 200), el.y);
|
||||
ctx.stroke();
|
||||
break;
|
||||
|
||||
case 'image':
|
||||
if (el.imageData) {
|
||||
// Draw actual image
|
||||
const img = el.imageObj;
|
||||
if (img && img.complete) {
|
||||
// Draw image scaled to fit element while maintaining aspect ratio
|
||||
const imgAspect = img.width / img.height;
|
||||
const elAspect = (el.width || 100) / (el.height || 100);
|
||||
|
||||
let drawWidth, drawHeight, drawX, drawY;
|
||||
|
||||
if (imgAspect > elAspect) {
|
||||
// Image is wider than element
|
||||
drawWidth = el.width || 100;
|
||||
drawHeight = drawWidth / imgAspect;
|
||||
drawX = el.x;
|
||||
drawY = el.y + ((el.height || 100) - drawHeight) / 2;
|
||||
} else {
|
||||
// Image is taller than element
|
||||
drawHeight = el.height || 100;
|
||||
drawWidth = drawHeight * imgAspect;
|
||||
drawX = el.x + ((el.width || 100) - drawWidth) / 2;
|
||||
drawY = el.y;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
|
||||
|
||||
// Draw border
|
||||
ctx.strokeStyle = '#cccccc';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(el.x, el.y, el.width || 100, el.height || 100);
|
||||
} else {
|
||||
// Image still loading or failed
|
||||
this.drawImagePlaceholder(ctx, el);
|
||||
}
|
||||
} else {
|
||||
// No image uploaded yet - draw placeholder
|
||||
this.drawImagePlaceholder(ctx, el);
|
||||
}
|
||||
break;
|
||||
|
||||
default: // text elements
|
||||
this.drawTextElement(el, ctx);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Wrap text to fit within maxWidth
|
||||
wrapText(ctx, text, maxWidth) {
|
||||
const words = text.split(' ');
|
||||
const lines = [];
|
||||
let currentLine = words[0];
|
||||
|
||||
for (let i = 1; i < words.length; i++) {
|
||||
const word = words[i];
|
||||
const width = ctx.measureText(currentLine + ' ' + word).width;
|
||||
if (width < maxWidth) {
|
||||
currentLine += ' ' + word;
|
||||
} else {
|
||||
lines.push(currentLine);
|
||||
currentLine = word;
|
||||
}
|
||||
}
|
||||
lines.push(currentLine);
|
||||
return lines;
|
||||
}
|
||||
|
||||
drawTextElement(el, ctx) {
|
||||
ctx.font = `${el.fontStyle || ''} ${el.fontSize || 14}px ${el.fontFamily || 'Arial'}`;
|
||||
ctx.fillStyle = el.color || '#333333';
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
const maxWidth = el.width || 150;
|
||||
const lineHeight = (el.fontSize || 14) * 1.2;
|
||||
const maxHeight = el.height || 1000;
|
||||
|
||||
// Calculate max lines that fit
|
||||
const maxLines = Math.floor(maxHeight / lineHeight);
|
||||
|
||||
// Split by explicit newlines first, then wrap each line
|
||||
const explicitLines = (el.text || '').split('\n');
|
||||
const allLines = [];
|
||||
|
||||
for (const line of explicitLines) {
|
||||
const metrics = ctx.measureText(line);
|
||||
if (metrics.width <= maxWidth) {
|
||||
allLines.push(line);
|
||||
} else {
|
||||
const wrapped = this.wrapText(ctx, line, maxWidth);
|
||||
allLines.push(...wrapped);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if we need ellipsis
|
||||
const needsEllipsis = allLines.length > maxLines;
|
||||
const linesToDraw = needsEllipsis ? maxLines : allLines.length;
|
||||
|
||||
// Draw lines
|
||||
let y = el.y;
|
||||
for (let i = 0; i < linesToDraw; i++) {
|
||||
const line = allLines[i];
|
||||
|
||||
// If this is the last line and we need ellipsis
|
||||
if (needsEllipsis && i === linesToDraw - 1) {
|
||||
const ellipsis = '...';
|
||||
// Check if the line with ellipsis would fit
|
||||
if (ctx.measureText(line + ellipsis).width <= maxWidth) {
|
||||
ctx.fillText(line + ellipsis, el.x, y);
|
||||
} else {
|
||||
// Need to trim the line to fit ellipsis
|
||||
let trimmed = line;
|
||||
while (trimmed.length > 0 && ctx.measureText(trimmed + ellipsis).width > maxWidth) {
|
||||
trimmed = trimmed.slice(0, -1);
|
||||
}
|
||||
ctx.fillText(trimmed + ellipsis, el.x, y);
|
||||
}
|
||||
} else {
|
||||
ctx.fillText(line, el.x, y);
|
||||
}
|
||||
y += lineHeight;
|
||||
}
|
||||
|
||||
// Update height based on actual content if not manually resized
|
||||
if (!el.manuallyResized) {
|
||||
el.height = Math.max(lineHeight, allLines.length * lineHeight);
|
||||
}
|
||||
}
|
||||
|
||||
drawSelection(el) {
|
||||
const ctx = this.ctx;
|
||||
const x = el.x;
|
||||
const y = el.y;
|
||||
const w = el.width || 150;
|
||||
const h = el.height || 30;
|
||||
const hs = this.handleSize;
|
||||
|
||||
// Selection border
|
||||
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;
|
||||
|
||||
const positions = [
|
||||
[x - hs/2, y - hs/2], // nw
|
||||
[x + w/2 - hs/2, y - hs/2], // n
|
||||
[x + w - hs/2, y - hs/2], // ne
|
||||
[x - hs/2, y + h/2 - hs/2], // w
|
||||
[x + w - hs/2, y + h/2 - hs/2], // e
|
||||
[x - hs/2, y + h - hs/2], // sw
|
||||
[x + w/2 - hs/2, y + h - hs/2], // s
|
||||
[x + w - hs/2, y + h - hs/2] // se
|
||||
];
|
||||
|
||||
positions.forEach(([hx, hy]) => {
|
||||
ctx.fillRect(hx, hy, hs, hs);
|
||||
ctx.strokeRect(hx, hy, hs, hs);
|
||||
});
|
||||
}
|
||||
|
||||
addElement(type, label, dropX, dropY) {
|
||||
this.elementCounter++;
|
||||
const id = `element-${this.elementCounter}`;
|
||||
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
const x = Math.max(this.pageX + 10, dropX - rect.left - 50);
|
||||
const y = Math.max(this.pageY + 10, dropY - rect.top - 15);
|
||||
|
||||
let el = {
|
||||
id, type, x, y,
|
||||
width: 150,
|
||||
height: 30,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Arial',
|
||||
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':
|
||||
case 'company':
|
||||
el.text = type === 'customer' ? 'Kundenname\nStraße Nr.\nPLZ Ort' : '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;
|
||||
}
|
||||
|
||||
this.elements.set(id, el);
|
||||
this.selectElement(id);
|
||||
this.draw();
|
||||
}
|
||||
|
||||
selectElement(id) {
|
||||
this.selectedElement = this.elements.get(id) || null;
|
||||
this.draw();
|
||||
if (this.selectedElement) {
|
||||
this.notifyChange();
|
||||
}
|
||||
}
|
||||
|
||||
deselectAll() {
|
||||
this.selectedElement = null;
|
||||
this.draw();
|
||||
if (window.invoiceGeneratorView?.$server) {
|
||||
window.invoiceGeneratorView.$server.resetPropertiesPanel();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementText(id, text) {
|
||||
const el = this.elements.get(id);
|
||||
if (el) {
|
||||
el.text = text;
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementPosition(id, x, y) {
|
||||
const el = this.elements.get(id);
|
||||
if (el) {
|
||||
if (x !== null) el.x = x;
|
||||
if (y !== null) el.y = y;
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementFontSize(id, size) {
|
||||
const el = this.elements.get(id);
|
||||
if (el) {
|
||||
el.fontSize = size;
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementImage(id, imageDataUrl) {
|
||||
const el = this.elements.get(id);
|
||||
if (el && el.type === 'image') {
|
||||
el.imageData = imageDataUrl;
|
||||
|
||||
// Create image object
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
el.imageObj = img;
|
||||
this.draw();
|
||||
};
|
||||
img.onerror = () => {
|
||||
console.error('Failed to load image');
|
||||
el.imageData = null;
|
||||
el.imageObj = null;
|
||||
this.draw();
|
||||
};
|
||||
img.src = imageDataUrl;
|
||||
}
|
||||
}
|
||||
|
||||
deleteElement(id) {
|
||||
this.elements.delete(id);
|
||||
if (this.selectedElement?.id === id) {
|
||||
this.selectedElement = null;
|
||||
}
|
||||
this.draw();
|
||||
}
|
||||
|
||||
clearCanvas() {
|
||||
this.elements.clear();
|
||||
this.selectedElement = null;
|
||||
this.draw();
|
||||
}
|
||||
|
||||
getCanvasData() {
|
||||
return {
|
||||
elements: Array.from(this.elements.values())
|
||||
};
|
||||
}
|
||||
|
||||
exportTemplate() {
|
||||
const data = JSON.stringify(this.getCanvasData(), null, 2);
|
||||
const blob = new Blob([data], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'template.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
generatePreview() {
|
||||
const temp = document.createElement('canvas');
|
||||
temp.width = this.pageWidth;
|
||||
temp.height = this.pageHeight;
|
||||
const tctx = temp.getContext('2d');
|
||||
|
||||
tctx.fillStyle = '#ffffff';
|
||||
tctx.fillRect(0, 0, temp.width, temp.height);
|
||||
|
||||
// Draw elements offset by page position
|
||||
const originalPageX = this.pageX;
|
||||
const originalPageY = this.pageY;
|
||||
this.pageX = 0;
|
||||
this.pageY = 0;
|
||||
|
||||
this.elements.forEach(el => {
|
||||
const ox = el.x, oy = el.y;
|
||||
el.x = ox - originalPageX;
|
||||
el.y = oy - originalPageY;
|
||||
this.drawElement.call({ ctx: tctx, elements: this.elements }, el);
|
||||
el.x = ox;
|
||||
el.y = oy;
|
||||
});
|
||||
|
||||
this.pageX = originalPageX;
|
||||
this.pageY = originalPageY;
|
||||
|
||||
const win = window.open();
|
||||
if (win) {
|
||||
win.document.write(`<img src="${temp.toDataURL()}" style="max-width:100%">`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.invoiceGenerator = new InvoiceGenerator();
|
||||
})();
|
||||
@@ -44,9 +44,19 @@ public class DataInitializer implements CommandLineRunner {
|
||||
adminUser.setCreatedAt(LocalDateTime.now());
|
||||
adminUser.setUpdatedAt(LocalDateTime.now());
|
||||
adminUser.setRoles(Set.of("USER", "ADMIN"));
|
||||
adminUser.setTwoFactorEnabled(false); // 2FA deaktiviert für Admin
|
||||
|
||||
userRepository.save(adminUser);
|
||||
log.info("Created admin user: admin@votianlt.de / admin123");
|
||||
log.info("Created admin user: admin@votianlt.de / admin123 (2FA enabled)");
|
||||
} else {
|
||||
// Stelle sicher, dass bestehender Admin 2FA deaktiviert hat
|
||||
userRepository.findByEmail("admin@votianlt.de").ifPresent(adminUser -> {
|
||||
if (adminUser.isTwoFactorEnabled()) {
|
||||
adminUser.setTwoFactorEnabled(false);
|
||||
userRepository.save(adminUser);
|
||||
log.info("Updated admin user: 2FA disabled");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
log.info("Test users initialization completed.");
|
||||
|
||||
@@ -78,6 +78,8 @@ public final class AdminLayout extends AppLayout {
|
||||
// Only admin-specific menu items
|
||||
SideNavItem dashboard = new SideNavItem("Dashboard", "admin-dashboard", new Icon(VaadinIcon.DASHBOARD));
|
||||
SideNavItem pdfTest = new SideNavItem("PDF Test", "pdf-test", new Icon(VaadinIcon.FILE_TEXT_O));
|
||||
SideNavItem invoiceGenerator = new SideNavItem("Rechnungsgenerator", "invoice-generator",
|
||||
new Icon(VaadinIcon.FILE_PROCESS));
|
||||
SideNavItem priceTable = new SideNavItem("Preis-Tabelle", "admin-price-table", new Icon(VaadinIcon.COG));
|
||||
// SideNavItem systemSettings = new SideNavItem("Systemeinstellungen",
|
||||
// "admin-settings", new Icon(VaadinIcon.COG));
|
||||
@@ -88,6 +90,7 @@ public final class AdminLayout extends AppLayout {
|
||||
|
||||
nav.addItem(dashboard);
|
||||
nav.addItem(pdfTest);
|
||||
nav.addItem(invoiceGenerator);
|
||||
nav.addItem(priceTable);
|
||||
// nav.addItem(systemSettings);
|
||||
// nav.addItem(userManagement);
|
||||
|
||||
@@ -0,0 +1,454 @@
|
||||
package de.assecutor.votianlt.pages.view;
|
||||
|
||||
import com.vaadin.flow.component.AttachEvent;
|
||||
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.ButtonVariant;
|
||||
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.H2;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.component.upload.Upload;
|
||||
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
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.service.CustomerInvoiceService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
||||
@Route(value = "invoice-generator", layout = AdminLayout.class)
|
||||
@PageTitle("Rechnungsgenerator")
|
||||
@RolesAllowed("ADMIN")
|
||||
@JsModule("./invoice-generator/invoice-generator.js")
|
||||
public class InvoiceGeneratorView extends VerticalLayout {
|
||||
|
||||
private final CustomerInvoiceService customerInvoiceService;
|
||||
private Div canvasContainer;
|
||||
private VerticalLayout propertiesPanel;
|
||||
private Div selectedElementInfo;
|
||||
|
||||
public InvoiceGeneratorView(CustomerInvoiceService customerInvoiceService) {
|
||||
this.customerInvoiceService = customerInvoiceService;
|
||||
|
||||
setId("invoice-generator-view");
|
||||
getElement().setAttribute("class", "invoice-generator-view");
|
||||
|
||||
setSpacing(false);
|
||||
setPadding(false);
|
||||
getStyle().set("margin", "14px");
|
||||
setWidth("100%");
|
||||
setHeight("100%");
|
||||
|
||||
H2 title = new H2("Rechnungsgenerator");
|
||||
add(title);
|
||||
|
||||
// Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften)
|
||||
HorizontalLayout mainLayout = new HorizontalLayout();
|
||||
mainLayout.setWidth("100%");
|
||||
mainLayout.setHeight("calc(100vh - 150px)");
|
||||
mainLayout.setSpacing(true);
|
||||
|
||||
// Linke Seite: Templates/Bausteine
|
||||
VerticalLayout leftPanel = createTemplatesPanel();
|
||||
leftPanel.setWidth("250px");
|
||||
leftPanel.setHeightFull();
|
||||
|
||||
// Mitte: Canvas mit Konva.js
|
||||
VerticalLayout centerPanel = createCanvasPanel();
|
||||
centerPanel.setWidth("60%");
|
||||
centerPanel.setHeightFull();
|
||||
|
||||
// Rechte Seite: Eigenschaften
|
||||
propertiesPanel = createPropertiesPanel();
|
||||
propertiesPanel.setWidth("300px");
|
||||
propertiesPanel.setHeightFull();
|
||||
|
||||
mainLayout.add(leftPanel, centerPanel, propertiesPanel);
|
||||
mainLayout.expand(centerPanel);
|
||||
|
||||
add(mainLayout);
|
||||
|
||||
// Aktions-Buttons unter dem Canvas
|
||||
HorizontalLayout actionLayout = createActionButtons();
|
||||
add(actionLayout);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttach(AttachEvent attachEvent) {
|
||||
super.onAttach(attachEvent);
|
||||
// Register this view instance and initialize the canvas
|
||||
getElement().executeJs(
|
||||
"window.invoiceGeneratorView = this;" +
|
||||
"if (window.invoiceGenerator) {" +
|
||||
" console.log('Initializing invoice generator...');" +
|
||||
" window.invoiceGenerator.init();" +
|
||||
"} else {" +
|
||||
" console.error('Invoice generator not found');" +
|
||||
"}");
|
||||
}
|
||||
|
||||
private VerticalLayout createTemplatesPanel() {
|
||||
VerticalLayout panel = new VerticalLayout();
|
||||
panel.setPadding(true);
|
||||
panel.setSpacing(true);
|
||||
panel.getStyle()
|
||||
.set("background-color", "var(--lumo-contrast-5pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)");
|
||||
|
||||
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-m)")
|
||||
.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 hinzufügen
|
||||
Icon templateIcon = icon.create();
|
||||
templateIcon.setSize("var(--lumo-icon-size-s)");
|
||||
template.getElement().insertChild(0, templateIcon.getElement());
|
||||
|
||||
// Drag & Drop Attribute setzen
|
||||
template.getElement().setAttribute("draggable", "true");
|
||||
template.getElement().setAttribute("data-template-type", type);
|
||||
template.getElement().setAttribute("data-template-label", label);
|
||||
|
||||
// JavaScript Event Listener für Drag Start
|
||||
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 createCanvasPanel() {
|
||||
VerticalLayout panel = new VerticalLayout();
|
||||
panel.setPadding(false);
|
||||
panel.setSpacing(false);
|
||||
panel.setHeightFull();
|
||||
|
||||
// Canvas Container für Konva.js
|
||||
canvasContainer = new Div();
|
||||
canvasContainer.setId("invoice-canvas-container");
|
||||
canvasContainer.setWidth("100%");
|
||||
canvasContainer.setHeight("100%");
|
||||
canvasContainer.getStyle()
|
||||
.set("background-color", "#e8e8e8")
|
||||
.set("border", "2px dashed var(--lumo-contrast-30pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||
.set("position", "relative")
|
||||
.set("overflow", "hidden")
|
||||
.set("cursor", "default");
|
||||
|
||||
// Drop Zone Event Listener
|
||||
canvasContainer.getElement().executeJs(
|
||||
"var container = this;" +
|
||||
"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-30pct)';" +
|
||||
"});" +
|
||||
"container.addEventListener('drop', function(e) {" +
|
||||
" e.preventDefault();" +
|
||||
" container.style.borderColor = 'var(--lumo-contrast-30pct)';" +
|
||||
" var templateType = e.dataTransfer.getData('template-type');" +
|
||||
" var templateLabel = e.dataTransfer.getData('template-label');" +
|
||||
" if (templateType && window.invoiceGenerator) {" +
|
||||
" var rect = container.getBoundingClientRect();" +
|
||||
" var x = e.clientX - rect.left;" +
|
||||
" var y = e.clientY - rect.top;" +
|
||||
" window.invoiceGenerator.addElement(templateType, templateLabel, x, y);" +
|
||||
" }" +
|
||||
"});");
|
||||
|
||||
panel.add(canvasContainer);
|
||||
panel.expand(canvasContainer);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private VerticalLayout createPropertiesPanel() {
|
||||
VerticalLayout panel = new VerticalLayout();
|
||||
panel.setPadding(true);
|
||||
panel.setSpacing(true);
|
||||
panel.getStyle()
|
||||
.set("background-color", "var(--lumo-contrast-5pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)");
|
||||
|
||||
Span header = new Span("Eigenschaften");
|
||||
header.getStyle()
|
||||
.set("font-weight", "bold")
|
||||
.set("font-size", "var(--lumo-font-size-l)");
|
||||
|
||||
// Info-Text wenn kein Element ausgewählt
|
||||
selectedElementInfo = new Div();
|
||||
selectedElementInfo.setText("Klicken Sie auf ein Element im Canvas, um dessen Eigenschaften zu bearbeiten.");
|
||||
selectedElementInfo.getStyle()
|
||||
.set("color", "var(--lumo-secondary-text-color)")
|
||||
.set("font-size", "var(--lumo-font-size-s)");
|
||||
|
||||
panel.add(header, selectedElementInfo);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private HorizontalLayout createActionButtons() {
|
||||
HorizontalLayout layout = new HorizontalLayout();
|
||||
layout.setWidth("100%");
|
||||
layout.setJustifyContentMode(JustifyContentMode.END);
|
||||
layout.setPadding(true);
|
||||
|
||||
Button clearButton = new Button("Canvas leeren", new Icon(VaadinIcon.TRASH));
|
||||
clearButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
|
||||
clearButton.addClickListener(e -> {
|
||||
getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.clearCanvas(); }");
|
||||
showNotification("Canvas wurde geleert");
|
||||
});
|
||||
|
||||
Button previewButton = new Button("Vorschau", new Icon(VaadinIcon.EYE));
|
||||
previewButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST);
|
||||
previewButton.addClickListener(e -> {
|
||||
getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.generatePreview(); }");
|
||||
});
|
||||
|
||||
Button saveTemplateButton = new Button("Template speichern", new Icon(VaadinIcon.DOWNLOAD));
|
||||
saveTemplateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
saveTemplateButton.addClickListener(e -> {
|
||||
getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.exportTemplate(); }");
|
||||
});
|
||||
|
||||
Button generatePdfButton = new Button("PDF generieren", new Icon(VaadinIcon.FILE_TEXT_O));
|
||||
generatePdfButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
|
||||
generatePdfButton.addClickListener(e -> generatePdf());
|
||||
|
||||
layout.add(clearButton, previewButton, saveTemplateButton, generatePdfButton);
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
private void generatePdf() {
|
||||
try {
|
||||
// Hole die Canvas-Daten und generiere PDF
|
||||
getElement().executeJs(
|
||||
"if (window.invoiceGenerator) { return window.invoiceGenerator.getCanvasData(); } else { return null; }")
|
||||
.then(json -> {
|
||||
if (json == null) {
|
||||
showNotification("Fehler: Canvas-Daten konnten nicht gelesen werden");
|
||||
return;
|
||||
}
|
||||
// Hier würde die PDF-Generierung erfolgen
|
||||
showNotification("PDF wird generiert... (Demo)");
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
showNotification("Fehler beim Generieren des PDFs: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void showNotification(String message) {
|
||||
Notification.show(message, 3000, Notification.Position.BOTTOM_CENTER);
|
||||
}
|
||||
|
||||
// Wird von JavaScript aufgerufen wenn ein Element ausgewählt wird
|
||||
@ClientCallable
|
||||
public void updatePropertiesPanel(String elementId, String elementType, String text, Double x, Double y,
|
||||
Integer fontSize, String color) {
|
||||
getUI().ifPresent(ui -> ui.access(() -> {
|
||||
propertiesPanel.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)");
|
||||
|
||||
propertiesPanel.add(header, typeLabel);
|
||||
|
||||
// Für Bildelemente: Upload-Button anzeigen
|
||||
if ("image".equals(elementType)) {
|
||||
MemoryBuffer buffer = new MemoryBuffer();
|
||||
Upload upload = new Upload(buffer);
|
||||
upload.setAcceptedFileTypes("image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp");
|
||||
upload.setMaxFileSize(5 * 1024 * 1024); // 5 MB
|
||||
upload.setDropLabel(new Span("Bild hierher ziehen oder klicken"));
|
||||
upload.setWidthFull();
|
||||
|
||||
upload.addSucceededListener(event -> {
|
||||
try {
|
||||
// Bild als Base64 kodieren
|
||||
byte[] bytes = buffer.getInputStream().readAllBytes();
|
||||
String base64 = java.util.Base64.getEncoder().encodeToString(bytes);
|
||||
String mimeType = event.getMIMEType();
|
||||
String dataUrl = "data:" + mimeType + ";base64," + base64;
|
||||
|
||||
// An JavaScript übergeben
|
||||
getElement().executeJs(
|
||||
"if (window.invoiceGenerator) { window.invoiceGenerator.updateElementImage('"
|
||||
+ elementId + "', $0); }",
|
||||
dataUrl);
|
||||
showNotification("Bild erfolgreich hochgeladen");
|
||||
} catch (Exception ex) {
|
||||
showNotification("Fehler beim Hochladen: " + ex.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
upload.addFileRejectedListener(event -> {
|
||||
showNotification("Datei abgelehnt: " + event.getErrorMessage());
|
||||
});
|
||||
|
||||
propertiesPanel.add(upload);
|
||||
}
|
||||
|
||||
// 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.invoiceGenerator) { window.invoiceGenerator.updateElementText('" + elementId
|
||||
+ "', $0); }",
|
||||
e.getValue());
|
||||
});
|
||||
propertiesPanel.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.invoiceGenerator) { window.invoiceGenerator.updateElementPosition('" + elementId
|
||||
+ "', $0, null); }",
|
||||
newX);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
});
|
||||
propertiesPanel.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.invoiceGenerator) { window.invoiceGenerator.updateElementPosition('" + elementId
|
||||
+ "', null, $0); }",
|
||||
newY);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
});
|
||||
propertiesPanel.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.invoiceGenerator) { window.invoiceGenerator.updateElementFontSize('"
|
||||
+ elementId + "', $0); }",
|
||||
newSize);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
});
|
||||
propertiesPanel.add(fontSizeField);
|
||||
}
|
||||
|
||||
// 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.invoiceGenerator) { window.invoiceGenerator.deleteElement('" + elementId
|
||||
+ "'); }");
|
||||
resetPropertiesPanel();
|
||||
});
|
||||
propertiesPanel.add(deleteButton);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wird von JavaScript aufgerufen wenn die Auswahl aufgehoben wird
|
||||
*/
|
||||
@ClientCallable
|
||||
public void resetPropertiesPanel() {
|
||||
getUI().ifPresent(ui -> ui.access(() -> {
|
||||
propertiesPanel.removeAll();
|
||||
|
||||
Span header = new Span("Eigenschaften");
|
||||
header.getStyle()
|
||||
.set("font-weight", "bold")
|
||||
.set("font-size", "var(--lumo-font-size-l)");
|
||||
|
||||
selectedElementInfo = new Div();
|
||||
selectedElementInfo.setText("Klicken Sie auf ein Element im Canvas, um dessen Eigenschaften zu bearbeiten.");
|
||||
selectedElementInfo.getStyle()
|
||||
.set("color", "var(--lumo-secondary-text-color)")
|
||||
.set("font-size", "var(--lumo-font-size-s)");
|
||||
|
||||
propertiesPanel.add(header, selectedElementInfo);
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user