// Profile Invoice Generator - Initializes canvas on the edit-profile page // Global state to persist between tab switches window.profileInvoiceState = window.profileInvoiceState || { elements: [], selectedElement: null, elementCounter: 0, initialized: false }; window.initProfileInvoiceGenerator = function() { var containerId = 'invoice-canvas-container-profile'; var container = document.getElementById(containerId); if (!container) { console.error('Canvas container not found:', containerId); return; } // Always clear and recreate canvas to ensure clean state container.innerHTML = ''; window.profileInvoiceState.initialized = false; // 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'); // Restore state from global or initialize var elements = window.profileInvoiceState.elements; var selectedElement = window.profileInvoiceState.selectedElement; var elementCounter = window.profileInvoiceState.elementCounter; // Undo history: snapshots of the elements array, max 10 steps back. // Reset on every init so a canvas never restores states of the other // generator page (profile vs. admin system template). var undoStack = []; var MAX_UNDO_STEPS = 10; function pushUndoState() { try { var snapshot = JSON.stringify(elements); if (undoStack.length && undoStack[undoStack.length - 1] === snapshot) return; undoStack.push(snapshot); if (undoStack.length > MAX_UNDO_STEPS) undoStack.shift(); } catch (err) { console.error('Could not record undo state:', err); } } window.undoProfileCanvas = function() { if (!undoStack.length) return false; var snapshot = JSON.parse(undoStack.pop()); // Keep the same array reference (shared with profileInvoiceState) elements.length = 0; snapshot.forEach(function(el) { elements.push(el); }); selectedElement = null; saveState(); notifyElementDeselected(); draw(); return true; }; var isDragging = false; var dragStart = { x: 0, y: 0 }; var elementStart = { x: 0, y: 0 }; var gridSize = 5; // Image cache to prevent flickering during resize var imageCache = {}; // Page dimensions var padding = 10; var pageX, pageY, pageWidth, pageHeight; var zoomFactor = 1; var basePageWidth = 595; // A4 width in pixels at 96 DPI var basePageHeight = 842; // A4 height in pixels at 96 DPI function updatePageDimensions() { var w = canvas.width; var h = canvas.height; var availableWidth = w - padding * 2; var availableHeight = h - padding * 2; // Calculate zoom factor based on available space var zoomX = availableWidth / basePageWidth; var zoomY = availableHeight / basePageHeight; zoomFactor = Math.min(zoomX, zoomY); pageWidth = basePageWidth * zoomFactor; pageHeight = basePageHeight * zoomFactor; 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, el.isStatic || false, el.variable || null ); } } function notifyElementDeselected() { if (window.invoiceGeneratorViewProfile && window.invoiceGeneratorViewProfile.$server) { window.invoiceGeneratorViewProfile.$server.resetPropertiesPanel(); } } // Draw function function draw() { console.log('draw() called, elements:', elements.length); var w = canvas.width; var h = canvas.height; console.log('Canvas size:', w, 'x', h); // 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 (scaled by zoom factor) ctx.strokeStyle = 'rgba(200, 200, 200, 0.3)'; ctx.lineWidth = 0.5; var scaledGridSize = gridSize * zoomFactor; for (var x = pageX; x <= pageX + pageWidth; x += scaledGridSize) { ctx.beginPath(); ctx.moveTo(x, pageY); ctx.lineTo(x, pageY + pageHeight); ctx.stroke(); } for (var y = pageY; y <= pageY + pageHeight; y += scaledGridSize) { ctx.beginPath(); ctx.moveTo(pageX, y); ctx.lineTo(pageX + pageWidth, y); ctx.stroke(); } // Draw elements console.log('Drawing', elements.length, 'elements'); elements.forEach(function(el) { console.log('Drawing element:', el.id, 'at', el.x, el.y, 'text:', el.text); drawElement(el); }); // Draw selection if (selectedElement) { drawSelection(selectedElement); } } function drawElement(el) { ctx.save(); // Apply zoom factor to position and size var x = pageX + (el.x * zoomFactor); var y = pageY + (el.y * zoomFactor); var w = (el.width || 100) * zoomFactor; var h = (el.height || 30) * zoomFactor; var fontSize = (el.fontSize || 14) * zoomFactor; var textAlign = el.textAlign || 'left'; if (el.type === 'line') { ctx.strokeStyle = el.color || '#333333'; ctx.lineWidth = Math.max(1, zoomFactor); ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + w, y); ctx.stroke(); } else if (el.type === 'image') { if (el.imageData) { // Check if image is already cached if (imageCache[el.imageData]) { // Use cached image for flicker-free rendering var img = imageCache[el.imageData]; // Calculate dimensions to maintain aspect ratio (object-fit: contain) var imgAspect = img.width / img.height; var boxAspect = w / h; var drawW, drawH, drawX, drawY; if (imgAspect > boxAspect) { drawW = w; drawH = w / imgAspect; drawX = x; drawY = y + (h - drawH) / 2; } else { drawW = h * imgAspect; drawH = h; drawX = x + (w - drawW) / 2; drawY = y; } ctx.drawImage(img, drawX, drawY, drawW, drawH); } else { // Load and cache the image var img = new Image(); img.onload = function() { imageCache[el.imageData] = img; // Calculate dimensions to maintain aspect ratio var imgAspect = img.width / img.height; var boxAspect = w / h; var drawW, drawH, drawX, drawY; if (imgAspect > boxAspect) { drawW = w; drawH = w / imgAspect; drawX = x; drawY = y + (h - drawH) / 2; } else { drawW = h * imgAspect; drawH = h; drawX = x + (w - drawW) / 2; drawY = y; } ctx.drawImage(img, drawX, drawY, drawW, drawH); // Redraw selection if this element is selected if (selectedElement && selectedElement.id === el.id) { drawSelection(el); } }; img.src = el.imageData; } } else { // Draw placeholder ctx.fillStyle = '#f0f0f0'; ctx.fillRect(x, y, w, h); ctx.strokeStyle = '#999999'; ctx.strokeRect(x, y, w, h); ctx.fillStyle = '#666666'; ctx.font = Math.max(8, 12 * zoomFactor) + 'px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('Bild', x + w / 2, y + h / 2); } } else if (el.variable === 'services.list') { // Draw services list as a table drawServicesTable(el, x, y, w, h, fontSize); } else { // Text elements var lines = (el.text || '').split('\n'); var lineHeight = fontSize * 1.2; var totalTextHeight = lines.length * lineHeight; // Vertically center the text in the element var ty = y + (h - totalTextHeight) / 2; // Draw background highlight for static elements if (el.isStatic) { var textWidth = ctx.measureText(el.text || '').width + (10 * zoomFactor); // Different background colors: green for customer, blue for masterdata if (el.isCustomer) { ctx.fillStyle = 'rgba(46, 204, 113, 0.15)'; // Light green for customer } else { ctx.fillStyle = 'rgba(25, 118, 210, 0.1)'; // Light blue for masterdata } ctx.fillRect(x - (3 * zoomFactor), y - (2 * zoomFactor), Math.max(w, textWidth), h); } // Always use black text for static elements, otherwise use element color ctx.fillStyle = el.isStatic ? '#000000' : (el.color || '#333333'); // Masterdata elements are never bold; other elements respect their fontStyle var fontWeight = (el.isStatic && !el.isCustomer) ? '' : (el.fontStyle || ''); ctx.font = (fontWeight ? fontWeight + ' ' : '') + fontSize + 'px Arial'; ctx.textBaseline = 'top'; ctx.textAlign = textAlign; var textX = x; if (textAlign === 'center') { textX = x + (w / 2); } else if (textAlign === 'right') { textX = x + w; } lines.forEach(function(line) { ctx.fillText(line, textX, ty); ty += lineHeight; }); } ctx.restore(); } // Draw services list as a table with columns: Name, Steuersatz, Nettobetrag // Plus summary section below: Nettosumme, USt, Gesamtsumme function drawServicesTable(el, x, y, w, h, fontSize) { var lineHeight = fontSize * 1.4; var padding = 4 * zoomFactor; var rowHeight = lineHeight + padding * 2; var summaryRowHeight = fontSize * 1.6; var summaryGap = fontSize * 0.5; // Column widths (percentages of total width) var colNameWidth = w * 0.55; // 55% for Name (left-aligned) var colVatWidth = w * 0.20; // 20% for Steuersatz (right-aligned) var colNetWidth = w * 0.25; // 25% for Nettobetrag (right-aligned) var colNameX = x; var colVatX = x + colNameWidth; var colNetX = colVatX + colVatWidth; var vatRate = (window.profileInvoiceVatRate != null) ? window.profileInvoiceVatRate : 0.19; var vatPctLabel = (Math.round(vatRate * 10000) / 100).toString().replace('.', ',') + '%'; // Rows come from window.profileInvoiceServiceData when provided (system // invoice template: the three admin price table positions); otherwise // fall back to sample data (per-user profile invoice generator) var serviceData = window.profileInvoiceServiceData; var rows = (serviceData && serviceData.rows && serviceData.rows.length) ? serviceData.rows : [ { name: 'Umzugsleistung inkl. Verpackung', vat: vatPctLabel, net: '450,00 €' }, { name: 'Entsorgung Möbel', vat: vatPctLabel, net: '85,00 €' }, { name: 'Montage/De-Montage', vat: vatPctLabel, net: '120,00 €' } ]; // Calculate actual content height based on table + summary var tableOnlyHeight = rowHeight * (rows.length + 1); // Header + data rows var summaryOnlyHeight = summaryGap + (summaryRowHeight * 3) + summaryGap + summaryRowHeight + summaryGap; var calculatedContentHeight = tableOnlyHeight + summaryOnlyHeight; // Ensure background covers at least the element height or the calculated content var bgHeight = Math.max(h, calculatedContentHeight * zoomFactor); // Draw orange background highlight for entire service variable element var bgPadding = 3 * zoomFactor; ctx.fillStyle = 'rgba(255, 152, 0, 0.15)'; ctx.fillRect(x - bgPadding, y - (2 * zoomFactor), w + (2 * bgPadding), bgHeight + (4 * zoomFactor)); // Draw table header (overlays the background) ctx.fillStyle = '#f5f5f5'; ctx.fillRect(x, y, w, rowHeight); // Header border ctx.strokeStyle = '#cccccc'; ctx.lineWidth = Math.max(0.5, zoomFactor); ctx.beginPath(); ctx.moveTo(x, y + rowHeight); ctx.lineTo(x + w, y + rowHeight); ctx.stroke(); // Header text ctx.fillStyle = '#333333'; ctx.font = 'bold ' + fontSize + 'px Arial'; ctx.textBaseline = 'middle'; // Name column header (left-aligned) ctx.textAlign = 'left'; ctx.fillText('Name', colNameX + padding, y + rowHeight / 2); // Steuersatz column header (right-aligned) ctx.textAlign = 'right'; ctx.fillText('Steuersatz', colVatX + colVatWidth - padding, y + rowHeight / 2); // Nettobetrag column header (right-aligned) ctx.fillText('Nettobetrag', colNetX + colNetWidth - padding, y + rowHeight / 2); var currentY = y + rowHeight; // Draw data rows ctx.font = fontSize + 'px Arial'; rows.forEach(function(row, index) { // Draw row background (alternating) if (index % 2 === 1) { ctx.fillStyle = 'rgba(0,0,0,0.02)'; ctx.fillRect(x, currentY, w, rowHeight); } // Row bottom border ctx.strokeStyle = '#eeeeee'; ctx.lineWidth = Math.max(0.5, zoomFactor * 0.5); ctx.beginPath(); ctx.moveTo(x, currentY + rowHeight); ctx.lineTo(x + w, currentY + rowHeight); ctx.stroke(); // Draw cell text ctx.fillStyle = '#333333'; // Name (left-aligned) ctx.textAlign = 'left'; ctx.fillText(row.name, colNameX + padding, currentY + rowHeight / 2); // Steuersatz (right-aligned) ctx.textAlign = 'right'; ctx.fillText(row.vat, colVatX + colVatWidth - padding, currentY + rowHeight / 2); // Nettobetrag (right-aligned) ctx.fillText(row.net, colNetX + colNetWidth - padding, currentY + rowHeight / 2); currentY += rowHeight; }); // Draw column separator lines ctx.strokeStyle = '#e0e0e0'; ctx.lineWidth = Math.max(0.5, zoomFactor * 0.5); ctx.beginPath(); // Line between Name and Steuersatz ctx.moveTo(colVatX, y); ctx.lineTo(colVatX, currentY); // Line between Steuersatz and Nettobetrag ctx.moveTo(colNetX, y); ctx.lineTo(colNetX, currentY); ctx.stroke(); // Draw outer border around table ctx.strokeStyle = '#cccccc'; ctx.lineWidth = Math.max(0.5, zoomFactor); ctx.strokeRect(x, y, w, currentY - y); // Draw summary section below the table var summaryY = currentY + summaryGap; // Column positions for summary section var labelX = x + colNameWidth + colVatWidth * 0.3; // Label column (left-aligned) var valueX = x + w - padding; // Value column (right-aligned) // Totals: provided with the service data or calculated from sample data var netTotalLabel, vatTotalLabel, grossTotalLabel; if (serviceData && serviceData.netTotal) { netTotalLabel = serviceData.netTotal; vatTotalLabel = serviceData.vatTotal; grossTotalLabel = serviceData.grossTotal; } else { var netTotal = 655.00; // 450 + 85 + 120 var vatTotal = netTotal * vatRate; netTotalLabel = netTotal.toFixed(2).replace('.', ',') + ' €'; vatTotalLabel = vatTotal.toFixed(2).replace('.', ',') + ' €'; grossTotalLabel = (netTotal + vatTotal).toFixed(2).replace('.', ',') + ' €'; } // Draw summary lines ctx.textBaseline = 'middle'; // Nettosumme - label left, value right ctx.fillStyle = '#333333'; ctx.font = fontSize + 'px Arial'; ctx.textAlign = 'left'; ctx.fillText('Nettosumme:', labelX, summaryY + summaryRowHeight / 2); ctx.font = 'bold ' + fontSize + 'px Arial'; ctx.textAlign = 'right'; ctx.fillText(netTotalLabel, valueX, summaryY + summaryRowHeight / 2); summaryY += summaryRowHeight; // Umsatzsteuer - label left, value right ctx.font = fontSize + 'px Arial'; ctx.textAlign = 'left'; ctx.fillText('zzgl. ' + vatPctLabel + ' USt:', labelX, summaryY + summaryRowHeight / 2); ctx.font = 'bold ' + fontSize + 'px Arial'; ctx.textAlign = 'right'; ctx.fillText(vatTotalLabel, valueX, summaryY + summaryRowHeight / 2); summaryY += summaryRowHeight; // Gesamtsumme - label left, value right summaryY += summaryGap; // Extra gap before total ctx.fillStyle = '#000000'; ctx.font = 'bold ' + (fontSize * 1.1) + 'px Arial'; ctx.textAlign = 'left'; ctx.fillText('Gesamtsumme:', labelX, summaryY + summaryRowHeight / 2); ctx.textAlign = 'right'; ctx.fillText(grossTotalLabel, valueX, summaryY + summaryRowHeight / 2); } function drawSelection(el) { var x = pageX + (el.x * zoomFactor); var y = pageY + (el.y * zoomFactor); var w = (el.width || 100) * zoomFactor; var h = (el.height || 30) * zoomFactor; // For text elements, calculate actual text dimensions if (el.type !== 'line' && el.type !== 'image') { var fontSize = (el.fontSize || 14) * zoomFactor; ctx.font = (el.fontStyle || '') + ' ' + fontSize + 'px Arial'; // For services.list, calculate table height including summary section if (el.variable === 'services.list') { var lineHeight = fontSize * 1.4; var padding = 4 * zoomFactor; var rowHeight = lineHeight + padding * 2; var tableHeight = rowHeight + 3 * rowHeight; // Header + 3 rows var summaryRowHeight = fontSize * 1.6; var summaryGap = fontSize * 0.5; var summaryHeight = summaryGap + (summaryRowHeight * 3) + summaryGap + summaryRowHeight + summaryGap; var totalHeight = tableHeight + summaryHeight; h = Math.max(h, totalHeight); } else { var lines = (el.text || '').split('\n'); var maxLineWidth = 0; lines.forEach(function(line) { var lineWidth = ctx.measureText(line).width; maxLineWidth = Math.max(maxLineWidth, lineWidth); }); // Use the larger of defined width or actual text width w = Math.max(w, maxLineWidth + (10 * zoomFactor)); // Calculate actual height based on number of lines var lineHeight = fontSize * 1.2; var textHeight = lines.length * lineHeight; h = Math.max(h, textHeight + (6 * zoomFactor)); } } ctx.strokeStyle = '#1976d2'; ctx.lineWidth = Math.max(1, 2 * zoomFactor); ctx.setLineDash([5, 3]); ctx.strokeRect(x, y, w, h); ctx.setLineDash([]); // Don't show resize handles for static elements (except services.list) if (el.isStatic && el.variable !== 'services.list') { return; } // Handles var hs = Math.max(6, 8 * zoomFactor); // handle size scaled ctx.fillStyle = '#ffffff'; ctx.strokeStyle = '#1976d2'; ctx.lineWidth = Math.max(1, 2 * zoomFactor); 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 * zoomFactor); var ey = pageY + (el.y * zoomFactor); var ew = (el.width || 100) * zoomFactor; var eh = (el.height || 30) * zoomFactor; // For text elements, calculate actual text dimensions for hit testing if (el.type !== 'line' && el.type !== 'image') { var fontSize = (el.fontSize || 14) * zoomFactor; ctx.font = (el.fontStyle || '') + ' ' + fontSize + 'px Arial'; // For services.list, calculate table height if (el.variable === 'services.list') { var lineHeight = fontSize * 1.4; var padding = 4 * zoomFactor; var rowHeight = lineHeight + padding * 2; var tableHeight = rowHeight + 3 * rowHeight; // Header + 3 rows var summaryRowHeight = fontSize * 1.6; var summaryGap = fontSize * 0.5; var summaryHeight = summaryGap + (summaryRowHeight * 3) + summaryGap + summaryRowHeight + summaryGap; eh = Math.max(eh, tableHeight + summaryHeight); } else { var lines = (el.text || '').split('\n'); var maxLineWidth = 0; lines.forEach(function(line) { var lineWidth = ctx.measureText(line).width; maxLineWidth = Math.max(maxLineWidth, lineWidth); }); ew = Math.max(ew, maxLineWidth + (10 * zoomFactor)); var lineHeight = fontSize * 1.2; var textHeight = lines.length * lineHeight; eh = Math.max(eh, textHeight + (6 * zoomFactor)); } } return x >= ex && x <= ex + ew && y >= ey && y <= ey + eh; } // Resizing state var isResizing = false; var resizeHandle = -1; var resizeStart = { x: 0, y: 0, width: 0, height: 0 }; // Helper function to check if a point is on a resize handle function getResizeHandle(x, y, el) { if (!el) return -1; // Allow resizing for services.list even though it's static if (el.isStatic && el.variable !== 'services.list') return -1; var ex = pageX + (el.x * zoomFactor); var ey = pageY + (el.y * zoomFactor); var ew = (el.width || 100) * zoomFactor; var eh = (el.height || 30) * zoomFactor; // For services.list, calculate table height including summary if (el.variable === 'services.list') { var fontSize = (el.fontSize || 14) * zoomFactor; var lineHeight = fontSize * 1.4; var padding = 4 * zoomFactor; var rowHeight = lineHeight + padding * 2; var tableHeight = rowHeight + 3 * rowHeight; // Header + 3 rows var summaryRowHeight = fontSize * 1.6; var summaryGap = fontSize * 0.5; var summaryHeight = summaryGap + (summaryRowHeight * 3) + summaryGap + summaryRowHeight + summaryGap; eh = Math.max(eh, tableHeight + summaryHeight); } var hs = Math.max(6, 8 * zoomFactor); var halfHs = hs / 2; // Handle positions: 0=TL, 1=TC, 2=TR, 3=ML, 4=MR, 5=BL, 6=BC, 7=BR var handles = [ { x: ex - halfHs, y: ey - halfHs }, { x: ex + ew/2 - halfHs, y: ey - halfHs }, { x: ex + ew - halfHs, y: ey - halfHs }, { x: ex - halfHs, y: ey + eh/2 - halfHs }, { x: ex + ew - halfHs, y: ey + eh/2 - halfHs }, { x: ex - halfHs, y: ey + eh - halfHs }, { x: ex + ew/2 - halfHs, y: ey + eh - halfHs }, { x: ex + ew - halfHs, y: ey + eh - halfHs } ]; for (var i = 0; i < handles.length; i++) { if (x >= handles[i].x && x <= handles[i].x + hs && y >= handles[i].y && y <= handles[i].y + hs) { return i; } } return -1; } // 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 a resize handle of selected element if (selectedElement) { var handle = getResizeHandle(x, y, selectedElement); if (handle >= 0) { pushUndoState(); isResizing = true; resizeHandle = handle; resizeStart = { x: x, y: y, width: selectedElement.width || 100, height: selectedElement.height || 30, elemX: selectedElement.x, elemY: selectedElement.y }; e.preventDefault(); return; } } // 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) { pushUndoState(); 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 (isResizing && selectedElement) { // Calculate delta in base coordinates var dx = (x - resizeStart.x) / zoomFactor; var dy = (y - resizeStart.y) / zoomFactor; var newWidth = resizeStart.width; var newHeight = resizeStart.height; var newX = resizeStart.elemX; var newY = resizeStart.elemY; // Handle indices: 0=TL, 1=TC, 2=TR, 3=ML, 4=MR, 5=BL, 6=BC, 7=BR switch(resizeHandle) { case 0: // Top-left newWidth = Math.max(20, resizeStart.width - dx); newHeight = Math.max(20, resizeStart.height - dy); newX = resizeStart.elemX + (resizeStart.width - newWidth); newY = resizeStart.elemY + (resizeStart.height - newHeight); break; case 1: // Top-center newHeight = Math.max(20, resizeStart.height - dy); newY = resizeStart.elemY + (resizeStart.height - newHeight); break; case 2: // Top-right newWidth = Math.max(20, resizeStart.width + dx); newHeight = Math.max(20, resizeStart.height - dy); newY = resizeStart.elemY + (resizeStart.height - newHeight); break; case 3: // Middle-left newWidth = Math.max(20, resizeStart.width - dx); newX = resizeStart.elemX + (resizeStart.width - newWidth); break; case 4: // Middle-right newWidth = Math.max(20, resizeStart.width + dx); break; case 5: // Bottom-left newWidth = Math.max(20, resizeStart.width - dx); newHeight = Math.max(20, resizeStart.height + dy); newX = resizeStart.elemX + (resizeStart.width - newWidth); break; case 6: // Bottom-center newHeight = Math.max(20, resizeStart.height + dy); break; case 7: // Bottom-right newWidth = Math.max(20, resizeStart.width + dx); newHeight = Math.max(20, resizeStart.height + dy); break; } selectedElement.width = snapToGrid(newWidth); selectedElement.height = snapToGrid(newHeight); selectedElement.x = snapToGrid(newX); selectedElement.y = snapToGrid(newY); draw(); notifyElementSelected(selectedElement); } else if (isDragging && selectedElement) { // Adjust mouse movement by zoom factor to get stored coordinates var dx = (x - dragStart.x) / zoomFactor; var dy = (y - dragStart.y) / zoomFactor; var newX = elementStart.x + dx; var newY = elementStart.y + dy; // Constrain to page (using base coordinates, not zoomed) newX = Math.max(0, Math.min(newX, basePageWidth - (selectedElement.width || 100))); newY = Math.max(0, Math.min(newY, basePageHeight - (selectedElement.height || 30))); // Snap to grid selectedElement.x = snapToGrid(newX); selectedElement.y = snapToGrid(newY); draw(); notifyElementSelected(selectedElement); } else { // Update cursor based on resize handles or element hover if (selectedElement && !selectedElement.isStatic) { var handle = getResizeHandle(x, y, selectedElement); if (handle >= 0) { // Set cursor based on handle type var cursors = ['nwse-resize', 'ns-resize', 'nesw-resize', 'ew-resize', 'ew-resize', 'nesw-resize', 'ns-resize', 'nwse-resize']; canvas.style.cursor = cursors[handle]; } else { 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'; } } else { 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; isResizing = false; resizeHandle = -1; canvas.style.cursor = 'default'; }); canvas.addEventListener('mouseleave', function() { isDragging = false; isResizing = false; resizeHandle = -1; canvas.style.cursor = 'default'; }); // Keyboard navigation canvas.setAttribute('tabindex', '0'); canvas.addEventListener('keydown', function(e) { // Undo with Ctrl+Z / Cmd+Z (works without a selected element) if ((e.ctrlKey || e.metaKey) && (e.key === 'z' || e.key === 'Z')) { window.undoProfileCanvas(); e.preventDefault(); return; } if (!selectedElement) return; if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Delete'].indexOf(e.key) > -1) { pushUndoState(); } var moved = false; switch(e.key) { case 'ArrowUp': if (e.shiftKey) { selectedElement.y = Math.max(0, selectedElement.y - 1); } else { selectedElement.y = Math.max(0, Math.floor((selectedElement.y - 1) / gridSize) * gridSize); } moved = true; e.preventDefault(); break; case 'ArrowDown': if (e.shiftKey) { selectedElement.y = Math.min(basePageHeight - (selectedElement.height || 30), selectedElement.y + 1); } else { selectedElement.y = Math.min(basePageHeight - (selectedElement.height || 30), Math.ceil((selectedElement.y + 1) / gridSize) * gridSize); } moved = true; e.preventDefault(); break; case 'ArrowLeft': if (e.shiftKey) { selectedElement.x = Math.max(0, selectedElement.x - 1); } else { selectedElement.x = Math.max(0, Math.floor((selectedElement.x - 1) / gridSize) * gridSize); } moved = true; e.preventDefault(); break; case 'ArrowRight': if (e.shiftKey) { selectedElement.x = Math.min(basePageWidth - (selectedElement.width || 100), selectedElement.x + 1); } else { selectedElement.x = Math.min(basePageWidth - (selectedElement.width || 100), Math.ceil((selectedElement.x + 1) / gridSize) * gridSize); } 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, isStatic, staticText, variable, isCustomer) { pushUndoState(); elementCounter++; var id = 'element-' + elementCounter; // Convert to page coordinates (adjusting for zoom factor) var x = (dropX - pageX - 50 * zoomFactor) / zoomFactor; var y = (dropY - pageY - 15 * zoomFactor) / zoomFactor; 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', isStatic: isStatic || false, variable: variable || null, isCustomer: isCustomer || false }; // Helper function to calculate height based on font size and lines function calculateHeight(fontSize, lineCount) { var lineHeight = fontSize * 1.2; return Math.round(lineCount * lineHeight + 6); } // Handle static elements (user data or customer data) if (isStatic && staticText) { el.text = staticText; el.color = '#000000'; // Black text for static elements el.fontStyle = 'bold'; // For services.list, set larger dimensions for the table with summary section if (variable === 'services.list') { el.width = 450; var lineHeight = el.fontSize * 1.4; var padding = 4; var rowHeight = lineHeight + padding * 2; var tableHeight = rowHeight + 3 * rowHeight; // Header + 3 sample rows var summaryRowHeight = el.fontSize * 1.6; var summaryGap = el.fontSize * 0.5; var summaryHeight = summaryGap + (summaryRowHeight * 3) + summaryGap + summaryRowHeight + summaryGap; var totalHeight = tableHeight + summaryHeight; el.height = Math.round(totalHeight); } else { el.height = calculateHeight(el.fontSize, 1); } } else { switch (type) { case 'text': el.text = 'Text eingeben...'; el.height = calculateHeight(el.fontSize, 1); break; case 'header': el.text = 'Überschrift'; el.fontSize = 24; el.fontStyle = 'bold'; el.color = '#000000'; el.width = 200; el.height = calculateHeight(el.fontSize, 1); break; case 'date': el.text = 'Datum: ' + new Date().toLocaleDateString('de-DE'); el.fontSize = 12; el.color = '#666666'; el.height = calculateHeight(el.fontSize, 1); break; case 'customer': el.text = 'Kundenname\nStraße Nr.\nPLZ Ort'; el.fontSize = 12; el.height = calculateHeight(el.fontSize, 3); break; case 'company': el.text = 'Ihr Unternehmen\nIhre Straße\nIhre PLZ Ort'; el.fontSize = 12; el.height = calculateHeight(el.fontSize, 3); break; case 'amount': el.text = 'Gesamtbetrag: 0,00 €'; el.fontStyle = 'bold'; el.width = 180; el.height = calculateHeight(el.fontSize, 1); 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 = calculateHeight(el.fontSize, 1); } } 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) { pushUndoState(); el.text = text; draw(); } }; window.updateProfileElementPosition = function(id, x, y) { var el = elements.find(function(e) { return e.id === id; }); if (el) { pushUndoState(); 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) { pushUndoState(); el.fontSize = size; // Update height based on text content and new font size if (el.type !== 'line' && el.type !== 'image') { var lines = (el.text || '').split('\n'); var lineHeight = size * 1.2; var textHeight = lines.length * lineHeight; el.height = Math.max(textHeight + 6, 20); // Minimum height of 20 } draw(); } }; window.updateProfileElementColor = function(id, color) { var el = elements.find(function(e) { return e.id === id; }); if (el) { pushUndoState(); el.color = color; draw(); } }; window.updateProfileElementSize = function(id, width, height) { var el = elements.find(function(e) { return e.id === id; }); if (el) { pushUndoState(); if (width !== null) el.width = width; if (height !== null) el.height = height; draw(); } }; window.updateProfileElementImage = function(id, imageData) { var el = elements.find(function(e) { return e.id === id; }); if (!el) return; // Load image to get dimensions var img = new Image(); img.onload = function() { pushUndoState(); var MAX_WIDTH = 1090; var width = img.width; var height = img.height; // Only resize if image is larger than MAX_WIDTH if (width > MAX_WIDTH) { height = Math.round(height * (MAX_WIDTH / width)); width = MAX_WIDTH; // Create canvas to resize image var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; var ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); // Get resized image data (maintain original format if possible) var resizedDataUrl = canvas.toDataURL('image/jpeg', 0.9); el.imageData = resizedDataUrl; } else { el.imageData = imageData; } el.text = 'Bild'; draw(); }; img.src = imageData; }; window.deleteProfileElement = function(id) { var index = elements.findIndex(function(e) { return e.id === id; }); if (index > -1) { pushUndoState(); elements.splice(index, 1); if (selectedElement && selectedElement.id === id) { selectedElement = null; notifyElementDeselected(); } draw(); } }; // Save state to global before functions are defined function saveState() { window.profileInvoiceState.elements = elements; window.profileInvoiceState.selectedElement = selectedElement; window.profileInvoiceState.elementCounter = elementCounter; } // Clear canvas function window.clearProfileCanvas = function() { if (elements.length) { pushUndoState(); } // Keep the same array reference (shared with profileInvoiceState and undo) elements.length = 0; selectedElement = null; saveState(); notifyElementDeselected(); draw(); }; // Get canvas data // Helper functions for percentage conversion function toPercentX(value) { return (value / basePageWidth * 100).toFixed(2); } function toPercentY(value) { return (value / basePageHeight * 100).toFixed(2); } function fromPercentX(percent) { return Math.round(parseFloat(percent) / 100 * basePageWidth); } function fromPercentY(percent) { return Math.round(parseFloat(percent) / 100 * basePageHeight); } window.getProfileCanvasData = function() { saveState(); // Convert pixel values to percentages for storage var elementsWithPercent = elements.map(function(el) { // For static elements with variables, handle text storage differently: // - masterdata variables: text is resolved from user data, don't store // - customer variables: store the placeholder text from canvas var textToStore = el.text; if (el.isStatic && el.variable) { if (el.variable.startsWith('masterdata.')) { // Don't store the text for masterdata variables - will be resolved from user data textToStore = null; } // For customer variables, keep the text (placeholder) as is } return { id: el.id, type: el.type, text: textToStore, xPercent: toPercentX(el.x), yPercent: toPercentY(el.y), widthPercent: toPercentX(el.width), heightPercent: toPercentY(el.height), fontSize: el.fontSize, fontStyle: el.fontStyle, textAlign: el.textAlign, color: el.color, isStatic: el.isStatic, isCustomer: el.isCustomer, variable: el.variable, imageData: el.imageData }; }); return { elements: elementsWithPercent }; }; window.updateProfileVatRate = function(rate) { if (rate == null || isNaN(rate)) return; window.profileInvoiceVatRate = rate; draw(); }; window.updateProfileMasterdataValue = function(key, value) { if (!window.masterdataValues) window.masterdataValues = {}; window.masterdataValues[key] = value; elements.forEach(function(el) { if (el.variable === key) { el.text = value; } }); draw(); }; draw(); window.profileInvoiceState.initialized = true; console.log('Profile canvas initialized with ' + elements.length + ' elements'); // 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'); var isStatic = e.dataTransfer.getData('is-static') === 'true'; var staticText = e.dataTransfer.getData('static-text'); var variable = e.dataTransfer.getData('variable'); var isCustomer = e.dataTransfer.getData('is-customer') === 'true'; 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, isStatic, staticText, variable, isCustomer); } }); } // Load template function - defined INSIDE initProfileInvoiceGenerator to access closure variables window.loadProfileTemplate = function(templateData) { try { console.log('loadProfileTemplate called with data:', JSON.stringify(templateData).substring(0, 200)); var data = (typeof templateData === 'string') ? JSON.parse(templateData) : templateData; if (data.elements && Array.isArray(data.elements)) { console.log('Loading ' + data.elements.length + ' elements'); console.log('Current elements array before clear:', elements.length); // Clear existing elements - use length = 0 to keep the same array reference elements.length = 0; selectedElement = null; console.log('Elements array after clear:', elements.length); // Master data mapping - these should be filled from user data var masterdata = window.masterdataValues || {}; // Load new elements data.elements.forEach(function(el) { console.log('Loading element:', el.id, 'type:', el.type, 'variable:', el.variable); // Ensure element has an ID if (!el.id) { elementCounter++; el.id = 'element-' + elementCounter; } // Convert percentages to pixels if they exist, otherwise use legacy pixel values if (el.xPercent !== undefined) { el.x = fromPercentX(el.xPercent); } if (el.yPercent !== undefined) { el.y = fromPercentY(el.yPercent); } if (el.widthPercent !== undefined) { el.width = fromPercentX(el.widthPercent); } if (el.heightPercent !== undefined) { el.height = fromPercentY(el.heightPercent); } // Handle elements with variables if (el.variable) { el.isStatic = true; if (el.variable.startsWith('customer.')) { el.isCustomer = true; // Customer variables - use saved text if available, otherwise use placeholder if (!el.text || el.text.trim() === '') { el.text = '[' + el.variable.replace('customer.', '') + ']'; } // If text is already set (from saved template), keep it } else if (el.variable.startsWith('masterdata.')) { // Masterdata variables - use actual value from masterdata or placeholder var value = masterdata[el.variable]; if (value && value.trim() !== '') { el.text = value; } else { el.text = '[' + el.variable.replace('masterdata.', '') + ']'; } } } // Legacy: If text contains a variable placeholder {{variable}}|Display Text, extract it else if (el.text && el.text.startsWith('{{')) { var pipeIndex = el.text.indexOf('|'); if (pipeIndex > -1) { // Format: {{variable}}|Display Text var placeholderPart = el.text.substring(0, pipeIndex); var displayText = el.text.substring(pipeIndex + 1); if (placeholderPart.startsWith('{{') && placeholderPart.endsWith('}}')) { var varName = placeholderPart.substring(2, placeholderPart.length - 2); el.variable = varName; if (varName.startsWith('masterdata.')) { el.isStatic = true; // Use masterdata value or fallback to display text var value = masterdata[varName]; el.text = (value && value.trim() !== '') ? value : displayText; } else if (varName.startsWith('customer.')) { el.isStatic = true; el.isCustomer = true; // Use saved text if available, otherwise use placeholder if (!el.text || el.text.trim() === '' || el.text.startsWith('{{')) { el.text = '[' + varName.replace('customer.', '') + ']'; } } } } else if (el.text.endsWith('}}')) { // Legacy format: {{variable}} without display text var varName = el.text.substring(2, el.text.length - 2); el.variable = varName; if (varName.startsWith('masterdata.')) { el.isStatic = true; var value = masterdata[varName]; el.text = (value && value.trim() !== '') ? value : '[' + varName.replace('masterdata.', '') + ']'; } else if (varName.startsWith('customer.')) { el.isStatic = true; el.isCustomer = true; el.text = '[' + varName.replace('customer.', '') + ']'; } } } console.log('Element processed:', el.id, 'x:', el.x, 'y:', el.y, 'text:', el.text); elements.push(el); console.log('Element pushed, elements count now:', elements.length); }); // Update counter to be higher than any loaded element elements.forEach(function(el) { if (el.id && el.id.startsWith('element-')) { var num = parseInt(el.id.replace('element-', '')); if (!isNaN(num) && num > elementCounter) { elementCounter = num; } } }); // Save to global state saveState(); console.log('Calling draw(), elements count:', elements.length); console.log('Canvas dimensions:', canvas.width, 'x', canvas.height); draw(); console.log('draw() completed'); notifyElementDeselected(); console.log('Template loaded with ' + elements.length + ' elements'); } else { console.log('No elements array found in template data'); } } catch (e) { console.error('Error loading template:', e); } }; };