- Admin-Dashboard: neue Sektion mit Grid aller Kunden (Kundennummer, Firma, Name, Kontakt, Ort, zugehöriger Nutzer) - Rechnungsgenerator (System-Template): Positionen ausschließlich aus den drei Parametern der Preistabelle statt Beispiel-Leistungen, in Canvas und PDF-Vorschau; Bausteine Netto-/Bruttosumme entfernt - AdminLayout: gleicher view-container-Rahmen wie MainLayout, Admin-Seiten damit optisch an Nutzer-Seiten angeglichen - Canvas-Generatoren (Admin und Profil): Undo-Historie mit bis zu 10 Schritten per Button und Strg+Z - Übersetzungen für alle 10 Sprachen ergänzt Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1404 lines
48 KiB
JavaScript
1404 lines
48 KiB
JavaScript
// 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);
|
|
}
|
|
};
|
|
};
|