feat: Admin-Dashboard mit Kundenübersicht, System-Rechnung auf Preistabellen-Positionen umgestellt und Undo im Rechnungsgenerator
- 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>
This commit is contained in:
@@ -40,6 +40,36 @@ window.initProfileInvoiceGenerator = function() {
|
||||
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 };
|
||||
@@ -306,8 +336,23 @@ window.initProfileInvoiceGenerator = function() {
|
||||
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 * 4; // Header + 3 data rows
|
||||
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
|
||||
@@ -346,21 +391,11 @@ window.initProfileInvoiceGenerator = function() {
|
||||
// Nettobetrag column header (right-aligned)
|
||||
ctx.fillText('Nettobetrag', colNetX + colNetWidth - padding, y + rowHeight / 2);
|
||||
|
||||
var vatRate = (window.profileInvoiceVatRate != null) ? window.profileInvoiceVatRate : 0.19;
|
||||
var vatPctLabel = (Math.round(vatRate * 10000) / 100).toString().replace('.', ',') + '%';
|
||||
|
||||
// Sample data rows (placeholder)
|
||||
var sampleData = [
|
||||
{ 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 €' }
|
||||
];
|
||||
|
||||
var currentY = y + rowHeight;
|
||||
|
||||
// Draw sample rows
|
||||
|
||||
// Draw data rows
|
||||
ctx.font = fontSize + 'px Arial';
|
||||
sampleData.forEach(function(row, index) {
|
||||
rows.forEach(function(row, index) {
|
||||
// Draw row background (alternating)
|
||||
if (index % 2 === 1) {
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.02)';
|
||||
@@ -416,11 +451,20 @@ window.initProfileInvoiceGenerator = function() {
|
||||
var labelX = x + colNameWidth + colVatWidth * 0.3; // Label column (left-aligned)
|
||||
var valueX = x + w - padding; // Value column (right-aligned)
|
||||
|
||||
// Calculate totals from sample data
|
||||
var netTotal = 655.00; // 450 + 85 + 120
|
||||
var vatTotal = netTotal * vatRate;
|
||||
var grossTotal = netTotal + vatTotal;
|
||||
|
||||
// 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';
|
||||
|
||||
@@ -431,18 +475,18 @@ window.initProfileInvoiceGenerator = function() {
|
||||
ctx.fillText('Nettosumme:', labelX, summaryY + summaryRowHeight / 2);
|
||||
ctx.font = 'bold ' + fontSize + 'px Arial';
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(netTotal.toFixed(2).replace('.', ',') + ' €', valueX, summaryY + summaryRowHeight / 2);
|
||||
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(vatTotal.toFixed(2).replace('.', ',') + ' €', valueX, summaryY + summaryRowHeight / 2);
|
||||
ctx.fillText(vatTotalLabel, valueX, summaryY + summaryRowHeight / 2);
|
||||
summaryY += summaryRowHeight;
|
||||
|
||||
|
||||
// Gesamtsumme - label left, value right
|
||||
summaryY += summaryGap; // Extra gap before total
|
||||
ctx.fillStyle = '#000000';
|
||||
@@ -450,7 +494,7 @@ window.initProfileInvoiceGenerator = function() {
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText('Gesamtsumme:', labelX, summaryY + summaryRowHeight / 2);
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(grossTotal.toFixed(2).replace('.', ',') + ' €', valueX, summaryY + summaryRowHeight / 2);
|
||||
ctx.fillText(grossTotalLabel, valueX, summaryY + summaryRowHeight / 2);
|
||||
}
|
||||
|
||||
function drawSelection(el) {
|
||||
@@ -630,6 +674,7 @@ window.initProfileInvoiceGenerator = function() {
|
||||
if (selectedElement) {
|
||||
var handle = getResizeHandle(x, y, selectedElement);
|
||||
if (handle >= 0) {
|
||||
pushUndoState();
|
||||
isResizing = true;
|
||||
resizeHandle = handle;
|
||||
resizeStart = {
|
||||
@@ -655,6 +700,7 @@ window.initProfileInvoiceGenerator = function() {
|
||||
}
|
||||
|
||||
if (clickedElement) {
|
||||
pushUndoState();
|
||||
selectedElement = clickedElement;
|
||||
isDragging = true;
|
||||
dragStart = { x: x, y: y };
|
||||
@@ -795,8 +841,19 @@ window.initProfileInvoiceGenerator = function() {
|
||||
// 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) {
|
||||
@@ -856,6 +913,7 @@ window.initProfileInvoiceGenerator = function() {
|
||||
|
||||
// Add element function
|
||||
window.addProfileElement = function(type, label, dropX, dropY, isStatic, staticText, variable, isCustomer) {
|
||||
pushUndoState();
|
||||
elementCounter++;
|
||||
var id = 'element-' + elementCounter;
|
||||
|
||||
@@ -972,23 +1030,26 @@ window.initProfileInvoiceGenerator = function() {
|
||||
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') {
|
||||
@@ -1004,14 +1065,16 @@ window.initProfileInvoiceGenerator = function() {
|
||||
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();
|
||||
@@ -1025,6 +1088,7 @@ window.initProfileInvoiceGenerator = function() {
|
||||
// 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;
|
||||
@@ -1057,6 +1121,7 @@ window.initProfileInvoiceGenerator = function() {
|
||||
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;
|
||||
@@ -1075,10 +1140,13 @@ window.initProfileInvoiceGenerator = function() {
|
||||
|
||||
// Clear canvas function
|
||||
window.clearProfileCanvas = function() {
|
||||
elements = [];
|
||||
if (elements.length) {
|
||||
pushUndoState();
|
||||
}
|
||||
// Keep the same array reference (shared with profileInvoiceState and undo)
|
||||
elements.length = 0;
|
||||
selectedElement = null;
|
||||
window.profileInvoiceState.elements = [];
|
||||
window.profileInvoiceState.selectedElement = null;
|
||||
saveState();
|
||||
notifyElementDeselected();
|
||||
draw();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user