diff --git a/src/main/bundles/dev.bundle b/src/main/bundles/dev.bundle index a7dbdec..14e5388 100644 Binary files a/src/main/bundles/dev.bundle and b/src/main/bundles/dev.bundle differ diff --git a/src/main/frontend/invoice-generator/invoice-generator.js b/src/main/frontend/invoice-generator/invoice-generator.js index 892053a..bb65bd3 100644 --- a/src/main/frontend/invoice-generator/invoice-generator.js +++ b/src/main/frontend/invoice-generator/invoice-generator.js @@ -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; })(); diff --git a/src/main/frontend/invoice-generator/profile-invoice-generator.js b/src/main/frontend/invoice-generator/profile-invoice-generator.js new file mode 100644 index 0000000..4214a96 --- /dev/null +++ b/src/main/frontend/invoice-generator/profile-invoice-generator.js @@ -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); + } + }); + } +}; diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java index 5aa5483..2aa85f8 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java @@ -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,93 +271,148 @@ 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"); - - // Felder für Rechnungsstellung (für Live-Update) + // 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(); + + // 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"); + + 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); - billingLeft.add(partsTitle, billingEnabled, prefixField, ustIdField, taxNumberField, bankNameField, ibanField, - taxRateField, introTextArea, termsTextArea); + // 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)"); - // 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"); + mainLayout.add(leftPanel, centerPanel, propertiesPanelProfile); + mainLayout.expand(centerPanel); + billingTab.add(mainLayout); + billingTab.expand(mainLayout); + + // 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)"); + + Button clearButton = new Button("Leeren", new Icon(VaadinIcon.TRASH)); + clearButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); + clearButton.addClickListener(e -> { + getElement().executeJs("if (window.clearProfileCanvas) { window.clearProfileCanvas(); }"); + Notification.show("Canvas wurde geleert", 2000, Notification.Position.BOTTOM_CENTER); + }); + + Button previewPdfButton = new Button("Vorschau", new Icon(VaadinIcon.EYE)); + previewPdfButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS); + previewPdfButton.addClickListener(e -> generatePreviewPdfFromProfile()); + + Button saveTemplateButton = new Button("Template speichern", new Icon(VaadinIcon.DOWNLOAD)); + saveTemplateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + saveTemplateButton.addClickListener(e -> { + getElement().executeJs( + "if (window.getProfileCanvasData) {" + + " var data = JSON.stringify(window.getProfileCanvasData(), null, 2);" + + " var blob = new Blob([data], { type: 'application/json' });" + + " var url = URL.createObjectURL(blob);" + + " var a = document.createElement('a');" + + " a.href = url;" + + " a.download = 'template.json';" + + " a.click();" + + " URL.revokeObjectURL(url);" + + "}"); + }); + + actionLayout.add(clearButton, previewPdfButton, saveTemplateButton); + billingTab.add(actionLayout); + + tabSheet.add("Rechnungserstellung", billingTab); - // Echte PDF-Vorschau mittels StreamResource und iframe - Div previewWrapper = new Div(); - previewWrapper.setWidth("100%"); - previewWrapper.setHeight("650px"); - previewWrapper.getStyle().set("overflow", "hidden").set("background", "var(--lumo-contrast-10pct)") - .set("padding", "0"); - - // Initial noch keine PDF laden (erst bei aktiver Checkbox) - pdfFrame = new IFrame(); - pdfFrame.setWidth("99%"); - pdfFrame.setHeight("98%"); - pdfFrame.getStyle().set("border", "none"); - pdfFrame.getStyle().set("background", "var(--lumo-contrast-10pct)"); - previewWrapper.removeAll(); - previewWrapper.add(pdfFrame); - - billingRight.add(previewTitle, previewWrapper); - - billingTab.add(billingLeft, billingRight); - tabSheet.add("Rechnungsstellung", billingTab); - - // Bestehende Rechnungsdaten laden (nach Erstellung aller Felder) + // 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(); @@ -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); + })); + } + } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java b/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java index 4775d6d..649b6c2 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/InvoiceGeneratorView.java @@ -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 diff --git a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java index b50842a..693fbbd 100644 --- a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java +++ b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java @@ -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(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + + 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("
"); + + // Escape HTML special characters in text + text = text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + + if ("line".equals(type)) { + htmlBuilder.append("
"); + } else if ("image".equals(type)) { + if (element.has("imageData")) { + String imageData = element.get("imageData").asText(); + htmlBuilder.append(""); + } else { + htmlBuilder.append("[Bild]"); + } + } else { + htmlBuilder.append(text); + } + + htmlBuilder.append("
"); + } + } + + htmlBuilder.append(""); + + // Generate PDF from HTML + return generatePdfFromHtmlString(htmlBuilder.toString()); + } }