Erweiterungen
This commit is contained in:
Binary file not shown.
@@ -4,7 +4,7 @@
|
||||
'use strict';
|
||||
|
||||
class InvoiceGenerator {
|
||||
constructor() {
|
||||
constructor(containerId) {
|
||||
this.canvas = null;
|
||||
this.ctx = null;
|
||||
this.elements = new Map();
|
||||
@@ -16,22 +16,33 @@
|
||||
this.elementStart = { x: 0, y: 0, w: 0, h: 0 };
|
||||
this.activeHandle = null;
|
||||
this.container = null;
|
||||
this.containerId = containerId || 'invoice-canvas-container';
|
||||
|
||||
// Page dimensions (A4 at 96 DPI)
|
||||
this.pageX = 50;
|
||||
this.pageY = 30;
|
||||
this.pageWidth = 794;
|
||||
this.pageHeight = 1123;
|
||||
this.scale = 1;
|
||||
|
||||
// Handle configuration
|
||||
this.handleSize = 10;
|
||||
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() {
|
||||
this.container = document.getElementById('invoice-canvas-container');
|
||||
this.container = document.getElementById(this.containerId);
|
||||
if (!this.container) {
|
||||
console.error('Canvas container not found');
|
||||
console.error('Canvas container not found: ' + this.containerId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,6 +61,10 @@
|
||||
this.canvas.addEventListener('mouseup', 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', () => {
|
||||
this.updateCanvasSize();
|
||||
this.draw();
|
||||
@@ -63,14 +78,32 @@
|
||||
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);
|
||||
|
||||
// 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) {
|
||||
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 {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top
|
||||
x: (canvasX - this.pageX) / this.scale,
|
||||
y: (canvasY - this.pageY) / this.scale
|
||||
};
|
||||
}
|
||||
|
||||
@@ -166,14 +199,18 @@
|
||||
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;
|
||||
let newX = this.elementStart.x + dx;
|
||||
let newY = 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)));
|
||||
// Constrain to page (in page coordinates, page starts at 0,0)
|
||||
newX = Math.max(0, Math.min(newX,
|
||||
this.pageWidth - (this.selectedElement.width || 100)));
|
||||
newY = Math.max(0, Math.min(newY,
|
||||
this.pageHeight - (this.selectedElement.height || 30)));
|
||||
|
||||
// Snap to grid
|
||||
this.selectedElement.x = this.snapToGrid(newX);
|
||||
this.selectedElement.y = this.snapToGrid(newY);
|
||||
|
||||
this.draw();
|
||||
this.notifyChange();
|
||||
@@ -211,6 +248,47 @@
|
||||
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) {
|
||||
if (!this.selectedElement || !this.activeHandle) return;
|
||||
|
||||
@@ -308,17 +386,31 @@
|
||||
ctx.fillStyle = '#e8e8e8';
|
||||
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.fillRect(this.pageX + 4, this.pageY + this.pageHeight, this.pageWidth, 4);
|
||||
ctx.fillRect(this.pageX + this.pageWidth, this.pageY + 4, 4, this.pageHeight);
|
||||
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.scale, this.pageY + 4 * this.scale, 4, this.pageHeight * this.scale);
|
||||
ctx.restore();
|
||||
|
||||
// Page
|
||||
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.lineWidth = 1;
|
||||
ctx.strokeRect(this.pageX, this.pageY, this.pageWidth, this.pageHeight);
|
||||
ctx.lineWidth = 1 / this.scale;
|
||||
ctx.strokeRect(0, 0, this.pageWidth, this.pageHeight);
|
||||
|
||||
// Draw grid if enabled
|
||||
if (this.showGrid) {
|
||||
this.drawGrid(ctx);
|
||||
}
|
||||
|
||||
// Draw elements
|
||||
this.elements.forEach(el => this.drawElement(el));
|
||||
@@ -327,6 +419,8 @@
|
||||
if (this.selectedElement) {
|
||||
this.drawSelection(this.selectedElement);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
drawImagePlaceholder(ctx, el) {
|
||||
@@ -491,7 +585,7 @@
|
||||
const y = el.y;
|
||||
const w = el.width || 150;
|
||||
const h = el.height || 30;
|
||||
const hs = this.handleSize;
|
||||
const hs = this.handleSize / this.scale;
|
||||
|
||||
// Selection border
|
||||
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) {
|
||||
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);
|
||||
// dropX and dropY are in container coordinates, convert to page coordinates
|
||||
const pageCoordX = (dropX - this.pageX) / this.scale;
|
||||
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 = {
|
||||
id, type, x, y,
|
||||
@@ -595,6 +719,10 @@
|
||||
this.draw();
|
||||
if (this.selectedElement) {
|
||||
this.notifyChange();
|
||||
// Focus canvas for keyboard navigation
|
||||
if (this.canvas) {
|
||||
this.canvas.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,8 +745,8 @@
|
||||
updateElementPosition(id, x, y) {
|
||||
const el = this.elements.get(id);
|
||||
if (el) {
|
||||
if (x !== null) el.x = x;
|
||||
if (y !== null) el.y = y;
|
||||
if (x !== null) el.x = this.snapToGrid(x);
|
||||
if (y !== null) el.y = this.snapToGrid(y);
|
||||
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) {
|
||||
const el = this.elements.get(id);
|
||||
if (el && el.type === 'image') {
|
||||
@@ -683,6 +819,12 @@
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
exportTemplateJson() {
|
||||
// Return template data as JSON string for Java processing
|
||||
const data = this.getCanvasData();
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
|
||||
generatePreview() {
|
||||
const temp = document.createElement('canvas');
|
||||
temp.width = this.pageWidth;
|
||||
@@ -718,4 +860,5 @@
|
||||
}
|
||||
|
||||
window.invoiceGenerator = new InvoiceGenerator();
|
||||
window.InvoiceGenerator = InvoiceGenerator;
|
||||
})();
|
||||
|
||||
534
src/main/frontend/invoice-generator/profile-invoice-generator.js
Normal file
534
src/main/frontend/invoice-generator/profile-invoice-generator.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -6,8 +6,9 @@ import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
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.Input;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
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.data.binder.Binder;
|
||||
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.Route;
|
||||
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.security.SecurityService;
|
||||
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
||||
import com.vaadin.flow.component.dependency.JsModule;
|
||||
import com.vaadin.flow.component.ClientCallable;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
|
||||
@PageTitle("Profil bearbeiten")
|
||||
@Route(value = "edit-profile", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||
@RolesAllowed({ "USER", "ADMIN" })
|
||||
@JsModule("./invoice-generator/profile-invoice-generator.js")
|
||||
public class EditProfileView extends HorizontalLayout {
|
||||
private final TextField prefixField;
|
||||
private final TextField ustIdField;
|
||||
@@ -59,6 +62,7 @@ public class EditProfileView extends HorizontalLayout {
|
||||
private final CustomerInvoiceService customerInvoiceService;
|
||||
private UserInvoiceData currentInvoiceData;
|
||||
private Checkbox billingEnabled;
|
||||
private VerticalLayout propertiesPanelProfile;
|
||||
|
||||
public EditProfileView(UserService userService, UserInvoiceDataService userInvoiceDataService,
|
||||
CustomerInvoiceService customerInvoiceService, SecurityService securityService) {
|
||||
@@ -78,7 +82,7 @@ public class EditProfileView extends HorizontalLayout {
|
||||
|
||||
// Linke Spalte: Formular
|
||||
VerticalLayout formColumn = new VerticalLayout();
|
||||
formColumn.setWidth("68%");
|
||||
formColumn.setWidthFull();
|
||||
formColumn.setHeightFull();
|
||||
formColumn.setPadding(false);
|
||||
formColumn.setSpacing(false);
|
||||
@@ -267,94 +271,149 @@ public class EditProfileView extends HorizontalLayout {
|
||||
mapTab.setSpacing(true);
|
||||
mapTab.add(coordsLabel, mapDiv);
|
||||
tabSheet.add("Karte", mapTab);
|
||||
// Dritter Tab: Rechnungsstellung
|
||||
HorizontalLayout billingTab = new HorizontalLayout();
|
||||
// Dritter Tab: Rechnungserstellung
|
||||
VerticalLayout billingTab = new VerticalLayout();
|
||||
billingTab.setWidthFull();
|
||||
billingTab.setSpacing(true);
|
||||
billingTab.setPadding(false);
|
||||
billingTab.setHeightFull();
|
||||
|
||||
// Linke Spalte: Rechnungsbestandteile
|
||||
VerticalLayout billingLeft = new VerticalLayout();
|
||||
billingLeft.setWidth("45%");
|
||||
billingLeft.setPadding(false);
|
||||
billingLeft.setSpacing(true);
|
||||
H3 partsTitle = new H3("Rechnungsbestandteile");
|
||||
partsTitle.getStyle().set("margin", "0 0 var(--lumo-space-s) 0");
|
||||
// Rechnungsfelder initialisieren (für Speicherung, aber nicht im UI)
|
||||
prefixField = new TextField();
|
||||
ustIdField = new TextField();
|
||||
taxNumberField = new TextField();
|
||||
bankNameField = new TextField();
|
||||
ibanField = new TextField();
|
||||
taxRateField = new TextField();
|
||||
introTextArea = new TextArea();
|
||||
termsTextArea = new TextArea();
|
||||
pdfFrame = new IFrame();
|
||||
|
||||
// Felder für Rechnungsstellung (für Live-Update)
|
||||
// Nur die Checkbox "Rechnungslegung über votianLT"
|
||||
billingEnabled = new Checkbox("Rechnungslegung über votianLT");
|
||||
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");
|
||||
ustIdField = new TextField("USt-IdNr.");
|
||||
taxNumberField = new TextField("Steuernummer");
|
||||
bankNameField = new TextField("Bankname");
|
||||
ibanField = new TextField("IBAN");
|
||||
taxRateField = new TextField("Steuersatz");
|
||||
introTextArea = new TextArea("Einleitungstext");
|
||||
termsTextArea = new TextArea("Zahlungsbedingungen");
|
||||
introTextArea.setWidthFull();
|
||||
termsTextArea.setWidthFull();
|
||||
// Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften)
|
||||
HorizontalLayout mainLayout = new HorizontalLayout();
|
||||
mainLayout.setWidthFull();
|
||||
mainLayout.setHeight("500px");
|
||||
mainLayout.setSpacing(true);
|
||||
mainLayout.getStyle().set("overflow", "hidden");
|
||||
|
||||
// Anfangszustand: Felder deaktiviert bis Checkbox aktiv
|
||||
setBillingFieldsEnabled(false);
|
||||
// Linke Seite: Templates/Bausteine
|
||||
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
|
||||
prefixField.setValueChangeMode(ValueChangeMode.EAGER);
|
||||
ustIdField.setValueChangeMode(ValueChangeMode.EAGER);
|
||||
taxNumberField.setValueChangeMode(ValueChangeMode.EAGER);
|
||||
bankNameField.setValueChangeMode(ValueChangeMode.EAGER);
|
||||
ibanField.setValueChangeMode(ValueChangeMode.EAGER);
|
||||
taxRateField.setValueChangeMode(ValueChangeMode.EAGER);
|
||||
introTextArea.setValueChangeMode(ValueChangeMode.EAGER);
|
||||
termsTextArea.setValueChangeMode(ValueChangeMode.EAGER);
|
||||
prefixField.addValueChangeListener(e -> refreshPdf());
|
||||
ustIdField.addValueChangeListener(e -> refreshPdf());
|
||||
taxNumberField.addValueChangeListener(e -> refreshPdf());
|
||||
bankNameField.addValueChangeListener(e -> refreshPdf());
|
||||
ibanField.addValueChangeListener(e -> refreshPdf());
|
||||
taxRateField.addValueChangeListener(e -> refreshPdf());
|
||||
introTextArea.addValueChangeListener(e -> refreshPdf());
|
||||
termsTextArea.addValueChangeListener(e -> refreshPdf());
|
||||
// Mitte: Canvas
|
||||
Div canvasContainer = new Div();
|
||||
canvasContainer.setId("invoice-canvas-container-profile");
|
||||
canvasContainer.setWidth("100%");
|
||||
canvasContainer.setHeight("100%");
|
||||
canvasContainer.getStyle()
|
||||
.set("background-color", "#e8e8e8")
|
||||
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||
.set("overflow", "hidden")
|
||||
.set("position", "relative");
|
||||
|
||||
billingLeft.add(partsTitle, billingEnabled, prefixField, ustIdField, taxNumberField, bankNameField, ibanField,
|
||||
taxRateField, introTextArea, termsTextArea);
|
||||
VerticalLayout centerPanel = new VerticalLayout();
|
||||
centerPanel.setWidth("60%");
|
||||
centerPanel.setHeightFull();
|
||||
centerPanel.setPadding(false);
|
||||
centerPanel.setSpacing(false);
|
||||
centerPanel.getStyle()
|
||||
.set("flex-grow", "1")
|
||||
.set("min-width", "0");
|
||||
centerPanel.add(canvasContainer);
|
||||
centerPanel.expand(canvasContainer);
|
||||
|
||||
// Rechte Spalte: Vorschau
|
||||
VerticalLayout billingRight = new VerticalLayout();
|
||||
billingRight.setWidth("55%");
|
||||
billingRight.setPadding(false);
|
||||
billingRight.setSpacing(false);
|
||||
billingRight.setHeightFull();
|
||||
billingTab.setFlexGrow(1, billingRight);
|
||||
H3 previewTitle = new H3("Rechnungsvorschau");
|
||||
previewTitle.getStyle().set("margin", "0 0 var(--lumo-space-s) 0");
|
||||
// Rechte Seite: Eigenschaften
|
||||
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)");
|
||||
|
||||
// Echte PDF-Vorschau mittels StreamResource und iframe
|
||||
Div previewWrapper = new Div();
|
||||
previewWrapper.setWidth("100%");
|
||||
previewWrapper.setHeight("650px");
|
||||
previewWrapper.getStyle().set("overflow", "hidden").set("background", "var(--lumo-contrast-10pct)")
|
||||
.set("padding", "0");
|
||||
mainLayout.add(leftPanel, centerPanel, propertiesPanelProfile);
|
||||
mainLayout.expand(centerPanel);
|
||||
billingTab.add(mainLayout);
|
||||
billingTab.expand(mainLayout);
|
||||
|
||||
// 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);
|
||||
// Action Buttons für den Rechnungsgenerator
|
||||
HorizontalLayout actionLayout = new HorizontalLayout();
|
||||
actionLayout.setWidthFull();
|
||||
actionLayout.setJustifyContentMode(JustifyContentMode.END);
|
||||
actionLayout.setSpacing(true);
|
||||
actionLayout.getStyle().set("margin-top", "var(--lumo-space-s)");
|
||||
|
||||
billingRight.add(previewTitle, previewWrapper);
|
||||
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);
|
||||
});
|
||||
|
||||
billingTab.add(billingLeft, billingRight);
|
||||
tabSheet.add("Rechnungsstellung", billingTab);
|
||||
Button previewPdfButton = new Button("Vorschau", new Icon(VaadinIcon.EYE));
|
||||
previewPdfButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
|
||||
previewPdfButton.addClickListener(e -> generatePreviewPdfFromProfile());
|
||||
|
||||
// Bestehende Rechnungsdaten laden (nach Erstellung aller Felder)
|
||||
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);
|
||||
|
||||
// Bestehende Rechnungsdaten laden (nur für die Checkbox)
|
||||
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)
|
||||
VerticalLayout switches = new VerticalLayout();
|
||||
switches.setPadding(false);
|
||||
@@ -494,25 +553,6 @@ public class EditProfileView extends HorizontalLayout {
|
||||
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() {
|
||||
try {
|
||||
byte[] bytes = generatePreviewPdf();
|
||||
@@ -712,4 +752,336 @@ public class EditProfileView extends HorizontalLayout {
|
||||
&& !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);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,15 +2,10 @@ 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;
|
||||
@@ -18,17 +13,20 @@ 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.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.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")
|
||||
@@ -48,41 +46,59 @@ public class InvoiceGeneratorView extends VerticalLayout {
|
||||
|
||||
setSpacing(false);
|
||||
setPadding(false);
|
||||
getStyle().set("margin", "14px");
|
||||
setMargin(false);
|
||||
setWidth("100%");
|
||||
setHeight("100%");
|
||||
|
||||
H2 title = new H2("Rechnungsgenerator");
|
||||
add(title);
|
||||
getStyle()
|
||||
.set("overflow", "hidden")
|
||||
.set("box-sizing", "border-box")
|
||||
.set("display", "flex")
|
||||
.set("flex-direction", "column");
|
||||
|
||||
// Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften)
|
||||
HorizontalLayout mainLayout = new HorizontalLayout();
|
||||
mainLayout.setWidth("100%");
|
||||
mainLayout.setHeight("calc(100vh - 150px)");
|
||||
mainLayout.setHeight("calc(100vh - 60px)"); // Abzug für Action-Buttons
|
||||
mainLayout.setSpacing(true);
|
||||
mainLayout.getStyle().set("overflow", "hidden");
|
||||
|
||||
// Linke Seite: Templates/Bausteine
|
||||
VerticalLayout leftPanel = createTemplatesPanel();
|
||||
leftPanel.setWidth("250px");
|
||||
leftPanel.setHeightFull();
|
||||
leftPanel.getStyle()
|
||||
.set("flex-shrink", "0")
|
||||
.set("min-width", "250px")
|
||||
.set("overflow", "auto");
|
||||
|
||||
// Mitte: Canvas mit Konva.js
|
||||
VerticalLayout centerPanel = createCanvasPanel();
|
||||
centerPanel.setWidth("60%");
|
||||
centerPanel.setHeightFull();
|
||||
centerPanel.getStyle()
|
||||
.set("flex-grow", "1")
|
||||
.set("min-width", "0");
|
||||
|
||||
// Rechte Seite: Eigenschaften
|
||||
propertiesPanel = createPropertiesPanel();
|
||||
propertiesPanel.setWidth("300px");
|
||||
propertiesPanel.setHeightFull();
|
||||
propertiesPanel.getStyle()
|
||||
.set("flex-shrink", "0")
|
||||
.set("min-width", "300px")
|
||||
.set("overflow", "auto");
|
||||
|
||||
mainLayout.add(leftPanel, centerPanel, propertiesPanel);
|
||||
mainLayout.expand(centerPanel);
|
||||
|
||||
add(mainLayout);
|
||||
|
||||
// Aktions-Buttons unter dem Canvas
|
||||
// Aktions-Buttons unter dem Canvas (fixe Höhe)
|
||||
HorizontalLayout actionLayout = createActionButtons();
|
||||
actionLayout.setHeight("60px");
|
||||
actionLayout.getStyle()
|
||||
.set("flex-shrink", "0")
|
||||
.set("padding", "0 var(--lumo-space-m)");
|
||||
add(actionLayout);
|
||||
}
|
||||
|
||||
@@ -104,9 +120,11 @@ public class InvoiceGeneratorView extends VerticalLayout {
|
||||
VerticalLayout panel = new VerticalLayout();
|
||||
panel.setPadding(true);
|
||||
panel.setSpacing(true);
|
||||
panel.setHeightFull();
|
||||
panel.getStyle()
|
||||
.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");
|
||||
header.getStyle()
|
||||
@@ -221,9 +239,11 @@ public class InvoiceGeneratorView extends VerticalLayout {
|
||||
VerticalLayout panel = new VerticalLayout();
|
||||
panel.setPadding(true);
|
||||
panel.setSpacing(true);
|
||||
panel.setHeightFull();
|
||||
panel.getStyle()
|
||||
.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");
|
||||
header.getStyle()
|
||||
@@ -246,7 +266,9 @@ public class InvoiceGeneratorView extends VerticalLayout {
|
||||
HorizontalLayout layout = new HorizontalLayout();
|
||||
layout.setWidth("100%");
|
||||
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));
|
||||
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));
|
||||
previewButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST);
|
||||
previewButton.addClickListener(e -> {
|
||||
getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.generatePreview(); }");
|
||||
});
|
||||
previewButton.addClickListener(e -> generatePreviewPdf());
|
||||
|
||||
Button saveTemplateButton = new Button("Template speichern", new Icon(VaadinIcon.DOWNLOAD));
|
||||
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) {
|
||||
Notification.show(message, 3000, Notification.Position.BOTTOM_CENTER);
|
||||
}
|
||||
@@ -413,6 +519,114 @@ public class InvoiceGeneratorView extends VerticalLayout {
|
||||
}
|
||||
});
|
||||
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
|
||||
|
||||
@@ -242,4 +242,91 @@ public class CustomerInvoiceService {
|
||||
|
||||
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("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'");
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user