Erweiterungen
This commit is contained in:
Binary file not shown.
@@ -4,7 +4,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
class InvoiceGenerator {
|
class InvoiceGenerator {
|
||||||
constructor() {
|
constructor(containerId) {
|
||||||
this.canvas = null;
|
this.canvas = null;
|
||||||
this.ctx = null;
|
this.ctx = null;
|
||||||
this.elements = new Map();
|
this.elements = new Map();
|
||||||
@@ -16,22 +16,33 @@
|
|||||||
this.elementStart = { x: 0, y: 0, w: 0, h: 0 };
|
this.elementStart = { x: 0, y: 0, w: 0, h: 0 };
|
||||||
this.activeHandle = null;
|
this.activeHandle = null;
|
||||||
this.container = null;
|
this.container = null;
|
||||||
|
this.containerId = containerId || 'invoice-canvas-container';
|
||||||
|
|
||||||
// Page dimensions (A4 at 96 DPI)
|
// Page dimensions (A4 at 96 DPI)
|
||||||
this.pageX = 50;
|
this.pageX = 50;
|
||||||
this.pageY = 30;
|
this.pageY = 30;
|
||||||
this.pageWidth = 794;
|
this.pageWidth = 794;
|
||||||
this.pageHeight = 1123;
|
this.pageHeight = 1123;
|
||||||
|
this.scale = 1;
|
||||||
|
|
||||||
// Handle configuration
|
// Handle configuration
|
||||||
this.handleSize = 10;
|
this.handleSize = 10;
|
||||||
this.handlePadding = 3;
|
this.handlePadding = 3;
|
||||||
|
|
||||||
|
// Grid configuration (Snap to Grid)
|
||||||
|
this.gridSize = 5;
|
||||||
|
this.showGrid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap value to grid
|
||||||
|
snapToGrid(value) {
|
||||||
|
return Math.round(value / this.gridSize) * this.gridSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.container = document.getElementById('invoice-canvas-container');
|
this.container = document.getElementById(this.containerId);
|
||||||
if (!this.container) {
|
if (!this.container) {
|
||||||
console.error('Canvas container not found');
|
console.error('Canvas container not found: ' + this.containerId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +61,10 @@
|
|||||||
this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this));
|
this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this));
|
||||||
this.canvas.addEventListener('mouseleave', this.onMouseUp.bind(this));
|
this.canvas.addEventListener('mouseleave', this.onMouseUp.bind(this));
|
||||||
|
|
||||||
|
// Keyboard navigation for selected element
|
||||||
|
this.canvas.setAttribute('tabindex', '0');
|
||||||
|
this.canvas.addEventListener('keydown', this.onKeyDown.bind(this));
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
this.updateCanvasSize();
|
this.updateCanvasSize();
|
||||||
this.draw();
|
this.draw();
|
||||||
@@ -63,14 +78,32 @@
|
|||||||
const rect = this.container.getBoundingClientRect();
|
const rect = this.container.getBoundingClientRect();
|
||||||
this.canvas.width = rect.width;
|
this.canvas.width = rect.width;
|
||||||
this.canvas.height = rect.height;
|
this.canvas.height = rect.height;
|
||||||
this.pageX = Math.max(20, (this.canvas.width - this.pageWidth) / 2);
|
|
||||||
|
// Calculate scale to maximize page size while maintaining aspect ratio
|
||||||
|
// Use full available space with minimal padding
|
||||||
|
const padding = 10;
|
||||||
|
const availableWidth = this.canvas.width - padding * 2;
|
||||||
|
const availableHeight = this.canvas.height - padding * 2;
|
||||||
|
|
||||||
|
// Scale to fit the available space (can be larger than 1 if container is bigger than A4)
|
||||||
|
this.scale = Math.min(availableWidth / this.pageWidth, availableHeight / this.pageHeight);
|
||||||
|
|
||||||
|
// Center the scaled page
|
||||||
|
const scaledPageWidth = this.pageWidth * this.scale;
|
||||||
|
const scaledPageHeight = this.pageHeight * this.scale;
|
||||||
|
this.pageX = (this.canvas.width - scaledPageWidth) / 2;
|
||||||
|
this.pageY = (this.canvas.height - scaledPageHeight) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMousePos(e) {
|
getMousePos(e) {
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
|
// Convert screen coordinates to canvas coordinates
|
||||||
|
const canvasX = e.clientX - rect.left;
|
||||||
|
const canvasY = e.clientY - rect.top;
|
||||||
|
// Convert to page coordinates (accounting for scale and offset)
|
||||||
return {
|
return {
|
||||||
x: e.clientX - rect.left,
|
x: (canvasX - this.pageX) / this.scale,
|
||||||
y: e.clientY - rect.top
|
y: (canvasY - this.pageY) / this.scale
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,14 +199,18 @@
|
|||||||
const dx = pos.x - this.dragStart.x;
|
const dx = pos.x - this.dragStart.x;
|
||||||
const dy = pos.y - this.dragStart.y;
|
const dy = pos.y - this.dragStart.y;
|
||||||
|
|
||||||
this.selectedElement.x = this.elementStart.x + dx;
|
let newX = this.elementStart.x + dx;
|
||||||
this.selectedElement.y = this.elementStart.y + dy;
|
let newY = this.elementStart.y + dy;
|
||||||
|
|
||||||
// Constrain to page
|
// Constrain to page (in page coordinates, page starts at 0,0)
|
||||||
this.selectedElement.x = Math.max(this.pageX, Math.min(this.selectedElement.x,
|
newX = Math.max(0, Math.min(newX,
|
||||||
this.pageX + this.pageWidth - (this.selectedElement.width || 100)));
|
this.pageWidth - (this.selectedElement.width || 100)));
|
||||||
this.selectedElement.y = Math.max(this.pageY, Math.min(this.selectedElement.y,
|
newY = Math.max(0, Math.min(newY,
|
||||||
this.pageY + this.pageHeight - (this.selectedElement.height || 30)));
|
this.pageHeight - (this.selectedElement.height || 30)));
|
||||||
|
|
||||||
|
// Snap to grid
|
||||||
|
this.selectedElement.x = this.snapToGrid(newX);
|
||||||
|
this.selectedElement.y = this.snapToGrid(newY);
|
||||||
|
|
||||||
this.draw();
|
this.draw();
|
||||||
this.notifyChange();
|
this.notifyChange();
|
||||||
@@ -211,6 +248,47 @@
|
|||||||
this.canvas.style.cursor = 'default';
|
this.canvas.style.cursor = 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onKeyDown(e) {
|
||||||
|
if (!this.selectedElement) return;
|
||||||
|
|
||||||
|
const step = this.gridSize; // Move by grid size (5px)
|
||||||
|
let moved = false;
|
||||||
|
|
||||||
|
switch(e.key) {
|
||||||
|
case 'ArrowUp':
|
||||||
|
this.selectedElement.y = Math.max(0, this.selectedElement.y - step);
|
||||||
|
moved = true;
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
this.selectedElement.y = Math.min(
|
||||||
|
this.pageHeight - (this.selectedElement.height || 30),
|
||||||
|
this.selectedElement.y + step
|
||||||
|
);
|
||||||
|
moved = true;
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
this.selectedElement.x = Math.max(0, this.selectedElement.x - step);
|
||||||
|
moved = true;
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
this.selectedElement.x = Math.min(
|
||||||
|
this.pageWidth - (this.selectedElement.width || 100),
|
||||||
|
this.selectedElement.x + step
|
||||||
|
);
|
||||||
|
moved = true;
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moved) {
|
||||||
|
this.draw();
|
||||||
|
this.notifyChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
doResize(mouseX, mouseY) {
|
doResize(mouseX, mouseY) {
|
||||||
if (!this.selectedElement || !this.activeHandle) return;
|
if (!this.selectedElement || !this.activeHandle) return;
|
||||||
|
|
||||||
@@ -308,17 +386,31 @@
|
|||||||
ctx.fillStyle = '#e8e8e8';
|
ctx.fillStyle = '#e8e8e8';
|
||||||
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
// Page shadow
|
ctx.save();
|
||||||
|
|
||||||
|
// Apply scale and offset
|
||||||
|
ctx.translate(this.pageX, this.pageY);
|
||||||
|
ctx.scale(this.scale, this.scale);
|
||||||
|
|
||||||
|
// Page shadow (drawn before scaling offset)
|
||||||
|
ctx.save();
|
||||||
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
ctx.fillStyle = 'rgba(0,0,0,0.2)';
|
ctx.fillStyle = 'rgba(0,0,0,0.2)';
|
||||||
ctx.fillRect(this.pageX + 4, this.pageY + this.pageHeight, this.pageWidth, 4);
|
ctx.fillRect(this.pageX + 4 * this.scale, this.pageY + this.pageHeight * this.scale, this.pageWidth * this.scale, 4);
|
||||||
ctx.fillRect(this.pageX + this.pageWidth, this.pageY + 4, 4, this.pageHeight);
|
ctx.fillRect(this.pageX + this.pageWidth * this.scale, this.pageY + 4 * this.scale, 4, this.pageHeight * this.scale);
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
// Page
|
// Page
|
||||||
ctx.fillStyle = '#ffffff';
|
ctx.fillStyle = '#ffffff';
|
||||||
ctx.fillRect(this.pageX, this.pageY, this.pageWidth, this.pageHeight);
|
ctx.fillRect(0, 0, this.pageWidth, this.pageHeight);
|
||||||
ctx.strokeStyle = '#cccccc';
|
ctx.strokeStyle = '#cccccc';
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1 / this.scale;
|
||||||
ctx.strokeRect(this.pageX, this.pageY, this.pageWidth, this.pageHeight);
|
ctx.strokeRect(0, 0, this.pageWidth, this.pageHeight);
|
||||||
|
|
||||||
|
// Draw grid if enabled
|
||||||
|
if (this.showGrid) {
|
||||||
|
this.drawGrid(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw elements
|
// Draw elements
|
||||||
this.elements.forEach(el => this.drawElement(el));
|
this.elements.forEach(el => this.drawElement(el));
|
||||||
@@ -327,6 +419,8 @@
|
|||||||
if (this.selectedElement) {
|
if (this.selectedElement) {
|
||||||
this.drawSelection(this.selectedElement);
|
this.drawSelection(this.selectedElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
drawImagePlaceholder(ctx, el) {
|
drawImagePlaceholder(ctx, el) {
|
||||||
@@ -491,7 +585,7 @@
|
|||||||
const y = el.y;
|
const y = el.y;
|
||||||
const w = el.width || 150;
|
const w = el.width || 150;
|
||||||
const h = el.height || 30;
|
const h = el.height || 30;
|
||||||
const hs = this.handleSize;
|
const hs = this.handleSize / this.scale;
|
||||||
|
|
||||||
// Selection border
|
// Selection border
|
||||||
ctx.strokeStyle = '#1976d2';
|
ctx.strokeStyle = '#1976d2';
|
||||||
@@ -522,13 +616,43 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawGrid(ctx) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = 'rgba(200, 200, 200, 0.3)';
|
||||||
|
ctx.lineWidth = 0.5 / this.scale;
|
||||||
|
|
||||||
|
// Vertical lines (draw in page coordinates)
|
||||||
|
for (let x = 0; x <= this.pageWidth; x += this.gridSize) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, 0);
|
||||||
|
ctx.lineTo(x, this.pageHeight);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal lines
|
||||||
|
for (let y = 0; y <= this.pageHeight; y += this.gridSize) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y);
|
||||||
|
ctx.lineTo(this.pageWidth, y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
addElement(type, label, dropX, dropY) {
|
addElement(type, label, dropX, dropY) {
|
||||||
this.elementCounter++;
|
this.elementCounter++;
|
||||||
const id = `element-${this.elementCounter}`;
|
const id = `element-${this.elementCounter}`;
|
||||||
|
|
||||||
const rect = this.container.getBoundingClientRect();
|
// dropX and dropY are in container coordinates, convert to page coordinates
|
||||||
const x = Math.max(this.pageX + 10, dropX - rect.left - 50);
|
const pageCoordX = (dropX - this.pageX) / this.scale;
|
||||||
const y = Math.max(this.pageY + 10, dropY - rect.top - 15);
|
const pageCoordY = (dropY - this.pageY) / this.scale;
|
||||||
|
|
||||||
|
// Snap initial position to grid (relative to page origin)
|
||||||
|
const rawX = Math.max(10, pageCoordX - 50);
|
||||||
|
const rawY = Math.max(10, pageCoordY - 15);
|
||||||
|
const x = this.snapToGrid(rawX);
|
||||||
|
const y = this.snapToGrid(rawY);
|
||||||
|
|
||||||
let el = {
|
let el = {
|
||||||
id, type, x, y,
|
id, type, x, y,
|
||||||
@@ -595,6 +719,10 @@
|
|||||||
this.draw();
|
this.draw();
|
||||||
if (this.selectedElement) {
|
if (this.selectedElement) {
|
||||||
this.notifyChange();
|
this.notifyChange();
|
||||||
|
// Focus canvas for keyboard navigation
|
||||||
|
if (this.canvas) {
|
||||||
|
this.canvas.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,8 +745,8 @@
|
|||||||
updateElementPosition(id, x, y) {
|
updateElementPosition(id, x, y) {
|
||||||
const el = this.elements.get(id);
|
const el = this.elements.get(id);
|
||||||
if (el) {
|
if (el) {
|
||||||
if (x !== null) el.x = x;
|
if (x !== null) el.x = this.snapToGrid(x);
|
||||||
if (y !== null) el.y = y;
|
if (y !== null) el.y = this.snapToGrid(y);
|
||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -631,6 +759,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateElementColor(id, color) {
|
||||||
|
const el = this.elements.get(id);
|
||||||
|
if (el) {
|
||||||
|
el.color = color;
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateElementImage(id, imageDataUrl) {
|
updateElementImage(id, imageDataUrl) {
|
||||||
const el = this.elements.get(id);
|
const el = this.elements.get(id);
|
||||||
if (el && el.type === 'image') {
|
if (el && el.type === 'image') {
|
||||||
@@ -683,6 +819,12 @@
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportTemplateJson() {
|
||||||
|
// Return template data as JSON string for Java processing
|
||||||
|
const data = this.getCanvasData();
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
generatePreview() {
|
generatePreview() {
|
||||||
const temp = document.createElement('canvas');
|
const temp = document.createElement('canvas');
|
||||||
temp.width = this.pageWidth;
|
temp.width = this.pageWidth;
|
||||||
@@ -718,4 +860,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.invoiceGenerator = new InvoiceGenerator();
|
window.invoiceGenerator = new InvoiceGenerator();
|
||||||
|
window.InvoiceGenerator = InvoiceGenerator;
|
||||||
})();
|
})();
|
||||||
|
|||||||
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.formlayout.FormLayout;
|
||||||
import com.vaadin.flow.component.html.Div;
|
import com.vaadin.flow.component.html.Div;
|
||||||
import com.vaadin.flow.component.html.Span;
|
import com.vaadin.flow.component.html.Span;
|
||||||
import com.vaadin.flow.component.html.H3;
|
import com.vaadin.flow.component.dialog.Dialog;
|
||||||
import com.vaadin.flow.component.html.IFrame;
|
import com.vaadin.flow.component.html.IFrame;
|
||||||
|
import com.vaadin.flow.component.html.Input;
|
||||||
import com.vaadin.flow.component.icon.Icon;
|
import com.vaadin.flow.component.icon.Icon;
|
||||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||||
import com.vaadin.flow.component.notification.Notification;
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
@@ -26,7 +27,6 @@ import com.vaadin.flow.component.textfield.TextArea;
|
|||||||
import com.vaadin.flow.component.tabs.TabSheet;
|
import com.vaadin.flow.component.tabs.TabSheet;
|
||||||
import com.vaadin.flow.data.binder.Binder;
|
import com.vaadin.flow.data.binder.Binder;
|
||||||
import com.vaadin.flow.data.validator.EmailValidator;
|
import com.vaadin.flow.data.validator.EmailValidator;
|
||||||
import com.vaadin.flow.data.value.ValueChangeMode;
|
|
||||||
import com.vaadin.flow.router.PageTitle;
|
import com.vaadin.flow.router.PageTitle;
|
||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
import de.assecutor.votianlt.model.User;
|
import de.assecutor.votianlt.model.User;
|
||||||
@@ -37,11 +37,14 @@ import de.assecutor.votianlt.pages.service.UserService;
|
|||||||
import de.assecutor.votianlt.pages.service.UserInvoiceDataService;
|
import de.assecutor.votianlt.pages.service.UserInvoiceDataService;
|
||||||
import de.assecutor.votianlt.security.SecurityService;
|
import de.assecutor.votianlt.security.SecurityService;
|
||||||
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
||||||
|
import com.vaadin.flow.component.dependency.JsModule;
|
||||||
|
import com.vaadin.flow.component.ClientCallable;
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
|
|
||||||
@PageTitle("Profil bearbeiten")
|
@PageTitle("Profil bearbeiten")
|
||||||
@Route(value = "edit-profile", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
@Route(value = "edit-profile", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||||
@RolesAllowed({ "USER", "ADMIN" })
|
@RolesAllowed({ "USER", "ADMIN" })
|
||||||
|
@JsModule("./invoice-generator/profile-invoice-generator.js")
|
||||||
public class EditProfileView extends HorizontalLayout {
|
public class EditProfileView extends HorizontalLayout {
|
||||||
private final TextField prefixField;
|
private final TextField prefixField;
|
||||||
private final TextField ustIdField;
|
private final TextField ustIdField;
|
||||||
@@ -59,6 +62,7 @@ public class EditProfileView extends HorizontalLayout {
|
|||||||
private final CustomerInvoiceService customerInvoiceService;
|
private final CustomerInvoiceService customerInvoiceService;
|
||||||
private UserInvoiceData currentInvoiceData;
|
private UserInvoiceData currentInvoiceData;
|
||||||
private Checkbox billingEnabled;
|
private Checkbox billingEnabled;
|
||||||
|
private VerticalLayout propertiesPanelProfile;
|
||||||
|
|
||||||
public EditProfileView(UserService userService, UserInvoiceDataService userInvoiceDataService,
|
public EditProfileView(UserService userService, UserInvoiceDataService userInvoiceDataService,
|
||||||
CustomerInvoiceService customerInvoiceService, SecurityService securityService) {
|
CustomerInvoiceService customerInvoiceService, SecurityService securityService) {
|
||||||
@@ -78,7 +82,7 @@ public class EditProfileView extends HorizontalLayout {
|
|||||||
|
|
||||||
// Linke Spalte: Formular
|
// Linke Spalte: Formular
|
||||||
VerticalLayout formColumn = new VerticalLayout();
|
VerticalLayout formColumn = new VerticalLayout();
|
||||||
formColumn.setWidth("68%");
|
formColumn.setWidthFull();
|
||||||
formColumn.setHeightFull();
|
formColumn.setHeightFull();
|
||||||
formColumn.setPadding(false);
|
formColumn.setPadding(false);
|
||||||
formColumn.setSpacing(false);
|
formColumn.setSpacing(false);
|
||||||
@@ -267,94 +271,149 @@ public class EditProfileView extends HorizontalLayout {
|
|||||||
mapTab.setSpacing(true);
|
mapTab.setSpacing(true);
|
||||||
mapTab.add(coordsLabel, mapDiv);
|
mapTab.add(coordsLabel, mapDiv);
|
||||||
tabSheet.add("Karte", mapTab);
|
tabSheet.add("Karte", mapTab);
|
||||||
// Dritter Tab: Rechnungsstellung
|
// Dritter Tab: Rechnungserstellung
|
||||||
HorizontalLayout billingTab = new HorizontalLayout();
|
VerticalLayout billingTab = new VerticalLayout();
|
||||||
billingTab.setWidthFull();
|
billingTab.setWidthFull();
|
||||||
billingTab.setSpacing(true);
|
billingTab.setSpacing(true);
|
||||||
billingTab.setPadding(false);
|
billingTab.setPadding(false);
|
||||||
|
billingTab.setHeightFull();
|
||||||
|
|
||||||
// Linke Spalte: Rechnungsbestandteile
|
// Rechnungsfelder initialisieren (für Speicherung, aber nicht im UI)
|
||||||
VerticalLayout billingLeft = new VerticalLayout();
|
prefixField = new TextField();
|
||||||
billingLeft.setWidth("45%");
|
ustIdField = new TextField();
|
||||||
billingLeft.setPadding(false);
|
taxNumberField = new TextField();
|
||||||
billingLeft.setSpacing(true);
|
bankNameField = new TextField();
|
||||||
H3 partsTitle = new H3("Rechnungsbestandteile");
|
ibanField = new TextField();
|
||||||
partsTitle.getStyle().set("margin", "0 0 var(--lumo-space-s) 0");
|
taxRateField = new TextField();
|
||||||
|
introTextArea = new TextArea();
|
||||||
|
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 = new Checkbox("Rechnungslegung über votianLT");
|
||||||
billingEnabled.setValue(false);
|
billingEnabled.setValue(false);
|
||||||
billingEnabled.addValueChangeListener(e -> toggleBilling(e.getValue()));
|
billingEnabled.addValueChangeListener(e -> {
|
||||||
|
// Nur noch für das Speichern des Status, keine PDF-Vorschau mehr
|
||||||
|
});
|
||||||
|
billingTab.add(billingEnabled);
|
||||||
|
|
||||||
prefixField = new TextField("Rechnungs Präfix");
|
// Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften)
|
||||||
ustIdField = new TextField("USt-IdNr.");
|
HorizontalLayout mainLayout = new HorizontalLayout();
|
||||||
taxNumberField = new TextField("Steuernummer");
|
mainLayout.setWidthFull();
|
||||||
bankNameField = new TextField("Bankname");
|
mainLayout.setHeight("500px");
|
||||||
ibanField = new TextField("IBAN");
|
mainLayout.setSpacing(true);
|
||||||
taxRateField = new TextField("Steuersatz");
|
mainLayout.getStyle().set("overflow", "hidden");
|
||||||
introTextArea = new TextArea("Einleitungstext");
|
|
||||||
termsTextArea = new TextArea("Zahlungsbedingungen");
|
|
||||||
introTextArea.setWidthFull();
|
|
||||||
termsTextArea.setWidthFull();
|
|
||||||
|
|
||||||
// Anfangszustand: Felder deaktiviert bis Checkbox aktiv
|
// Linke Seite: Templates/Bausteine
|
||||||
setBillingFieldsEnabled(false);
|
VerticalLayout leftPanel = createTemplatesPanelForProfile();
|
||||||
|
leftPanel.setWidth("220px");
|
||||||
|
leftPanel.setHeightFull();
|
||||||
|
leftPanel.getStyle()
|
||||||
|
.set("flex-shrink", "0")
|
||||||
|
.set("min-width", "220px")
|
||||||
|
.set("overflow", "auto")
|
||||||
|
.set("background-color", "var(--lumo-contrast-5pct)")
|
||||||
|
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||||
|
.set("padding", "var(--lumo-space-m)");
|
||||||
|
|
||||||
// Live-Update: EAGER und Listener
|
// Mitte: Canvas
|
||||||
prefixField.setValueChangeMode(ValueChangeMode.EAGER);
|
Div canvasContainer = new Div();
|
||||||
ustIdField.setValueChangeMode(ValueChangeMode.EAGER);
|
canvasContainer.setId("invoice-canvas-container-profile");
|
||||||
taxNumberField.setValueChangeMode(ValueChangeMode.EAGER);
|
canvasContainer.setWidth("100%");
|
||||||
bankNameField.setValueChangeMode(ValueChangeMode.EAGER);
|
canvasContainer.setHeight("100%");
|
||||||
ibanField.setValueChangeMode(ValueChangeMode.EAGER);
|
canvasContainer.getStyle()
|
||||||
taxRateField.setValueChangeMode(ValueChangeMode.EAGER);
|
.set("background-color", "#e8e8e8")
|
||||||
introTextArea.setValueChangeMode(ValueChangeMode.EAGER);
|
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||||
termsTextArea.setValueChangeMode(ValueChangeMode.EAGER);
|
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||||
prefixField.addValueChangeListener(e -> refreshPdf());
|
.set("overflow", "hidden")
|
||||||
ustIdField.addValueChangeListener(e -> refreshPdf());
|
.set("position", "relative");
|
||||||
taxNumberField.addValueChangeListener(e -> refreshPdf());
|
|
||||||
bankNameField.addValueChangeListener(e -> refreshPdf());
|
|
||||||
ibanField.addValueChangeListener(e -> refreshPdf());
|
|
||||||
taxRateField.addValueChangeListener(e -> refreshPdf());
|
|
||||||
introTextArea.addValueChangeListener(e -> refreshPdf());
|
|
||||||
termsTextArea.addValueChangeListener(e -> refreshPdf());
|
|
||||||
|
|
||||||
billingLeft.add(partsTitle, billingEnabled, prefixField, ustIdField, taxNumberField, bankNameField, ibanField,
|
VerticalLayout centerPanel = new VerticalLayout();
|
||||||
taxRateField, introTextArea, termsTextArea);
|
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
|
// Rechte Seite: Eigenschaften
|
||||||
VerticalLayout billingRight = new VerticalLayout();
|
propertiesPanelProfile = createPropertiesPanelForProfile();
|
||||||
billingRight.setWidth("55%");
|
propertiesPanelProfile.setWidth("280px");
|
||||||
billingRight.setPadding(false);
|
propertiesPanelProfile.setHeightFull();
|
||||||
billingRight.setSpacing(false);
|
propertiesPanelProfile.getStyle()
|
||||||
billingRight.setHeightFull();
|
.set("flex-shrink", "0")
|
||||||
billingTab.setFlexGrow(1, billingRight);
|
.set("min-width", "280px")
|
||||||
H3 previewTitle = new H3("Rechnungsvorschau");
|
.set("overflow", "auto")
|
||||||
previewTitle.getStyle().set("margin", "0 0 var(--lumo-space-s) 0");
|
.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
|
mainLayout.add(leftPanel, centerPanel, propertiesPanelProfile);
|
||||||
Div previewWrapper = new Div();
|
mainLayout.expand(centerPanel);
|
||||||
previewWrapper.setWidth("100%");
|
billingTab.add(mainLayout);
|
||||||
previewWrapper.setHeight("650px");
|
billingTab.expand(mainLayout);
|
||||||
previewWrapper.getStyle().set("overflow", "hidden").set("background", "var(--lumo-contrast-10pct)")
|
|
||||||
.set("padding", "0");
|
|
||||||
|
|
||||||
// Initial noch keine PDF laden (erst bei aktiver Checkbox)
|
// Action Buttons für den Rechnungsgenerator
|
||||||
pdfFrame = new IFrame();
|
HorizontalLayout actionLayout = new HorizontalLayout();
|
||||||
pdfFrame.setWidth("99%");
|
actionLayout.setWidthFull();
|
||||||
pdfFrame.setHeight("98%");
|
actionLayout.setJustifyContentMode(JustifyContentMode.END);
|
||||||
pdfFrame.getStyle().set("border", "none");
|
actionLayout.setSpacing(true);
|
||||||
pdfFrame.getStyle().set("background", "var(--lumo-contrast-10pct)");
|
actionLayout.getStyle().set("margin-top", "var(--lumo-space-s)");
|
||||||
previewWrapper.removeAll();
|
|
||||||
previewWrapper.add(pdfFrame);
|
|
||||||
|
|
||||||
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);
|
Button previewPdfButton = new Button("Vorschau", new Icon(VaadinIcon.EYE));
|
||||||
tabSheet.add("Rechnungsstellung", billingTab);
|
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();
|
loadInvoiceData();
|
||||||
|
|
||||||
|
// Initialize invoice generator when the billing tab is selected
|
||||||
|
// Also register this view instance for JavaScript callbacks
|
||||||
|
tabSheet.addSelectedChangeListener(e -> {
|
||||||
|
if ("Rechnungserstellung".equals(e.getSelectedTab().getLabel())) {
|
||||||
|
getElement().executeJs(
|
||||||
|
"window.invoiceGeneratorViewProfile = $0;" +
|
||||||
|
"setTimeout(function() { " +
|
||||||
|
" if (window.initProfileInvoiceGenerator) { " +
|
||||||
|
" window.initProfileInvoiceGenerator(); " +
|
||||||
|
" } else { " +
|
||||||
|
" console.error('initProfileInvoiceGenerator not found'); " +
|
||||||
|
" } " +
|
||||||
|
"}, 300);", getElement());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Zweiter Tab: Einstellungen (Beispiel mit Schaltern)
|
// Zweiter Tab: Einstellungen (Beispiel mit Schaltern)
|
||||||
VerticalLayout switches = new VerticalLayout();
|
VerticalLayout switches = new VerticalLayout();
|
||||||
switches.setPadding(false);
|
switches.setPadding(false);
|
||||||
@@ -494,25 +553,6 @@ public class EditProfileView extends HorizontalLayout {
|
|||||||
termsTextArea.setEnabled(enabled);
|
termsTextArea.setEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checkbox steuert Aktivierung und PDF
|
|
||||||
private void toggleBilling(boolean enabled) {
|
|
||||||
setBillingFieldsEnabled(enabled);
|
|
||||||
if (enabled) {
|
|
||||||
refreshPdf();
|
|
||||||
} else {
|
|
||||||
if (pdfFrame != null) {
|
|
||||||
pdfFrame.setSrc("about:blank");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Sicherstellen, dass das IFrame den verfügbaren Raum nutzt
|
|
||||||
if (pdfFrame != null) {
|
|
||||||
pdfFrame.setWidth("100%");
|
|
||||||
pdfFrame.setHeight("100%");
|
|
||||||
pdfFrame.getStyle().set("border", "none");
|
|
||||||
pdfFrame.getStyle().set("display", "block");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refreshPdf() {
|
private void refreshPdf() {
|
||||||
try {
|
try {
|
||||||
byte[] bytes = generatePreviewPdf();
|
byte[] bytes = generatePreviewPdf();
|
||||||
@@ -712,4 +752,336 @@ public class EditProfileView extends HorizontalLayout {
|
|||||||
&& !houseNumberField.isInvalid() && !zipField.isInvalid() && !cityField.isInvalid();
|
&& !houseNumberField.isInvalid() && !zipField.isInvalid() && !cityField.isInvalid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Methoden für den Rechnungsgenerator im Profil
|
||||||
|
private void generatePreviewPdfFromProfile() {
|
||||||
|
try {
|
||||||
|
getElement().executeJs(
|
||||||
|
"if (window.getProfileCanvasData) { return window.getProfileCanvasData(); } else { return null; }")
|
||||||
|
.then(result -> {
|
||||||
|
if (result == null) {
|
||||||
|
Notification.show("Fehler: Canvas-Daten konnten nicht gelesen werden", 3000, Notification.Position.BOTTOM_CENTER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String templateData;
|
||||||
|
if (result instanceof elemental.json.JsonValue) {
|
||||||
|
elemental.json.JsonValue jsonValue = (elemental.json.JsonValue) result;
|
||||||
|
templateData = jsonValue.toJson();
|
||||||
|
} else {
|
||||||
|
templateData = result.toString();
|
||||||
|
}
|
||||||
|
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData);
|
||||||
|
showPdfInDialog(pdfBytes);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Notification.show("Fehler beim Generieren der Vorschau: " + ex.getMessage(), 3000, Notification.Position.BOTTOM_CENTER);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Notification.show("Fehler: " + ex.getMessage(), 3000, Notification.Position.BOTTOM_CENTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showPdfInDialog(byte[] pdfBytes) {
|
||||||
|
// Create a stream resource for the PDF
|
||||||
|
String base64Pdf = Base64.getEncoder().encodeToString(pdfBytes);
|
||||||
|
String dataUrl = "data:application/pdf;base64," + base64Pdf;
|
||||||
|
|
||||||
|
// Create dialog
|
||||||
|
Dialog pdfDialog = new Dialog();
|
||||||
|
pdfDialog.setHeaderTitle("PDF Vorschau");
|
||||||
|
pdfDialog.setWidth("90vw");
|
||||||
|
pdfDialog.setHeight("90vh");
|
||||||
|
|
||||||
|
// Create a Div to hold the PDF viewer
|
||||||
|
Div pdfContainer = new Div();
|
||||||
|
pdfContainer.setWidth("100%");
|
||||||
|
pdfContainer.setHeight("100%");
|
||||||
|
pdfContainer.getStyle()
|
||||||
|
.set("display", "flex")
|
||||||
|
.set("flex-direction", "column")
|
||||||
|
.set("overflow", "hidden");
|
||||||
|
|
||||||
|
// Use an iframe with data URL for PDF display
|
||||||
|
IFrame pdfFrame = new IFrame();
|
||||||
|
pdfFrame.setWidth("100%");
|
||||||
|
pdfFrame.setHeight("100%");
|
||||||
|
pdfFrame.getElement().setAttribute("src", dataUrl);
|
||||||
|
pdfFrame.getStyle()
|
||||||
|
.set("border", "none")
|
||||||
|
.set("flex-grow", "1");
|
||||||
|
|
||||||
|
pdfContainer.add(pdfFrame);
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
Button closeButton = new Button("Schließen", e -> pdfDialog.close());
|
||||||
|
closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
|
||||||
|
// Download button
|
||||||
|
Button downloadButton = new Button("Herunterladen", e -> {
|
||||||
|
getElement().executeJs(
|
||||||
|
"const link = document.createElement('a');" +
|
||||||
|
"link.href = 'data:application/pdf;base64," + base64Pdf + "';" +
|
||||||
|
"link.download = 'vorschau.pdf';" +
|
||||||
|
"link.click();");
|
||||||
|
});
|
||||||
|
downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
|
|
||||||
|
pdfDialog.add(pdfContainer);
|
||||||
|
pdfDialog.getFooter().add(downloadButton, closeButton);
|
||||||
|
pdfDialog.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panel-Methoden für den Rechnungsgenerator
|
||||||
|
private VerticalLayout createTemplatesPanelForProfile() {
|
||||||
|
VerticalLayout panel = new VerticalLayout();
|
||||||
|
panel.setPadding(false);
|
||||||
|
panel.setSpacing(true);
|
||||||
|
panel.setHeightFull();
|
||||||
|
|
||||||
|
Span header = new Span("Textbausteine");
|
||||||
|
header.getStyle()
|
||||||
|
.set("font-weight", "bold")
|
||||||
|
.set("font-size", "var(--lumo-font-size-l)");
|
||||||
|
|
||||||
|
// Draggable Templates
|
||||||
|
Div textBlock = createDraggableTemplate("Textfeld", VaadinIcon.TEXT_LABEL, "text");
|
||||||
|
Div headerBlock = createDraggableTemplate("Überschrift", VaadinIcon.HEADER, "header");
|
||||||
|
Div dateBlock = createDraggableTemplate("Datum", VaadinIcon.CALENDAR, "date");
|
||||||
|
Div customerBlock = createDraggableTemplate("Kundeninfo", VaadinIcon.USER, "customer");
|
||||||
|
Div companyBlock = createDraggableTemplate("Firmeninfo", VaadinIcon.OFFICE, "company");
|
||||||
|
Div amountBlock = createDraggableTemplate("Betrag", VaadinIcon.COIN_PILES, "amount");
|
||||||
|
Div lineBlock = createDraggableTemplate("Linie", VaadinIcon.LINE_V, "line");
|
||||||
|
Div imageBlock = createDraggableTemplate("Bild", VaadinIcon.PICTURE, "image");
|
||||||
|
|
||||||
|
panel.add(header, textBlock, headerBlock, dateBlock, customerBlock, companyBlock, amountBlock, lineBlock,
|
||||||
|
imageBlock);
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Div createDraggableTemplate(String label, VaadinIcon icon, String type) {
|
||||||
|
Div template = new Div();
|
||||||
|
template.setText(label);
|
||||||
|
template.getStyle()
|
||||||
|
.set("padding", "var(--lumo-space-s)")
|
||||||
|
.set("margin", "var(--lumo-space-xs) 0")
|
||||||
|
.set("background-color", "var(--lumo-base-color)")
|
||||||
|
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||||
|
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||||
|
.set("cursor", "grab")
|
||||||
|
.set("display", "flex")
|
||||||
|
.set("align-items", "center")
|
||||||
|
.set("gap", "var(--lumo-space-s)")
|
||||||
|
.set("user-select", "none");
|
||||||
|
|
||||||
|
Icon templateIcon = icon.create();
|
||||||
|
templateIcon.setSize("var(--lumo-icon-size-s)");
|
||||||
|
template.getElement().insertChild(0, templateIcon.getElement());
|
||||||
|
|
||||||
|
template.getElement().setAttribute("draggable", "true");
|
||||||
|
template.getElement().setAttribute("data-template-type", type);
|
||||||
|
template.getElement().setAttribute("data-template-label", label);
|
||||||
|
|
||||||
|
template.getElement().executeJs(
|
||||||
|
"this.addEventListener('dragstart', function(e) {" +
|
||||||
|
" e.dataTransfer.setData('template-type', this.getAttribute('data-template-type'));" +
|
||||||
|
" e.dataTransfer.setData('template-label', this.getAttribute('data-template-label'));" +
|
||||||
|
" this.style.opacity = '0.5';" +
|
||||||
|
"});" +
|
||||||
|
"this.addEventListener('dragend', function(e) {" +
|
||||||
|
" this.style.opacity = '1';" +
|
||||||
|
"});");
|
||||||
|
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VerticalLayout createPropertiesPanelForProfile() {
|
||||||
|
VerticalLayout panel = new VerticalLayout();
|
||||||
|
panel.setPadding(false);
|
||||||
|
panel.setSpacing(true);
|
||||||
|
panel.setHeightFull();
|
||||||
|
|
||||||
|
Span header = new Span("Eigenschaften");
|
||||||
|
header.getStyle()
|
||||||
|
.set("font-weight", "bold")
|
||||||
|
.set("font-size", "var(--lumo-font-size-l)");
|
||||||
|
|
||||||
|
Div infoText = new Div();
|
||||||
|
infoText.setText("Klicken Sie auf ein Element im Canvas, um dessen Eigenschaften zu bearbeiten.");
|
||||||
|
infoText.getStyle()
|
||||||
|
.set("color", "var(--lumo-secondary-text-color)")
|
||||||
|
.set("font-size", "var(--lumo-font-size-s)");
|
||||||
|
|
||||||
|
panel.add(header, infoText);
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ClientCallable
|
||||||
|
public void updatePropertiesPanel(String elementId, String elementType, String text, Double x, Double y,
|
||||||
|
Integer fontSize, String color, Double width, Double height) {
|
||||||
|
getUI().ifPresent(ui -> ui.access(() -> {
|
||||||
|
propertiesPanelProfile.removeAll();
|
||||||
|
|
||||||
|
Span header = new Span("Eigenschaften");
|
||||||
|
header.getStyle()
|
||||||
|
.set("font-weight", "bold")
|
||||||
|
.set("font-size", "var(--lumo-font-size-l)");
|
||||||
|
|
||||||
|
// Element Typ Anzeige
|
||||||
|
Span typeLabel = new Span("Typ: " + elementType);
|
||||||
|
typeLabel.getStyle().set("font-size", "var(--lumo-font-size-s)");
|
||||||
|
|
||||||
|
propertiesPanelProfile.add(header, typeLabel);
|
||||||
|
|
||||||
|
// Text Feld (nur für Text-Elemente)
|
||||||
|
if (!"line".equals(elementType) && !"image".equals(elementType)) {
|
||||||
|
TextField textField = new TextField("Text");
|
||||||
|
textField.setValue(text != null ? text : "");
|
||||||
|
textField.setWidthFull();
|
||||||
|
textField.addValueChangeListener(e -> {
|
||||||
|
getElement().executeJs(
|
||||||
|
"if (window.updateProfileElementText) { window.updateProfileElementText('" + elementId
|
||||||
|
+ "', $0); }",
|
||||||
|
e.getValue());
|
||||||
|
});
|
||||||
|
propertiesPanelProfile.add(textField);
|
||||||
|
}
|
||||||
|
|
||||||
|
// X Position
|
||||||
|
TextField xField = new TextField("X Position");
|
||||||
|
xField.setValue(x != null ? String.valueOf(Math.round(x)) : "0");
|
||||||
|
xField.setWidthFull();
|
||||||
|
xField.addValueChangeListener(e -> {
|
||||||
|
try {
|
||||||
|
double newX = Double.parseDouble(e.getValue());
|
||||||
|
getElement().executeJs(
|
||||||
|
"if (window.updateProfileElementPosition) { window.updateProfileElementPosition('" + elementId
|
||||||
|
+ "', $0, null); }",
|
||||||
|
newX);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
propertiesPanelProfile.add(xField);
|
||||||
|
|
||||||
|
// Y Position
|
||||||
|
TextField yField = new TextField("Y Position");
|
||||||
|
yField.setValue(y != null ? String.valueOf(Math.round(y)) : "0");
|
||||||
|
yField.setWidthFull();
|
||||||
|
yField.addValueChangeListener(e -> {
|
||||||
|
try {
|
||||||
|
double newY = Double.parseDouble(e.getValue());
|
||||||
|
getElement().executeJs(
|
||||||
|
"if (window.updateProfileElementPosition) { window.updateProfileElementPosition('" + elementId
|
||||||
|
+ "', null, $0); }",
|
||||||
|
newY);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
propertiesPanelProfile.add(yField);
|
||||||
|
|
||||||
|
// Font Size (nur für Text-Elemente)
|
||||||
|
if (!"line".equals(elementType) && !"image".equals(elementType)) {
|
||||||
|
TextField fontSizeField = new TextField("Schriftgröße");
|
||||||
|
fontSizeField.setValue(fontSize != null ? String.valueOf(fontSize) : "16");
|
||||||
|
fontSizeField.setWidthFull();
|
||||||
|
fontSizeField.addValueChangeListener(e -> {
|
||||||
|
try {
|
||||||
|
int newSize = Integer.parseInt(e.getValue());
|
||||||
|
getElement().executeJs(
|
||||||
|
"if (window.updateProfileElementFontSize) { window.updateProfileElementFontSize('"
|
||||||
|
+ elementId + "', $0); }",
|
||||||
|
newSize);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
propertiesPanelProfile.add(fontSizeField);
|
||||||
|
|
||||||
|
// Farbe
|
||||||
|
Div colorContainer = new Div();
|
||||||
|
colorContainer.getStyle()
|
||||||
|
.set("display", "flex")
|
||||||
|
.set("align-items", "center")
|
||||||
|
.set("gap", "var(--lumo-space-s)")
|
||||||
|
.set("margin-top", "var(--lumo-space-s)");
|
||||||
|
|
||||||
|
Span colorLabel = new Span("Farbe");
|
||||||
|
colorLabel.getStyle().set("font-size", "var(--lumo-font-size-s)");
|
||||||
|
|
||||||
|
Input colorPicker = new Input();
|
||||||
|
colorPicker.setType("color");
|
||||||
|
colorPicker.setValue(color != null ? color : "#333333");
|
||||||
|
colorPicker.getStyle()
|
||||||
|
.set("width", "50px")
|
||||||
|
.set("height", "32px")
|
||||||
|
.set("padding", "0")
|
||||||
|
.set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||||
|
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||||
|
.set("cursor", "pointer");
|
||||||
|
|
||||||
|
TextField colorHexField = new TextField();
|
||||||
|
colorHexField.setValue(color != null ? color : "#333333");
|
||||||
|
colorHexField.setWidth("100px");
|
||||||
|
colorHexField.getStyle().set("margin", "0");
|
||||||
|
|
||||||
|
colorPicker.addValueChangeListener(e -> {
|
||||||
|
String newColor = e.getValue();
|
||||||
|
colorHexField.setValue(newColor);
|
||||||
|
getElement().executeJs(
|
||||||
|
"if (window.updateProfileElementColor) { window.updateProfileElementColor('"
|
||||||
|
+ elementId + "', $0); }",
|
||||||
|
newColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
colorHexField.addValueChangeListener(e -> {
|
||||||
|
String newColor = e.getValue();
|
||||||
|
if (newColor.matches("^#[0-9A-Fa-f]{6}$")) {
|
||||||
|
colorPicker.setValue(newColor);
|
||||||
|
getElement().executeJs(
|
||||||
|
"if (window.updateProfileElementColor) { window.updateProfileElementColor('"
|
||||||
|
+ elementId + "', $0); }",
|
||||||
|
newColor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
colorContainer.add(colorLabel, colorPicker, colorHexField);
|
||||||
|
propertiesPanelProfile.add(colorContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Löschen Button
|
||||||
|
Button deleteButton = new Button("Element löschen", new Icon(VaadinIcon.TRASH));
|
||||||
|
deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
|
||||||
|
deleteButton.setWidthFull();
|
||||||
|
deleteButton.addClickListener(e -> {
|
||||||
|
getElement().executeJs(
|
||||||
|
"if (window.deleteProfileElement) { window.deleteProfileElement('" + elementId
|
||||||
|
+ "'); }");
|
||||||
|
resetPropertiesPanel();
|
||||||
|
});
|
||||||
|
propertiesPanelProfile.add(deleteButton);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ClientCallable
|
||||||
|
public void resetPropertiesPanel() {
|
||||||
|
getUI().ifPresent(ui -> ui.access(() -> {
|
||||||
|
if (propertiesPanelProfile == null) return;
|
||||||
|
|
||||||
|
propertiesPanelProfile.removeAll();
|
||||||
|
|
||||||
|
Span header = new Span("Eigenschaften");
|
||||||
|
header.getStyle()
|
||||||
|
.set("font-weight", "bold")
|
||||||
|
.set("font-size", "var(--lumo-font-size-l)");
|
||||||
|
|
||||||
|
Div infoText = new Div();
|
||||||
|
infoText.setText("Klicken Sie auf ein Element im Canvas, um dessen Eigenschaften zu bearbeiten.");
|
||||||
|
infoText.getStyle()
|
||||||
|
.set("color", "var(--lumo-secondary-text-color)")
|
||||||
|
.set("font-size", "var(--lumo-font-size-s)");
|
||||||
|
|
||||||
|
propertiesPanelProfile.add(header, infoText);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,10 @@ package de.assecutor.votianlt.pages.view;
|
|||||||
|
|
||||||
import com.vaadin.flow.component.AttachEvent;
|
import com.vaadin.flow.component.AttachEvent;
|
||||||
import com.vaadin.flow.component.ClientCallable;
|
import com.vaadin.flow.component.ClientCallable;
|
||||||
import com.vaadin.flow.component.Component;
|
|
||||||
import com.vaadin.flow.component.UI;
|
|
||||||
import com.vaadin.flow.component.button.Button;
|
import com.vaadin.flow.component.button.Button;
|
||||||
import com.vaadin.flow.component.button.ButtonVariant;
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
import com.vaadin.flow.component.dependency.JsModule;
|
import com.vaadin.flow.component.dependency.JsModule;
|
||||||
import com.vaadin.flow.component.page.PendingJavaScriptResult;
|
|
||||||
import com.vaadin.flow.server.Command;
|
|
||||||
import com.vaadin.flow.component.html.Div;
|
import com.vaadin.flow.component.html.Div;
|
||||||
import com.vaadin.flow.component.html.H2;
|
|
||||||
import com.vaadin.flow.component.html.Span;
|
import com.vaadin.flow.component.html.Span;
|
||||||
import com.vaadin.flow.component.icon.Icon;
|
import com.vaadin.flow.component.icon.Icon;
|
||||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||||
@@ -18,17 +13,20 @@ import com.vaadin.flow.component.notification.Notification;
|
|||||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
import com.vaadin.flow.component.textfield.TextField;
|
import com.vaadin.flow.component.textfield.TextField;
|
||||||
|
import com.vaadin.flow.component.html.Input;
|
||||||
|
import com.vaadin.flow.component.dialog.Dialog;
|
||||||
|
import com.vaadin.flow.component.html.IFrame;
|
||||||
|
import com.vaadin.flow.server.StreamResource;
|
||||||
|
import elemental.json.JsonValue;
|
||||||
|
import elemental.json.JsonType;
|
||||||
import com.vaadin.flow.component.upload.Upload;
|
import com.vaadin.flow.component.upload.Upload;
|
||||||
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
|
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
|
||||||
import com.vaadin.flow.router.PageTitle;
|
import com.vaadin.flow.router.PageTitle;
|
||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
import com.vaadin.flow.server.StreamResource;
|
|
||||||
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
|
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
|
||||||
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
||||||
import jakarta.annotation.security.RolesAllowed;
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
|
|
||||||
@Route(value = "invoice-generator", layout = AdminLayout.class)
|
@Route(value = "invoice-generator", layout = AdminLayout.class)
|
||||||
@PageTitle("Rechnungsgenerator")
|
@PageTitle("Rechnungsgenerator")
|
||||||
@RolesAllowed("ADMIN")
|
@RolesAllowed("ADMIN")
|
||||||
@@ -48,41 +46,59 @@ public class InvoiceGeneratorView extends VerticalLayout {
|
|||||||
|
|
||||||
setSpacing(false);
|
setSpacing(false);
|
||||||
setPadding(false);
|
setPadding(false);
|
||||||
getStyle().set("margin", "14px");
|
setMargin(false);
|
||||||
setWidth("100%");
|
setWidth("100%");
|
||||||
setHeight("100%");
|
setHeight("100%");
|
||||||
|
getStyle()
|
||||||
H2 title = new H2("Rechnungsgenerator");
|
.set("overflow", "hidden")
|
||||||
add(title);
|
.set("box-sizing", "border-box")
|
||||||
|
.set("display", "flex")
|
||||||
|
.set("flex-direction", "column");
|
||||||
|
|
||||||
// Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften)
|
// Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften)
|
||||||
HorizontalLayout mainLayout = new HorizontalLayout();
|
HorizontalLayout mainLayout = new HorizontalLayout();
|
||||||
mainLayout.setWidth("100%");
|
mainLayout.setWidth("100%");
|
||||||
mainLayout.setHeight("calc(100vh - 150px)");
|
mainLayout.setHeight("calc(100vh - 60px)"); // Abzug für Action-Buttons
|
||||||
mainLayout.setSpacing(true);
|
mainLayout.setSpacing(true);
|
||||||
|
mainLayout.getStyle().set("overflow", "hidden");
|
||||||
|
|
||||||
// Linke Seite: Templates/Bausteine
|
// Linke Seite: Templates/Bausteine
|
||||||
VerticalLayout leftPanel = createTemplatesPanel();
|
VerticalLayout leftPanel = createTemplatesPanel();
|
||||||
leftPanel.setWidth("250px");
|
leftPanel.setWidth("250px");
|
||||||
leftPanel.setHeightFull();
|
leftPanel.setHeightFull();
|
||||||
|
leftPanel.getStyle()
|
||||||
|
.set("flex-shrink", "0")
|
||||||
|
.set("min-width", "250px")
|
||||||
|
.set("overflow", "auto");
|
||||||
|
|
||||||
// Mitte: Canvas mit Konva.js
|
// Mitte: Canvas mit Konva.js
|
||||||
VerticalLayout centerPanel = createCanvasPanel();
|
VerticalLayout centerPanel = createCanvasPanel();
|
||||||
centerPanel.setWidth("60%");
|
centerPanel.setWidth("60%");
|
||||||
centerPanel.setHeightFull();
|
centerPanel.setHeightFull();
|
||||||
|
centerPanel.getStyle()
|
||||||
|
.set("flex-grow", "1")
|
||||||
|
.set("min-width", "0");
|
||||||
|
|
||||||
// Rechte Seite: Eigenschaften
|
// Rechte Seite: Eigenschaften
|
||||||
propertiesPanel = createPropertiesPanel();
|
propertiesPanel = createPropertiesPanel();
|
||||||
propertiesPanel.setWidth("300px");
|
propertiesPanel.setWidth("300px");
|
||||||
propertiesPanel.setHeightFull();
|
propertiesPanel.setHeightFull();
|
||||||
|
propertiesPanel.getStyle()
|
||||||
|
.set("flex-shrink", "0")
|
||||||
|
.set("min-width", "300px")
|
||||||
|
.set("overflow", "auto");
|
||||||
|
|
||||||
mainLayout.add(leftPanel, centerPanel, propertiesPanel);
|
mainLayout.add(leftPanel, centerPanel, propertiesPanel);
|
||||||
mainLayout.expand(centerPanel);
|
mainLayout.expand(centerPanel);
|
||||||
|
|
||||||
add(mainLayout);
|
add(mainLayout);
|
||||||
|
|
||||||
// Aktions-Buttons unter dem Canvas
|
// Aktions-Buttons unter dem Canvas (fixe Höhe)
|
||||||
HorizontalLayout actionLayout = createActionButtons();
|
HorizontalLayout actionLayout = createActionButtons();
|
||||||
|
actionLayout.setHeight("60px");
|
||||||
|
actionLayout.getStyle()
|
||||||
|
.set("flex-shrink", "0")
|
||||||
|
.set("padding", "0 var(--lumo-space-m)");
|
||||||
add(actionLayout);
|
add(actionLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,9 +120,11 @@ public class InvoiceGeneratorView extends VerticalLayout {
|
|||||||
VerticalLayout panel = new VerticalLayout();
|
VerticalLayout panel = new VerticalLayout();
|
||||||
panel.setPadding(true);
|
panel.setPadding(true);
|
||||||
panel.setSpacing(true);
|
panel.setSpacing(true);
|
||||||
|
panel.setHeightFull();
|
||||||
panel.getStyle()
|
panel.getStyle()
|
||||||
.set("background-color", "var(--lumo-contrast-5pct)")
|
.set("background-color", "var(--lumo-contrast-5pct)")
|
||||||
.set("border-radius", "var(--lumo-border-radius-m)");
|
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||||
|
.set("overflow", "auto");
|
||||||
|
|
||||||
Span header = new Span("Textbausteine");
|
Span header = new Span("Textbausteine");
|
||||||
header.getStyle()
|
header.getStyle()
|
||||||
@@ -221,9 +239,11 @@ public class InvoiceGeneratorView extends VerticalLayout {
|
|||||||
VerticalLayout panel = new VerticalLayout();
|
VerticalLayout panel = new VerticalLayout();
|
||||||
panel.setPadding(true);
|
panel.setPadding(true);
|
||||||
panel.setSpacing(true);
|
panel.setSpacing(true);
|
||||||
|
panel.setHeightFull();
|
||||||
panel.getStyle()
|
panel.getStyle()
|
||||||
.set("background-color", "var(--lumo-contrast-5pct)")
|
.set("background-color", "var(--lumo-contrast-5pct)")
|
||||||
.set("border-radius", "var(--lumo-border-radius-m)");
|
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||||
|
.set("overflow", "auto");
|
||||||
|
|
||||||
Span header = new Span("Eigenschaften");
|
Span header = new Span("Eigenschaften");
|
||||||
header.getStyle()
|
header.getStyle()
|
||||||
@@ -246,7 +266,9 @@ public class InvoiceGeneratorView extends VerticalLayout {
|
|||||||
HorizontalLayout layout = new HorizontalLayout();
|
HorizontalLayout layout = new HorizontalLayout();
|
||||||
layout.setWidth("100%");
|
layout.setWidth("100%");
|
||||||
layout.setJustifyContentMode(JustifyContentMode.END);
|
layout.setJustifyContentMode(JustifyContentMode.END);
|
||||||
layout.setPadding(true);
|
layout.setPadding(false);
|
||||||
|
layout.setSpacing(true);
|
||||||
|
layout.setAlignItems(Alignment.CENTER);
|
||||||
|
|
||||||
Button clearButton = new Button("Canvas leeren", new Icon(VaadinIcon.TRASH));
|
Button clearButton = new Button("Canvas leeren", new Icon(VaadinIcon.TRASH));
|
||||||
clearButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
|
clearButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
|
||||||
@@ -257,9 +279,7 @@ public class InvoiceGeneratorView extends VerticalLayout {
|
|||||||
|
|
||||||
Button previewButton = new Button("Vorschau", new Icon(VaadinIcon.EYE));
|
Button previewButton = new Button("Vorschau", new Icon(VaadinIcon.EYE));
|
||||||
previewButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST);
|
previewButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST);
|
||||||
previewButton.addClickListener(e -> {
|
previewButton.addClickListener(e -> generatePreviewPdf());
|
||||||
getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.generatePreview(); }");
|
|
||||||
});
|
|
||||||
|
|
||||||
Button saveTemplateButton = new Button("Template speichern", new Icon(VaadinIcon.DOWNLOAD));
|
Button saveTemplateButton = new Button("Template speichern", new Icon(VaadinIcon.DOWNLOAD));
|
||||||
saveTemplateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
saveTemplateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
@@ -294,6 +314,92 @@ public class InvoiceGeneratorView extends VerticalLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void generatePreviewPdf() {
|
||||||
|
try {
|
||||||
|
getElement().executeJs(
|
||||||
|
"if (window.invoiceGenerator) { return window.invoiceGenerator.exportTemplateJson(); } else { return null; }")
|
||||||
|
.then(result -> {
|
||||||
|
if (result == null) {
|
||||||
|
showNotification("Fehler: Canvas-Daten konnten nicht gelesen werden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String templateData;
|
||||||
|
if (result instanceof JsonValue) {
|
||||||
|
JsonValue jsonValue = (JsonValue) result;
|
||||||
|
if (jsonValue.getType() == JsonType.STRING) {
|
||||||
|
templateData = jsonValue.asString();
|
||||||
|
} else {
|
||||||
|
templateData = jsonValue.toJson();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templateData = result.toString();
|
||||||
|
}
|
||||||
|
byte[] pdfBytes = customerInvoiceService.generatePdfFromCanvasTemplate(templateData);
|
||||||
|
showPdfInDialog(pdfBytes);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
showNotification("Fehler beim Generieren der Vorschau: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception ex) {
|
||||||
|
showNotification("Fehler: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showPdfInDialog(byte[] pdfBytes) {
|
||||||
|
// Create a stream resource for the PDF
|
||||||
|
StreamResource resource = new StreamResource("preview.pdf", () -> new java.io.ByteArrayInputStream(pdfBytes));
|
||||||
|
resource.setContentType("application/pdf");
|
||||||
|
resource.setCacheTime(0);
|
||||||
|
|
||||||
|
// Create dialog
|
||||||
|
Dialog pdfDialog = new Dialog();
|
||||||
|
pdfDialog.setHeaderTitle("PDF Vorschau");
|
||||||
|
pdfDialog.setWidth("90vw");
|
||||||
|
pdfDialog.setHeight("90vh");
|
||||||
|
|
||||||
|
// Create a Div to hold the PDF viewer
|
||||||
|
Div pdfContainer = new Div();
|
||||||
|
pdfContainer.setWidth("100%");
|
||||||
|
pdfContainer.setHeight("100%");
|
||||||
|
pdfContainer.getStyle()
|
||||||
|
.set("display", "flex")
|
||||||
|
.set("flex-direction", "column")
|
||||||
|
.set("overflow", "hidden");
|
||||||
|
|
||||||
|
// Use an iframe with data URL for PDF display
|
||||||
|
String base64Pdf = java.util.Base64.getEncoder().encodeToString(pdfBytes);
|
||||||
|
String dataUrl = "data:application/pdf;base64," + base64Pdf;
|
||||||
|
|
||||||
|
IFrame pdfFrame = new IFrame();
|
||||||
|
pdfFrame.setWidth("100%");
|
||||||
|
pdfFrame.setHeight("100%");
|
||||||
|
pdfFrame.getElement().setAttribute("src", dataUrl);
|
||||||
|
pdfFrame.getStyle()
|
||||||
|
.set("border", "none")
|
||||||
|
.set("flex-grow", "1");
|
||||||
|
|
||||||
|
pdfContainer.add(pdfFrame);
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
Button closeButton = new Button("Schließen", e -> pdfDialog.close());
|
||||||
|
closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
|
||||||
|
// Download button
|
||||||
|
Button downloadButton = new Button("Herunterladen", e -> {
|
||||||
|
getElement().executeJs(
|
||||||
|
"const link = document.createElement('a');" +
|
||||||
|
"link.href = 'data:application/pdf;base64," + base64Pdf + "';" +
|
||||||
|
"link.download = 'vorschau.pdf';" +
|
||||||
|
"link.click();");
|
||||||
|
});
|
||||||
|
downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
|
|
||||||
|
pdfDialog.add(pdfContainer);
|
||||||
|
pdfDialog.getFooter().add(downloadButton, closeButton);
|
||||||
|
pdfDialog.open();
|
||||||
|
}
|
||||||
|
|
||||||
private void showNotification(String message) {
|
private void showNotification(String message) {
|
||||||
Notification.show(message, 3000, Notification.Position.BOTTOM_CENTER);
|
Notification.show(message, 3000, Notification.Position.BOTTOM_CENTER);
|
||||||
}
|
}
|
||||||
@@ -413,6 +519,114 @@ public class InvoiceGeneratorView extends VerticalLayout {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
propertiesPanel.add(fontSizeField);
|
propertiesPanel.add(fontSizeField);
|
||||||
|
|
||||||
|
// Schriftfarbe mit Dialog
|
||||||
|
Span colorLabel = new Span("Schriftfarbe");
|
||||||
|
colorLabel.getStyle().set("font-size", "var(--lumo-font-size-s)");
|
||||||
|
propertiesPanel.add(colorLabel);
|
||||||
|
|
||||||
|
// Aktuelle Farbe anzeigen und klickbar machen
|
||||||
|
String currentColor = color != null ? color : "#333333";
|
||||||
|
|
||||||
|
HorizontalLayout colorPreviewLayout = new HorizontalLayout();
|
||||||
|
colorPreviewLayout.setSpacing(true);
|
||||||
|
colorPreviewLayout.setAlignItems(Alignment.CENTER);
|
||||||
|
colorPreviewLayout.setWidthFull();
|
||||||
|
colorPreviewLayout.getStyle().set("cursor", "pointer");
|
||||||
|
|
||||||
|
// Farbvorschau-Box
|
||||||
|
Div colorPreview = new Div();
|
||||||
|
colorPreview.getStyle()
|
||||||
|
.set("width", "40px")
|
||||||
|
.set("height", "30px")
|
||||||
|
.set("background-color", currentColor)
|
||||||
|
.set("border", "1px solid var(--lumo-contrast-30pct)")
|
||||||
|
.set("border-radius", "var(--lumo-border-radius-m)");
|
||||||
|
|
||||||
|
Span colorHexLabel = new Span(currentColor);
|
||||||
|
colorHexLabel.getStyle()
|
||||||
|
.set("font-family", "monospace")
|
||||||
|
.set("font-size", "var(--lumo-font-size-s)");
|
||||||
|
|
||||||
|
colorPreviewLayout.add(colorPreview, colorHexLabel);
|
||||||
|
|
||||||
|
// Color Picker Dialog
|
||||||
|
Dialog colorDialog = new Dialog();
|
||||||
|
colorDialog.setHeaderTitle("Schriftfarbe wählen");
|
||||||
|
|
||||||
|
VerticalLayout dialogLayout = new VerticalLayout();
|
||||||
|
dialogLayout.setSpacing(true);
|
||||||
|
dialogLayout.setPadding(true);
|
||||||
|
|
||||||
|
// Color Picker im Dialog
|
||||||
|
Input dialogColorPicker = new Input();
|
||||||
|
dialogColorPicker.setType("color");
|
||||||
|
dialogColorPicker.setValue(currentColor);
|
||||||
|
dialogColorPicker.getStyle()
|
||||||
|
.set("width", "100%")
|
||||||
|
.set("height", "50px")
|
||||||
|
.set("padding", "0");
|
||||||
|
|
||||||
|
// Hex-Eingabe im Dialog
|
||||||
|
TextField dialogHexField = new TextField("Hex-Farbwert");
|
||||||
|
dialogHexField.setValue(currentColor);
|
||||||
|
dialogHexField.setWidthFull();
|
||||||
|
|
||||||
|
// Sync zwischen Color Picker und Hex-Feld
|
||||||
|
dialogColorPicker.addValueChangeListener(e -> {
|
||||||
|
dialogHexField.setValue(e.getValue());
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogHexField.addValueChangeListener(e -> {
|
||||||
|
String newColor = e.getValue();
|
||||||
|
if (newColor.matches("^#[0-9A-Fa-f]{6}$")) {
|
||||||
|
dialogColorPicker.setValue(newColor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogLayout.add(dialogColorPicker, dialogHexField);
|
||||||
|
colorDialog.add(dialogLayout);
|
||||||
|
|
||||||
|
// Dialog Buttons
|
||||||
|
Button dialogCancelButton = new Button("Abbrechen", e -> {
|
||||||
|
colorDialog.close();
|
||||||
|
// Reset to original values
|
||||||
|
dialogColorPicker.setValue(currentColor);
|
||||||
|
dialogHexField.setValue(currentColor);
|
||||||
|
});
|
||||||
|
dialogCancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
|
|
||||||
|
Button dialogApplyButton = new Button("Übernehmen", e -> {
|
||||||
|
String newColor = dialogColorPicker.getValue();
|
||||||
|
// Update preview
|
||||||
|
colorPreview.getStyle().set("background-color", newColor);
|
||||||
|
colorHexLabel.setText(newColor);
|
||||||
|
// Apply to element
|
||||||
|
getElement().executeJs(
|
||||||
|
"if (window.invoiceGenerator) { window.invoiceGenerator.updateElementColor('"
|
||||||
|
+ elementId + "', $0); }",
|
||||||
|
newColor);
|
||||||
|
colorDialog.close();
|
||||||
|
showNotification("Farbe übernommen");
|
||||||
|
});
|
||||||
|
dialogApplyButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
|
||||||
|
colorDialog.getFooter().add(dialogCancelButton, dialogApplyButton);
|
||||||
|
|
||||||
|
// Öffne Dialog beim Klick auf die Vorschau
|
||||||
|
Runnable openColorDialog = () -> {
|
||||||
|
// Aktualisiere Dialog mit aktuellem Wert
|
||||||
|
String actualColor = colorHexLabel.getText();
|
||||||
|
dialogColorPicker.setValue(actualColor);
|
||||||
|
dialogHexField.setValue(actualColor);
|
||||||
|
colorDialog.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
colorPreviewLayout.addClickListener(e -> openColorDialog.run());
|
||||||
|
colorPreview.addClickListener(e -> openColorDialog.run());
|
||||||
|
colorHexLabel.addClickListener(e -> openColorDialog.run());
|
||||||
|
|
||||||
|
propertiesPanel.add(colorPreviewLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Löschen Button
|
// Löschen Button
|
||||||
|
|||||||
@@ -242,4 +242,91 @@ public class CustomerInvoiceService {
|
|||||||
|
|
||||||
return baos.toByteArray();
|
return baos.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a PDF preview from canvas template data.
|
||||||
|
* Creates an HTML representation of the canvas elements and converts it to PDF.
|
||||||
|
*/
|
||||||
|
public byte[] generatePdfFromCanvasTemplate(String jsonTemplateData) throws Exception {
|
||||||
|
// Parse the JSON template data
|
||||||
|
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
|
com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonTemplateData);
|
||||||
|
com.fasterxml.jackson.databind.JsonNode elements = rootNode.get("elements");
|
||||||
|
|
||||||
|
// Build HTML content from canvas elements
|
||||||
|
StringBuilder htmlBuilder = new StringBuilder();
|
||||||
|
htmlBuilder.append("<!DOCTYPE html>");
|
||||||
|
htmlBuilder.append("<html><head>");
|
||||||
|
htmlBuilder.append("<meta charset='UTF-8'>");
|
||||||
|
htmlBuilder.append("<style>");
|
||||||
|
htmlBuilder.append("@page { size: A4; margin: 0; }");
|
||||||
|
htmlBuilder.append("body { margin: 0; padding: 0; width: 210mm; height: 297mm; position: relative; font-family: Arial, sans-serif; }");
|
||||||
|
htmlBuilder.append(".element { position: absolute; }");
|
||||||
|
htmlBuilder.append(".text { white-space: pre-wrap; word-wrap: break-word; }");
|
||||||
|
htmlBuilder.append(".line { border-top: 1px solid #333; }");
|
||||||
|
htmlBuilder.append(".image { max-width: 100%; max-height: 100%; }");
|
||||||
|
htmlBuilder.append("</style>");
|
||||||
|
htmlBuilder.append("</head><body>");
|
||||||
|
|
||||||
|
if (elements != null && elements.isArray()) {
|
||||||
|
for (com.fasterxml.jackson.databind.JsonNode element : elements) {
|
||||||
|
String type = element.has("type") ? element.get("type").asText("text") : "text";
|
||||||
|
String text = element.has("text") ? element.get("text").asText("") : "";
|
||||||
|
double x = element.has("x") ? element.get("x").asDouble(0) : 0;
|
||||||
|
double y = element.has("y") ? element.get("y").asDouble(0) : 0;
|
||||||
|
double width = element.has("width") ? element.get("width").asDouble(150) : 150;
|
||||||
|
double height = element.has("height") ? element.get("height").asDouble(30) : 30;
|
||||||
|
int fontSize = element.has("fontSize") ? element.get("fontSize").asInt(14) : 14;
|
||||||
|
String fontStyle = element.has("fontStyle") ? element.get("fontStyle").asText("") : "";
|
||||||
|
String color = element.has("color") ? element.get("color").asText("#333333") : "#333333";
|
||||||
|
|
||||||
|
// Convert canvas coordinates to mm (assuming 96 DPI)
|
||||||
|
double mmX = x * 25.4 / 96;
|
||||||
|
double mmY = y * 25.4 / 96;
|
||||||
|
double mmWidth = width * 25.4 / 96;
|
||||||
|
double mmHeight = height * 25.4 / 96;
|
||||||
|
|
||||||
|
htmlBuilder.append("<div class='element ").append(type).append("' ");
|
||||||
|
htmlBuilder.append("style='");
|
||||||
|
htmlBuilder.append("left:").append(String.format(java.util.Locale.US, "%.2f", mmX)).append("mm;");
|
||||||
|
htmlBuilder.append("top:").append(String.format(java.util.Locale.US, "%.2f", mmY)).append("mm;");
|
||||||
|
htmlBuilder.append("width:").append(String.format(java.util.Locale.US, "%.2f", mmWidth)).append("mm;");
|
||||||
|
htmlBuilder.append("height:").append(String.format(java.util.Locale.US, "%.2f", mmHeight)).append("mm;");
|
||||||
|
htmlBuilder.append("font-size:").append(fontSize).append("pt;");
|
||||||
|
htmlBuilder.append("color:").append(color).append(";");
|
||||||
|
if (!fontStyle.isEmpty()) {
|
||||||
|
if (fontStyle.contains("bold")) htmlBuilder.append("font-weight:bold;");
|
||||||
|
}
|
||||||
|
htmlBuilder.append("'");
|
||||||
|
htmlBuilder.append(">");
|
||||||
|
|
||||||
|
// Escape HTML special characters in text
|
||||||
|
text = text.replace("&", "&")
|
||||||
|
.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