Erweiterungen

This commit is contained in:
2026-02-13 09:16:32 +01:00
parent 27f98ca7d9
commit 120a8e0571
9 changed files with 2031 additions and 2223 deletions

3053
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
<groupId>de.assecutor.votianlt</groupId> <groupId>de.assecutor.votianlt</groupId>
<artifactId>votianlt</artifactId> <artifactId>votianlt</artifactId>
<version>0.9.1</version> <version>0.9.2</version>
<packaging>jar</packaging> <packaging>jar</packaging>

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,721 @@
// Invoice Generator - Native HTML5 Canvas Implementation
(function() {
'use strict';
class InvoiceGenerator {
constructor() {
this.canvas = null;
this.ctx = null;
this.elements = new Map();
this.selectedElement = null;
this.elementCounter = 0;
this.isDragging = false;
this.isResizing = false;
this.dragStart = { x: 0, y: 0 };
this.elementStart = { x: 0, y: 0, w: 0, h: 0 };
this.activeHandle = null;
this.container = null;
// Page dimensions (A4 at 96 DPI)
this.pageX = 50;
this.pageY = 30;
this.pageWidth = 794;
this.pageHeight = 1123;
// Handle configuration
this.handleSize = 10;
this.handlePadding = 3;
}
init() {
this.container = document.getElementById('invoice-canvas-container');
if (!this.container) {
console.error('Canvas container not found');
return;
}
// Create canvas
this.canvas = document.createElement('canvas');
this.canvas.style.display = 'block';
this.updateCanvasSize();
this.ctx = this.canvas.getContext('2d');
this.container.innerHTML = '';
this.container.appendChild(this.canvas);
// Bind events
this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this));
this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this));
this.canvas.addEventListener('mouseleave', this.onMouseUp.bind(this));
window.addEventListener('resize', () => {
this.updateCanvasSize();
this.draw();
});
this.draw();
console.log('Invoice generator initialized');
}
updateCanvasSize() {
const rect = this.container.getBoundingClientRect();
this.canvas.width = rect.width;
this.canvas.height = rect.height;
this.pageX = Math.max(20, (this.canvas.width - this.pageWidth) / 2);
}
getMousePos(e) {
const rect = this.canvas.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
}
// Check if point is inside element
hitTest(x, y, el) {
const w = el.width || 150;
const h = el.height || 30;
return x >= el.x && x <= el.x + w && y >= el.y && y <= el.y + h;
}
// Check if point is on a resize handle
getHandleAt(x, y, el) {
if (!el) return null;
const w = el.width || 150;
const h = el.height || 30;
const hs = this.handleSize + this.handlePadding;
// Define handle positions (center points)
const handles = {
'nw': { x: el.x, y: el.y },
'n': { x: el.x + w / 2, y: el.y },
'ne': { x: el.x + w, y: el.y },
'w': { x: el.x, y: el.y + h / 2 },
'e': { x: el.x + w, y: el.y + h / 2 },
'sw': { x: el.x, y: el.y + h },
's': { x: el.x + w / 2, y: el.y + h },
'se': { x: el.x + w, y: el.y + h }
};
for (const [name, pos] of Object.entries(handles)) {
const dx = x - pos.x;
const dy = y - pos.y;
if (Math.abs(dx) <= hs && Math.abs(dy) <= hs) {
return name;
}
}
return null;
}
onMouseDown(e) {
const pos = this.getMousePos(e);
// First check if clicking on a handle of selected element
if (this.selectedElement) {
const handle = this.getHandleAt(pos.x, pos.y, this.selectedElement);
if (handle) {
console.log('Resize handle clicked:', handle);
this.isResizing = true;
this.activeHandle = handle;
this.dragStart = { x: pos.x, y: pos.y };
this.elementStart = {
x: this.selectedElement.x,
y: this.selectedElement.y,
w: this.selectedElement.width || 100,
h: this.selectedElement.height || 30
};
return;
}
}
// Check if clicking on an element
let clickedElement = null;
for (const el of Array.from(this.elements.values()).reverse()) {
if (this.hitTest(pos.x, pos.y, el)) {
clickedElement = el;
break;
}
}
if (clickedElement) {
this.selectElement(clickedElement.id);
this.isDragging = true;
this.dragStart = { x: pos.x, y: pos.y };
this.elementStart = { x: clickedElement.x, y: clickedElement.y };
this.canvas.style.cursor = 'move';
} else {
this.deselectAll();
}
this.draw();
}
onMouseMove(e) {
const pos = this.getMousePos(e);
if (this.isResizing && this.selectedElement) {
this.doResize(pos.x, pos.y);
return;
}
if (this.isDragging && this.selectedElement) {
const dx = pos.x - this.dragStart.x;
const dy = pos.y - this.dragStart.y;
this.selectedElement.x = this.elementStart.x + dx;
this.selectedElement.y = this.elementStart.y + dy;
// Constrain to page
this.selectedElement.x = Math.max(this.pageX, Math.min(this.selectedElement.x,
this.pageX + this.pageWidth - (this.selectedElement.width || 100)));
this.selectedElement.y = Math.max(this.pageY, Math.min(this.selectedElement.y,
this.pageY + this.pageHeight - (this.selectedElement.height || 30)));
this.draw();
this.notifyChange();
return;
}
// Update cursor
if (this.selectedElement) {
const handle = this.getHandleAt(pos.x, pos.y, this.selectedElement);
if (handle) {
const cursors = {
'nw': 'nw-resize', 'ne': 'ne-resize', 'sw': 'sw-resize', 'se': 'se-resize',
'n': 'ns-resize', 's': 'ns-resize', 'e': 'ew-resize', 'w': 'ew-resize'
};
this.canvas.style.cursor = cursors[handle] || 'default';
return;
}
}
// Check hover over elements
let hovering = false;
for (const el of Array.from(this.elements.values()).reverse()) {
if (this.hitTest(pos.x, pos.y, el)) {
hovering = true;
break;
}
}
this.canvas.style.cursor = hovering ? 'move' : 'default';
}
onMouseUp(e) {
this.isDragging = false;
this.isResizing = false;
this.activeHandle = null;
this.canvas.style.cursor = 'default';
}
doResize(mouseX, mouseY) {
if (!this.selectedElement || !this.activeHandle) return;
const el = this.selectedElement;
el.manuallyResized = true; // Mark as manually resized
const start = this.elementStart;
const minSize = 20;
// Calculate new dimensions based on handle
switch (this.activeHandle) {
case 'se':
el.width = Math.max(minSize, mouseX - start.x);
el.height = Math.max(minSize, mouseY - start.y);
break;
case 'sw':
const newWsw = start.w + (start.x - mouseX);
if (newWsw >= minSize) {
el.x = mouseX;
el.width = newWsw;
}
el.height = Math.max(minSize, mouseY - start.y);
break;
case 'ne':
el.width = Math.max(minSize, mouseX - start.x);
const newHne = start.h + (start.y - mouseY);
if (newHne >= minSize) {
el.y = mouseY;
el.height = newHne;
}
break;
case 'nw':
const newWnw = start.w + (start.x - mouseX);
const newHnw = start.h + (start.y - mouseY);
if (newWnw >= minSize) {
el.x = mouseX;
el.width = newWnw;
}
if (newHnw >= minSize) {
el.y = mouseY;
el.height = newHnw;
}
break;
case 'e':
el.width = Math.max(minSize, mouseX - start.x);
break;
case 'w':
const newWw = start.w + (start.x - mouseX);
if (newWw >= minSize) {
el.x = mouseX;
el.width = newWw;
}
break;
case 's':
el.height = Math.max(minSize, mouseY - start.y);
break;
case 'n':
const newHn = start.h + (start.y - mouseY);
if (newHn >= minSize) {
el.y = mouseY;
el.height = newHn;
}
break;
}
this.draw();
this.notifyChange();
}
notifyChange() {
if (this.selectedElement && window.invoiceGeneratorView?.$server) {
const el = this.selectedElement;
window.invoiceGeneratorView.$server.updatePropertiesPanel(
el.id, el.type, el.text || '', el.x, el.y, el.fontSize || 14, el.color || '#000000'
);
}
}
draw() {
if (!this.ctx) return;
const ctx = this.ctx;
// Clear
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Background
ctx.fillStyle = '#e8e8e8';
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Page shadow
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.fillRect(this.pageX + 4, this.pageY + this.pageHeight, this.pageWidth, 4);
ctx.fillRect(this.pageX + this.pageWidth, this.pageY + 4, 4, this.pageHeight);
// Page
ctx.fillStyle = '#ffffff';
ctx.fillRect(this.pageX, this.pageY, this.pageWidth, this.pageHeight);
ctx.strokeStyle = '#cccccc';
ctx.lineWidth = 1;
ctx.strokeRect(this.pageX, this.pageY, this.pageWidth, this.pageHeight);
// Draw elements
this.elements.forEach(el => this.drawElement(el));
// Draw selection
if (this.selectedElement) {
this.drawSelection(this.selectedElement);
}
}
drawImagePlaceholder(ctx, el) {
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(el.x, el.y, el.width || 100, el.height || 100);
ctx.strokeStyle = '#999999';
ctx.lineWidth = 1;
ctx.strokeRect(el.x, el.y, el.width || 100, el.height || 100);
ctx.fillStyle = '#666666';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Bild', el.x + (el.width || 100) / 2, el.y + (el.height || 100) / 2);
}
drawElement(el) {
const ctx = this.ctx;
ctx.save();
switch (el.type) {
case 'line':
ctx.strokeStyle = el.color || '#333333';
ctx.lineWidth = el.strokeWidth || 1;
ctx.beginPath();
ctx.moveTo(el.x, el.y);
ctx.lineTo(el.x + (el.width || 200), el.y);
ctx.stroke();
break;
case 'image':
if (el.imageData) {
// Draw actual image
const img = el.imageObj;
if (img && img.complete) {
// Draw image scaled to fit element while maintaining aspect ratio
const imgAspect = img.width / img.height;
const elAspect = (el.width || 100) / (el.height || 100);
let drawWidth, drawHeight, drawX, drawY;
if (imgAspect > elAspect) {
// Image is wider than element
drawWidth = el.width || 100;
drawHeight = drawWidth / imgAspect;
drawX = el.x;
drawY = el.y + ((el.height || 100) - drawHeight) / 2;
} else {
// Image is taller than element
drawHeight = el.height || 100;
drawWidth = drawHeight * imgAspect;
drawX = el.x + ((el.width || 100) - drawWidth) / 2;
drawY = el.y;
}
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
// Draw border
ctx.strokeStyle = '#cccccc';
ctx.lineWidth = 1;
ctx.strokeRect(el.x, el.y, el.width || 100, el.height || 100);
} else {
// Image still loading or failed
this.drawImagePlaceholder(ctx, el);
}
} else {
// No image uploaded yet - draw placeholder
this.drawImagePlaceholder(ctx, el);
}
break;
default: // text elements
this.drawTextElement(el, ctx);
}
ctx.restore();
}
// Wrap text to fit within maxWidth
wrapText(ctx, text, maxWidth) {
const words = text.split(' ');
const lines = [];
let currentLine = words[0];
for (let i = 1; i < words.length; i++) {
const word = words[i];
const width = ctx.measureText(currentLine + ' ' + word).width;
if (width < maxWidth) {
currentLine += ' ' + word;
} else {
lines.push(currentLine);
currentLine = word;
}
}
lines.push(currentLine);
return lines;
}
drawTextElement(el, ctx) {
ctx.font = `${el.fontStyle || ''} ${el.fontSize || 14}px ${el.fontFamily || 'Arial'}`;
ctx.fillStyle = el.color || '#333333';
ctx.textBaseline = 'top';
const maxWidth = el.width || 150;
const lineHeight = (el.fontSize || 14) * 1.2;
const maxHeight = el.height || 1000;
// Calculate max lines that fit
const maxLines = Math.floor(maxHeight / lineHeight);
// Split by explicit newlines first, then wrap each line
const explicitLines = (el.text || '').split('\n');
const allLines = [];
for (const line of explicitLines) {
const metrics = ctx.measureText(line);
if (metrics.width <= maxWidth) {
allLines.push(line);
} else {
const wrapped = this.wrapText(ctx, line, maxWidth);
allLines.push(...wrapped);
}
}
// Determine if we need ellipsis
const needsEllipsis = allLines.length > maxLines;
const linesToDraw = needsEllipsis ? maxLines : allLines.length;
// Draw lines
let y = el.y;
for (let i = 0; i < linesToDraw; i++) {
const line = allLines[i];
// If this is the last line and we need ellipsis
if (needsEllipsis && i === linesToDraw - 1) {
const ellipsis = '...';
// Check if the line with ellipsis would fit
if (ctx.measureText(line + ellipsis).width <= maxWidth) {
ctx.fillText(line + ellipsis, el.x, y);
} else {
// Need to trim the line to fit ellipsis
let trimmed = line;
while (trimmed.length > 0 && ctx.measureText(trimmed + ellipsis).width > maxWidth) {
trimmed = trimmed.slice(0, -1);
}
ctx.fillText(trimmed + ellipsis, el.x, y);
}
} else {
ctx.fillText(line, el.x, y);
}
y += lineHeight;
}
// Update height based on actual content if not manually resized
if (!el.manuallyResized) {
el.height = Math.max(lineHeight, allLines.length * lineHeight);
}
}
drawSelection(el) {
const ctx = this.ctx;
const x = el.x;
const y = el.y;
const w = el.width || 150;
const h = el.height || 30;
const hs = this.handleSize;
// Selection border
ctx.strokeStyle = '#1976d2';
ctx.lineWidth = 2;
ctx.setLineDash([5, 3]);
ctx.strokeRect(x, y, w, h);
ctx.setLineDash([]);
// Handles
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#1976d2';
ctx.lineWidth = 2;
const positions = [
[x - hs/2, y - hs/2], // nw
[x + w/2 - hs/2, y - hs/2], // n
[x + w - hs/2, y - hs/2], // ne
[x - hs/2, y + h/2 - hs/2], // w
[x + w - hs/2, y + h/2 - hs/2], // e
[x - hs/2, y + h - hs/2], // sw
[x + w/2 - hs/2, y + h - hs/2], // s
[x + w - hs/2, y + h - hs/2] // se
];
positions.forEach(([hx, hy]) => {
ctx.fillRect(hx, hy, hs, hs);
ctx.strokeRect(hx, hy, hs, hs);
});
}
addElement(type, label, dropX, dropY) {
this.elementCounter++;
const id = `element-${this.elementCounter}`;
const rect = this.container.getBoundingClientRect();
const x = Math.max(this.pageX + 10, dropX - rect.left - 50);
const y = Math.max(this.pageY + 10, dropY - rect.top - 15);
let el = {
id, type, x, y,
width: 150,
height: 30,
fontSize: 14,
fontFamily: 'Arial',
color: '#333333'
};
switch (type) {
case 'text':
el.text = 'Text eingeben...';
el.height = 20;
break;
case 'header':
el.text = 'Überschrift';
el.fontSize = 24;
el.fontStyle = 'bold';
el.color = '#000000';
el.width = 200;
el.height = 30;
break;
case 'date':
el.text = `Datum: ${new Date().toLocaleDateString('de-DE')}`;
el.fontSize = 12;
el.color = '#666666';
el.height = 16;
break;
case 'customer':
case 'company':
el.text = type === 'customer' ? 'Kundenname\nStraße Nr.\nPLZ Ort' : 'Ihr Unternehmen\nIhre Straße\nIhre PLZ Ort';
el.height = 50;
el.fontSize = 12;
break;
case 'amount':
el.text = 'Gesamtbetrag: 0,00 €';
el.fontStyle = 'bold';
el.width = 180;
el.height = 20;
break;
case 'line':
el.text = '';
el.width = 200;
el.height = 2;
break;
case 'image':
el.text = 'Bild';
el.width = 100;
el.height = 100;
break;
default:
el.text = label || 'Neues Element';
el.height = 20;
}
this.elements.set(id, el);
this.selectElement(id);
this.draw();
}
selectElement(id) {
this.selectedElement = this.elements.get(id) || null;
this.draw();
if (this.selectedElement) {
this.notifyChange();
}
}
deselectAll() {
this.selectedElement = null;
this.draw();
if (window.invoiceGeneratorView?.$server) {
window.invoiceGeneratorView.$server.resetPropertiesPanel();
}
}
updateElementText(id, text) {
const el = this.elements.get(id);
if (el) {
el.text = text;
this.draw();
}
}
updateElementPosition(id, x, y) {
const el = this.elements.get(id);
if (el) {
if (x !== null) el.x = x;
if (y !== null) el.y = y;
this.draw();
}
}
updateElementFontSize(id, size) {
const el = this.elements.get(id);
if (el) {
el.fontSize = size;
this.draw();
}
}
updateElementImage(id, imageDataUrl) {
const el = this.elements.get(id);
if (el && el.type === 'image') {
el.imageData = imageDataUrl;
// Create image object
const img = new Image();
img.onload = () => {
el.imageObj = img;
this.draw();
};
img.onerror = () => {
console.error('Failed to load image');
el.imageData = null;
el.imageObj = null;
this.draw();
};
img.src = imageDataUrl;
}
}
deleteElement(id) {
this.elements.delete(id);
if (this.selectedElement?.id === id) {
this.selectedElement = null;
}
this.draw();
}
clearCanvas() {
this.elements.clear();
this.selectedElement = null;
this.draw();
}
getCanvasData() {
return {
elements: Array.from(this.elements.values())
};
}
exportTemplate() {
const data = JSON.stringify(this.getCanvasData(), null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'template.json';
a.click();
URL.revokeObjectURL(url);
}
generatePreview() {
const temp = document.createElement('canvas');
temp.width = this.pageWidth;
temp.height = this.pageHeight;
const tctx = temp.getContext('2d');
tctx.fillStyle = '#ffffff';
tctx.fillRect(0, 0, temp.width, temp.height);
// Draw elements offset by page position
const originalPageX = this.pageX;
const originalPageY = this.pageY;
this.pageX = 0;
this.pageY = 0;
this.elements.forEach(el => {
const ox = el.x, oy = el.y;
el.x = ox - originalPageX;
el.y = oy - originalPageY;
this.drawElement.call({ ctx: tctx, elements: this.elements }, el);
el.x = ox;
el.y = oy;
});
this.pageX = originalPageX;
this.pageY = originalPageY;
const win = window.open();
if (win) {
win.document.write(`<img src="${temp.toDataURL()}" style="max-width:100%">`);
}
}
}
window.invoiceGenerator = new InvoiceGenerator();
})();

View File

@@ -44,9 +44,19 @@ public class DataInitializer implements CommandLineRunner {
adminUser.setCreatedAt(LocalDateTime.now()); adminUser.setCreatedAt(LocalDateTime.now());
adminUser.setUpdatedAt(LocalDateTime.now()); adminUser.setUpdatedAt(LocalDateTime.now());
adminUser.setRoles(Set.of("USER", "ADMIN")); adminUser.setRoles(Set.of("USER", "ADMIN"));
adminUser.setTwoFactorEnabled(false); // 2FA deaktiviert für Admin
userRepository.save(adminUser); userRepository.save(adminUser);
log.info("Created admin user: admin@votianlt.de / admin123"); log.info("Created admin user: admin@votianlt.de / admin123 (2FA enabled)");
} else {
// Stelle sicher, dass bestehender Admin 2FA deaktiviert hat
userRepository.findByEmail("admin@votianlt.de").ifPresent(adminUser -> {
if (adminUser.isTwoFactorEnabled()) {
adminUser.setTwoFactorEnabled(false);
userRepository.save(adminUser);
log.info("Updated admin user: 2FA disabled");
}
});
} }
log.info("Test users initialization completed."); log.info("Test users initialization completed.");

View File

@@ -78,6 +78,8 @@ public final class AdminLayout extends AppLayout {
// Only admin-specific menu items // Only admin-specific menu items
SideNavItem dashboard = new SideNavItem("Dashboard", "admin-dashboard", new Icon(VaadinIcon.DASHBOARD)); SideNavItem dashboard = new SideNavItem("Dashboard", "admin-dashboard", new Icon(VaadinIcon.DASHBOARD));
SideNavItem pdfTest = new SideNavItem("PDF Test", "pdf-test", new Icon(VaadinIcon.FILE_TEXT_O)); SideNavItem pdfTest = new SideNavItem("PDF Test", "pdf-test", new Icon(VaadinIcon.FILE_TEXT_O));
SideNavItem invoiceGenerator = new SideNavItem("Rechnungsgenerator", "invoice-generator",
new Icon(VaadinIcon.FILE_PROCESS));
SideNavItem priceTable = new SideNavItem("Preis-Tabelle", "admin-price-table", new Icon(VaadinIcon.COG)); SideNavItem priceTable = new SideNavItem("Preis-Tabelle", "admin-price-table", new Icon(VaadinIcon.COG));
// SideNavItem systemSettings = new SideNavItem("Systemeinstellungen", // SideNavItem systemSettings = new SideNavItem("Systemeinstellungen",
// "admin-settings", new Icon(VaadinIcon.COG)); // "admin-settings", new Icon(VaadinIcon.COG));
@@ -88,6 +90,7 @@ public final class AdminLayout extends AppLayout {
nav.addItem(dashboard); nav.addItem(dashboard);
nav.addItem(pdfTest); nav.addItem(pdfTest);
nav.addItem(invoiceGenerator);
nav.addItem(priceTable); nav.addItem(priceTable);
// nav.addItem(systemSettings); // nav.addItem(systemSettings);
// nav.addItem(userManagement); // nav.addItem(userManagement);

View File

@@ -0,0 +1,454 @@
package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.ClientCallable;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.page.PendingJavaScriptResult;
import com.vaadin.flow.server.Command;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.upload.Upload;
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.StreamResource;
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
import de.assecutor.votianlt.service.CustomerInvoiceService;
import jakarta.annotation.security.RolesAllowed;
import java.io.ByteArrayInputStream;
@Route(value = "invoice-generator", layout = AdminLayout.class)
@PageTitle("Rechnungsgenerator")
@RolesAllowed("ADMIN")
@JsModule("./invoice-generator/invoice-generator.js")
public class InvoiceGeneratorView extends VerticalLayout {
private final CustomerInvoiceService customerInvoiceService;
private Div canvasContainer;
private VerticalLayout propertiesPanel;
private Div selectedElementInfo;
public InvoiceGeneratorView(CustomerInvoiceService customerInvoiceService) {
this.customerInvoiceService = customerInvoiceService;
setId("invoice-generator-view");
getElement().setAttribute("class", "invoice-generator-view");
setSpacing(false);
setPadding(false);
getStyle().set("margin", "14px");
setWidth("100%");
setHeight("100%");
H2 title = new H2("Rechnungsgenerator");
add(title);
// Hauptlayout: Links (Templates) | Mitte (Canvas) | Rechts (Eigenschaften)
HorizontalLayout mainLayout = new HorizontalLayout();
mainLayout.setWidth("100%");
mainLayout.setHeight("calc(100vh - 150px)");
mainLayout.setSpacing(true);
// Linke Seite: Templates/Bausteine
VerticalLayout leftPanel = createTemplatesPanel();
leftPanel.setWidth("250px");
leftPanel.setHeightFull();
// Mitte: Canvas mit Konva.js
VerticalLayout centerPanel = createCanvasPanel();
centerPanel.setWidth("60%");
centerPanel.setHeightFull();
// Rechte Seite: Eigenschaften
propertiesPanel = createPropertiesPanel();
propertiesPanel.setWidth("300px");
propertiesPanel.setHeightFull();
mainLayout.add(leftPanel, centerPanel, propertiesPanel);
mainLayout.expand(centerPanel);
add(mainLayout);
// Aktions-Buttons unter dem Canvas
HorizontalLayout actionLayout = createActionButtons();
add(actionLayout);
}
@Override
protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);
// Register this view instance and initialize the canvas
getElement().executeJs(
"window.invoiceGeneratorView = this;" +
"if (window.invoiceGenerator) {" +
" console.log('Initializing invoice generator...');" +
" window.invoiceGenerator.init();" +
"} else {" +
" console.error('Invoice generator not found');" +
"}");
}
private VerticalLayout createTemplatesPanel() {
VerticalLayout panel = new VerticalLayout();
panel.setPadding(true);
panel.setSpacing(true);
panel.getStyle()
.set("background-color", "var(--lumo-contrast-5pct)")
.set("border-radius", "var(--lumo-border-radius-m)");
Span header = new Span("Textbausteine");
header.getStyle()
.set("font-weight", "bold")
.set("font-size", "var(--lumo-font-size-l)");
// Draggable Templates
Div textBlock = createDraggableTemplate("Textfeld", VaadinIcon.TEXT_LABEL, "text");
Div headerBlock = createDraggableTemplate("Überschrift", VaadinIcon.HEADER, "header");
Div dateBlock = createDraggableTemplate("Datum", VaadinIcon.CALENDAR, "date");
Div customerBlock = createDraggableTemplate("Kundeninfo", VaadinIcon.USER, "customer");
Div companyBlock = createDraggableTemplate("Firmeninfo", VaadinIcon.OFFICE, "company");
Div amountBlock = createDraggableTemplate("Betrag", VaadinIcon.COIN_PILES, "amount");
Div lineBlock = createDraggableTemplate("Linie", VaadinIcon.LINE_V, "line");
Div imageBlock = createDraggableTemplate("Bild", VaadinIcon.PICTURE, "image");
panel.add(header, textBlock, headerBlock, dateBlock, customerBlock, companyBlock, amountBlock, lineBlock,
imageBlock);
return panel;
}
private Div createDraggableTemplate(String label, VaadinIcon icon, String type) {
Div template = new Div();
template.setText(label);
template.getStyle()
.set("padding", "var(--lumo-space-m)")
.set("margin", "var(--lumo-space-xs) 0")
.set("background-color", "var(--lumo-base-color)")
.set("border", "1px solid var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-m)")
.set("cursor", "grab")
.set("display", "flex")
.set("align-items", "center")
.set("gap", "var(--lumo-space-s)")
.set("user-select", "none");
// Icon hinzufügen
Icon templateIcon = icon.create();
templateIcon.setSize("var(--lumo-icon-size-s)");
template.getElement().insertChild(0, templateIcon.getElement());
// Drag & Drop Attribute setzen
template.getElement().setAttribute("draggable", "true");
template.getElement().setAttribute("data-template-type", type);
template.getElement().setAttribute("data-template-label", label);
// JavaScript Event Listener für Drag Start
template.getElement().executeJs(
"this.addEventListener('dragstart', function(e) {" +
" e.dataTransfer.setData('template-type', this.getAttribute('data-template-type'));" +
" e.dataTransfer.setData('template-label', this.getAttribute('data-template-label'));" +
" this.style.opacity = '0.5';" +
"});" +
"this.addEventListener('dragend', function(e) {" +
" this.style.opacity = '1';" +
"});");
return template;
}
private VerticalLayout createCanvasPanel() {
VerticalLayout panel = new VerticalLayout();
panel.setPadding(false);
panel.setSpacing(false);
panel.setHeightFull();
// Canvas Container für Konva.js
canvasContainer = new Div();
canvasContainer.setId("invoice-canvas-container");
canvasContainer.setWidth("100%");
canvasContainer.setHeight("100%");
canvasContainer.getStyle()
.set("background-color", "#e8e8e8")
.set("border", "2px dashed var(--lumo-contrast-30pct)")
.set("border-radius", "var(--lumo-border-radius-m)")
.set("position", "relative")
.set("overflow", "hidden")
.set("cursor", "default");
// Drop Zone Event Listener
canvasContainer.getElement().executeJs(
"var container = this;" +
"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-30pct)';" +
"});" +
"container.addEventListener('drop', function(e) {" +
" e.preventDefault();" +
" container.style.borderColor = 'var(--lumo-contrast-30pct)';" +
" var templateType = e.dataTransfer.getData('template-type');" +
" var templateLabel = e.dataTransfer.getData('template-label');" +
" if (templateType && window.invoiceGenerator) {" +
" var rect = container.getBoundingClientRect();" +
" var x = e.clientX - rect.left;" +
" var y = e.clientY - rect.top;" +
" window.invoiceGenerator.addElement(templateType, templateLabel, x, y);" +
" }" +
"});");
panel.add(canvasContainer);
panel.expand(canvasContainer);
return panel;
}
private VerticalLayout createPropertiesPanel() {
VerticalLayout panel = new VerticalLayout();
panel.setPadding(true);
panel.setSpacing(true);
panel.getStyle()
.set("background-color", "var(--lumo-contrast-5pct)")
.set("border-radius", "var(--lumo-border-radius-m)");
Span header = new Span("Eigenschaften");
header.getStyle()
.set("font-weight", "bold")
.set("font-size", "var(--lumo-font-size-l)");
// Info-Text wenn kein Element ausgewählt
selectedElementInfo = new Div();
selectedElementInfo.setText("Klicken Sie auf ein Element im Canvas, um dessen Eigenschaften zu bearbeiten.");
selectedElementInfo.getStyle()
.set("color", "var(--lumo-secondary-text-color)")
.set("font-size", "var(--lumo-font-size-s)");
panel.add(header, selectedElementInfo);
return panel;
}
private HorizontalLayout createActionButtons() {
HorizontalLayout layout = new HorizontalLayout();
layout.setWidth("100%");
layout.setJustifyContentMode(JustifyContentMode.END);
layout.setPadding(true);
Button clearButton = new Button("Canvas leeren", new Icon(VaadinIcon.TRASH));
clearButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
clearButton.addClickListener(e -> {
getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.clearCanvas(); }");
showNotification("Canvas wurde geleert");
});
Button previewButton = new Button("Vorschau", new Icon(VaadinIcon.EYE));
previewButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST);
previewButton.addClickListener(e -> {
getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.generatePreview(); }");
});
Button saveTemplateButton = new Button("Template speichern", new Icon(VaadinIcon.DOWNLOAD));
saveTemplateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
saveTemplateButton.addClickListener(e -> {
getElement().executeJs("if (window.invoiceGenerator) { window.invoiceGenerator.exportTemplate(); }");
});
Button generatePdfButton = new Button("PDF generieren", new Icon(VaadinIcon.FILE_TEXT_O));
generatePdfButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
generatePdfButton.addClickListener(e -> generatePdf());
layout.add(clearButton, previewButton, saveTemplateButton, generatePdfButton);
return layout;
}
private void generatePdf() {
try {
// Hole die Canvas-Daten und generiere PDF
getElement().executeJs(
"if (window.invoiceGenerator) { return window.invoiceGenerator.getCanvasData(); } else { return null; }")
.then(json -> {
if (json == null) {
showNotification("Fehler: Canvas-Daten konnten nicht gelesen werden");
return;
}
// Hier würde die PDF-Generierung erfolgen
showNotification("PDF wird generiert... (Demo)");
});
} catch (Exception ex) {
showNotification("Fehler beim Generieren des PDFs: " + ex.getMessage());
}
}
private void showNotification(String message) {
Notification.show(message, 3000, Notification.Position.BOTTOM_CENTER);
}
// Wird von JavaScript aufgerufen wenn ein Element ausgewählt wird
@ClientCallable
public void updatePropertiesPanel(String elementId, String elementType, String text, Double x, Double y,
Integer fontSize, String color) {
getUI().ifPresent(ui -> ui.access(() -> {
propertiesPanel.removeAll();
Span header = new Span("Eigenschaften");
header.getStyle()
.set("font-weight", "bold")
.set("font-size", "var(--lumo-font-size-l)");
// Element Typ Anzeige
Span typeLabel = new Span("Typ: " + elementType);
typeLabel.getStyle().set("font-size", "var(--lumo-font-size-s)");
propertiesPanel.add(header, typeLabel);
// Für Bildelemente: Upload-Button anzeigen
if ("image".equals(elementType)) {
MemoryBuffer buffer = new MemoryBuffer();
Upload upload = new Upload(buffer);
upload.setAcceptedFileTypes("image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp");
upload.setMaxFileSize(5 * 1024 * 1024); // 5 MB
upload.setDropLabel(new Span("Bild hierher ziehen oder klicken"));
upload.setWidthFull();
upload.addSucceededListener(event -> {
try {
// Bild als Base64 kodieren
byte[] bytes = buffer.getInputStream().readAllBytes();
String base64 = java.util.Base64.getEncoder().encodeToString(bytes);
String mimeType = event.getMIMEType();
String dataUrl = "data:" + mimeType + ";base64," + base64;
// An JavaScript übergeben
getElement().executeJs(
"if (window.invoiceGenerator) { window.invoiceGenerator.updateElementImage('"
+ elementId + "', $0); }",
dataUrl);
showNotification("Bild erfolgreich hochgeladen");
} catch (Exception ex) {
showNotification("Fehler beim Hochladen: " + ex.getMessage());
}
});
upload.addFileRejectedListener(event -> {
showNotification("Datei abgelehnt: " + event.getErrorMessage());
});
propertiesPanel.add(upload);
}
// Text Feld (nur für Text-Elemente)
if (!"line".equals(elementType) && !"image".equals(elementType)) {
TextField textField = new TextField("Text");
textField.setValue(text != null ? text : "");
textField.setWidthFull();
textField.addValueChangeListener(e -> {
getElement().executeJs(
"if (window.invoiceGenerator) { window.invoiceGenerator.updateElementText('" + elementId
+ "', $0); }",
e.getValue());
});
propertiesPanel.add(textField);
}
// X Position
TextField xField = new TextField("X Position");
xField.setValue(x != null ? String.valueOf(Math.round(x)) : "0");
xField.setWidthFull();
xField.addValueChangeListener(e -> {
try {
double newX = Double.parseDouble(e.getValue());
getElement().executeJs(
"if (window.invoiceGenerator) { window.invoiceGenerator.updateElementPosition('" + elementId
+ "', $0, null); }",
newX);
} catch (NumberFormatException ignored) {
}
});
propertiesPanel.add(xField);
// Y Position
TextField yField = new TextField("Y Position");
yField.setValue(y != null ? String.valueOf(Math.round(y)) : "0");
yField.setWidthFull();
yField.addValueChangeListener(e -> {
try {
double newY = Double.parseDouble(e.getValue());
getElement().executeJs(
"if (window.invoiceGenerator) { window.invoiceGenerator.updateElementPosition('" + elementId
+ "', null, $0); }",
newY);
} catch (NumberFormatException ignored) {
}
});
propertiesPanel.add(yField);
// Font Size (nur für Text-Elemente)
if (!"line".equals(elementType) && !"image".equals(elementType)) {
TextField fontSizeField = new TextField("Schriftgröße");
fontSizeField.setValue(fontSize != null ? String.valueOf(fontSize) : "16");
fontSizeField.setWidthFull();
fontSizeField.addValueChangeListener(e -> {
try {
int newSize = Integer.parseInt(e.getValue());
getElement().executeJs(
"if (window.invoiceGenerator) { window.invoiceGenerator.updateElementFontSize('"
+ elementId + "', $0); }",
newSize);
} catch (NumberFormatException ignored) {
}
});
propertiesPanel.add(fontSizeField);
}
// Löschen Button
Button deleteButton = new Button("Element löschen", new Icon(VaadinIcon.TRASH));
deleteButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);
deleteButton.setWidthFull();
deleteButton.addClickListener(e -> {
getElement().executeJs(
"if (window.invoiceGenerator) { window.invoiceGenerator.deleteElement('" + elementId
+ "'); }");
resetPropertiesPanel();
});
propertiesPanel.add(deleteButton);
}));
}
/**
* Wird von JavaScript aufgerufen wenn die Auswahl aufgehoben wird
*/
@ClientCallable
public void resetPropertiesPanel() {
getUI().ifPresent(ui -> ui.access(() -> {
propertiesPanel.removeAll();
Span header = new Span("Eigenschaften");
header.getStyle()
.set("font-weight", "bold")
.set("font-size", "var(--lumo-font-size-l)");
selectedElementInfo = new Div();
selectedElementInfo.setText("Klicken Sie auf ein Element im Canvas, um dessen Eigenschaften zu bearbeiten.");
selectedElementInfo.getStyle()
.set("color", "var(--lumo-secondary-text-color)")
.set("font-size", "var(--lumo-font-size-s)");
propertiesPanel.add(header, selectedElementInfo);
}));
}
}

9
vite.config.ts Normal file
View File

@@ -0,0 +1,9 @@
import { UserConfigFn } from 'vite';
import { overrideVaadinConfig } from './vite.generated';
const customConfig: UserConfigFn = (env) => ({
// Here you can add custom Vite parameters
// https://vitejs.dev/config/
});
export default overrideVaadinConfig(customConfig);