Erweiterungen

This commit is contained in:
2026-02-18 17:51:06 +01:00
parent 19ac94e0b8
commit 8b6412cb0e
5 changed files with 401 additions and 59 deletions

Binary file not shown.

View File

@@ -239,6 +239,9 @@ window.initProfileInvoiceGenerator = function() {
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.fillText('Bild', x + w / 2, y + h / 2); 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 { } else {
// Text elements // Text elements
var lines = (el.text || '').split('\n'); var lines = (el.text || '').split('\n');
@@ -274,6 +277,169 @@ window.initProfileInvoiceGenerator = function() {
ctx.restore(); 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;
// Calculate actual content height based on table + summary
var tableOnlyHeight = rowHeight * 4; // Header + 3 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);
// Sample data rows (placeholder)
var sampleData = [
{ name: 'Umzugsleistung inkl. Verpackung', vat: '19%', net: '450,00 €' },
{ name: 'Entsorgung Möbel', vat: '19%', net: '85,00 €' },
{ name: 'Montage/De-Montage', vat: '19%', net: '120,00 €' }
];
var currentY = y + rowHeight;
// Draw sample rows
ctx.font = fontSize + 'px Arial';
sampleData.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)
// Calculate totals from sample data
var netTotal = 655.00; // 450 + 85 + 120
var vatRate = 0.19;
var vatTotal = 124.45; // 655 * 0.19
var grossTotal = 779.45; // 655 + 124.45
// 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(netTotal.toFixed(2).replace('.', ',') + ' €', valueX, summaryY + summaryRowHeight / 2);
summaryY += summaryRowHeight;
// Umsatzsteuer - label left, value right
ctx.font = fontSize + 'px Arial';
ctx.textAlign = 'left';
ctx.fillText('zzgl. 19% USt:', labelX, summaryY + summaryRowHeight / 2);
ctx.font = 'bold ' + fontSize + 'px Arial';
ctx.textAlign = 'right';
ctx.fillText(vatTotal.toFixed(2).replace('.', ',') + ' €', 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(grossTotal.toFixed(2).replace('.', ',') + ' €', valueX, summaryY + summaryRowHeight / 2);
}
function drawSelection(el) { function drawSelection(el) {
var x = pageX + (el.x * zoomFactor); var x = pageX + (el.x * zoomFactor);
var y = pageY + (el.y * zoomFactor); var y = pageY + (el.y * zoomFactor);
@@ -285,6 +451,18 @@ window.initProfileInvoiceGenerator = function() {
var fontSize = (el.fontSize || 14) * zoomFactor; var fontSize = (el.fontSize || 14) * zoomFactor;
ctx.font = (el.fontStyle || '') + ' ' + fontSize + 'px Arial'; 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 lines = (el.text || '').split('\n');
var maxLineWidth = 0; var maxLineWidth = 0;
lines.forEach(function(line) { lines.forEach(function(line) {
@@ -300,6 +478,7 @@ window.initProfileInvoiceGenerator = function() {
var textHeight = lines.length * lineHeight; var textHeight = lines.length * lineHeight;
h = Math.max(h, textHeight + (6 * zoomFactor)); h = Math.max(h, textHeight + (6 * zoomFactor));
} }
}
ctx.strokeStyle = '#1976d2'; ctx.strokeStyle = '#1976d2';
ctx.lineWidth = Math.max(1, 2 * zoomFactor); ctx.lineWidth = Math.max(1, 2 * zoomFactor);
@@ -307,8 +486,8 @@ window.initProfileInvoiceGenerator = function() {
ctx.strokeRect(x, y, w, h); ctx.strokeRect(x, y, w, h);
ctx.setLineDash([]); ctx.setLineDash([]);
// Don't show resize handles for static elements // Don't show resize handles for static elements (except services.list)
if (el.isStatic) { if (el.isStatic && el.variable !== 'services.list') {
return; return;
} }
@@ -346,6 +525,17 @@ window.initProfileInvoiceGenerator = function() {
var fontSize = (el.fontSize || 14) * zoomFactor; var fontSize = (el.fontSize || 14) * zoomFactor;
ctx.font = (el.fontStyle || '') + ' ' + fontSize + 'px Arial'; 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 lines = (el.text || '').split('\n');
var maxLineWidth = 0; var maxLineWidth = 0;
lines.forEach(function(line) { lines.forEach(function(line) {
@@ -359,6 +549,7 @@ window.initProfileInvoiceGenerator = function() {
var textHeight = lines.length * lineHeight; var textHeight = lines.length * lineHeight;
eh = Math.max(eh, textHeight + (6 * zoomFactor)); eh = Math.max(eh, textHeight + (6 * zoomFactor));
} }
}
return x >= ex && x <= ex + ew && y >= ey && y <= ey + eh; return x >= ex && x <= ex + ew && y >= ey && y <= ey + eh;
} }
@@ -370,12 +561,28 @@ window.initProfileInvoiceGenerator = function() {
// Helper function to check if a point is on a resize handle // Helper function to check if a point is on a resize handle
function getResizeHandle(x, y, el) { function getResizeHandle(x, y, el) {
if (!el || el.isStatic) return -1; 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 ex = pageX + (el.x * zoomFactor);
var ey = pageY + (el.y * zoomFactor); var ey = pageY + (el.y * zoomFactor);
var ew = (el.width || 100) * zoomFactor; var ew = (el.width || 100) * zoomFactor;
var eh = (el.height || 30) * 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 hs = Math.max(6, 8 * zoomFactor);
var halfHs = hs / 2; var halfHs = hs / 2;
@@ -657,7 +864,21 @@ window.initProfileInvoiceGenerator = function() {
el.text = staticText; el.text = staticText;
el.color = '#000000'; // Black text for static elements el.color = '#000000'; // Black text for static elements
el.fontStyle = 'bold'; 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); el.height = calculateHeight(el.fontSize, 1);
}
} else { } else {
switch (type) { switch (type) {
case 'text': case 'text':

View File

@@ -2,7 +2,7 @@ package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H2; import com.vaadin.flow.component.html.H2;
@@ -11,7 +11,7 @@ import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.IntegerField;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.BeforeEvent; import com.vaadin.flow.router.BeforeEvent;
@@ -39,8 +39,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
private final JobRepository jobRepository; private final JobRepository jobRepository;
private final ServiceRepository serviceRepository; private final ServiceRepository serviceRepository;
private final SecurityService securityService;
private Job currentJob; private Job currentJob;
private List<ServiceRow> gridRows = new ArrayList<>(); private List<ServiceRow> gridRows = new ArrayList<>();
private Grid<ServiceRow> servicesGrid; private Grid<ServiceRow> servicesGrid;
@@ -78,8 +76,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
SecurityService securityService) { SecurityService securityService) {
this.jobRepository = jobRepository; this.jobRepository = jobRepository;
this.serviceRepository = serviceRepository; this.serviceRepository = serviceRepository;
this.securityService = securityService;
setSizeFull(); setSizeFull();
setPadding(true); setPadding(true);
setSpacing(true); setSpacing(true);
@@ -256,12 +252,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
return servicesSection; return servicesSection;
} }
private void refreshServicesGrid() {
if (servicesGrid != null) {
servicesGrid.getDataProvider().refreshAll();
}
}
private List<Service> getSelectedServices() { private List<Service> getSelectedServices() {
return gridRows.stream().filter(row -> row.getService() != null).map(ServiceRow::getService).toList(); return gridRows.stream().filter(row -> row.getService() != null).map(ServiceRow::getService).toList();
} }
@@ -365,17 +355,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
return new BigDecimal("0.19"); // Default 19% VAT return new BigDecimal("0.19"); // Default 19% VAT
} }
private void updateSummarySection() {
// Refresh the services grid to update calculated prices
refreshServicesGrid();
// Recreate the summary section to update the values
int summarySectionIndex = getComponentCount() - 2; // Summary section is second to last
Div newSummarySection = createSummarySection();
remove(getComponentAt(summarySectionIndex)); // Remove old summary section
addComponentAtIndex(summarySectionIndex, newSummarySection); // Add new summary section
}
private String extractCompanyName(String customerSelection) { private String extractCompanyName(String customerSelection) {
if (customerSelection == null || customerSelection.isBlank()) { if (customerSelection == null || customerSelection.isBlank()) {
return ""; return "";

View File

@@ -863,8 +863,8 @@ public class EditProfileView extends HorizontalLayout {
panel.setSpacing(true); panel.setSpacing(true);
panel.setHeightFull(); panel.setHeightFull();
// Bereich 1: Meine Stammdaten (Variablen) // Bereich 1: Meine Stammdaten
Span invoiceHeader = new Span("Meine Stammdaten (Variablen)"); Span invoiceHeader = new Span("Meine Stammdaten");
invoiceHeader.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-m)") invoiceHeader.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-m)")
.set("margin-top", "var(--lumo-space-s)"); .set("margin-top", "var(--lumo-space-s)");
@@ -889,8 +889,23 @@ public class EditProfileView extends HorizontalLayout {
Div senderPhone = createVariableTemplate("Telefon", VaadinIcon.PHONE, "masterdata.phone", Div senderPhone = createVariableTemplate("Telefon", VaadinIcon.PHONE, "masterdata.phone",
phone.isEmpty() ? "Ihre Telefonnummer" : phone); phone.isEmpty() ? "Ihre Telefonnummer" : phone);
// Bereich 2: Kundendaten (Variablen) // Bereich 2: Leistungen
Span customerHeader = new Span("Kundendaten (Variablen)"); Span servicesHeader = new Span("Leistungen");
servicesHeader.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-m)")
.set("margin-top", "var(--lumo-space-m)");
// Leistungen als draggable Variable
Div servicesListBlock = createServicesVariableTemplate("Leistungen auflisten", VaadinIcon.LIST, "services.list",
"Artikel 1: 100,00 €\nArtikel 2: 50,00 €");
Div servicesNetBlock = createServicesVariableTemplate("Nettosumme", VaadinIcon.COIN_PILES, "services.net_total",
"150,00 €");
Div servicesVatBlock = createServicesVariableTemplate("Umsatzsteuer", VaadinIcon.COIN_PILES,
"services.vat_total", "28,50 €");
Div servicesGrossBlock = createServicesVariableTemplate("Bruttosumme", VaadinIcon.MONEY, "services.gross_total",
"178,50 €");
// Bereich 3: Kundendaten
Span customerHeader = new Span("Kundendaten");
customerHeader.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-m)") customerHeader.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-m)")
.set("margin-top", "var(--lumo-space-m)"); .set("margin-top", "var(--lumo-space-m)");
@@ -924,6 +939,7 @@ public class EditProfileView extends HorizontalLayout {
Div imageBlock = createDraggableTemplate("Bild", VaadinIcon.PICTURE, "image"); Div imageBlock = createDraggableTemplate("Bild", VaadinIcon.PICTURE, "image");
panel.add(invoiceHeader, senderCompany, senderName, senderAddress, senderCity, senderEmail, senderPhone, panel.add(invoiceHeader, senderCompany, senderName, senderAddress, senderCity, senderEmail, senderPhone,
servicesHeader, servicesListBlock, servicesNetBlock, servicesVatBlock, servicesGrossBlock,
customerHeader, customerCompany, customerName, customerAddress, customerCity, customerEmail, customerHeader, customerCompany, customerName, customerAddress, customerCity, customerEmail,
customerPhone, freeHeader, textBlock, headerBlock, dateBlock, customerBlock, companyBlock, amountBlock, customerPhone, freeHeader, textBlock, headerBlock, dateBlock, customerBlock, companyBlock, amountBlock,
lineBlock, imageBlock); lineBlock, imageBlock);
@@ -1000,6 +1016,43 @@ public class EditProfileView extends HorizontalLayout {
return template; return template;
} }
private Div createServicesVariableTemplate(String label, VaadinIcon icon, String variable, String defaultText) {
Div template = new Div();
template.setText(label);
template.getStyle().set("padding", "var(--lumo-space-s)").set("margin", "var(--lumo-space-xs) 0")
.set("background-color", "rgba(255, 193, 7, 0.15)") // Amber/gelb für Leistungen
.set("border", "1px solid rgba(255, 193, 7, 0.4)").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")
.set("font-size", "var(--lumo-font-size-s)").set("color", "#f57c00"); // Dunkles Orange
Icon templateIcon = icon.create();
templateIcon.setSize("var(--lumo-icon-size-s)");
template.getElement().insertChild(0, templateIcon.getElement());
template.getElement().setAttribute("draggable", "true");
template.getElement().setAttribute("data-template-type", "variable");
template.getElement().setAttribute("data-template-label", label);
template.getElement().setAttribute("data-variable", variable);
template.getElement().setAttribute("data-static-text", defaultText);
template.getElement().setAttribute("data-is-customer", "false");
template.getElement().setAttribute("data-is-services", "true");
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'));"
+ " e.dataTransfer.setData('variable', this.getAttribute('data-variable'));"
+ " e.dataTransfer.setData('static-text', this.getAttribute('data-static-text'));"
+ " e.dataTransfer.setData('is-static', 'true');"
+ " e.dataTransfer.setData('is-customer', this.getAttribute('data-is-customer'));"
+ " e.dataTransfer.setData('is-services', this.getAttribute('data-is-services'));"
+ " this.style.opacity = '0.5';" + "});" + "this.addEventListener('dragend', function(e) {"
+ " this.style.opacity = '1';" + "});");
return template;
}
private Div createDraggableTemplate(String label, VaadinIcon icon, String type) { private Div createDraggableTemplate(String label, VaadinIcon icon, String type) {
Div template = new Div(); Div template = new Div();
template.setText(label); template.setText(label);
@@ -1328,6 +1381,10 @@ public class EditProfileView extends HorizontalLayout {
case "customer.city" -> "PLZ und Ort des Kunden"; case "customer.city" -> "PLZ und Ort des Kunden";
case "customer.email" -> "E-Mail des Kunden"; case "customer.email" -> "E-Mail des Kunden";
case "customer.phone" -> "Telefon des Kunden"; case "customer.phone" -> "Telefon des Kunden";
case "services.list" -> "Liste aller Leistungen auf der Rechnung";
case "services.net_total" -> "Nettosumme aller Leistungen";
case "services.vat_total" -> "Umsatzsteuer aller Leistungen";
case "services.gross_total" -> "Bruttosumme aller Leistungen";
default -> "Variable: " + variable; default -> "Variable: " + variable;
}; };
} }

View File

@@ -372,8 +372,13 @@ public class CustomerInvoiceService {
if (fontStyle.contains("bold")) if (fontStyle.contains("bold"))
htmlBuilder.append("font-weight:bold;"); htmlBuilder.append("font-weight:bold;");
} }
// Vertically center content // For services.list use block display to allow table to fill width
if ("services.list".equals(variable)) {
htmlBuilder.append("display:block;overflow:visible;padding:0;");
} else {
// Vertically center content for other elements
htmlBuilder.append("display:flex;align-items:center;"); htmlBuilder.append("display:flex;align-items:center;");
}
htmlBuilder.append("'"); htmlBuilder.append("'");
htmlBuilder.append(">"); htmlBuilder.append(">");
@@ -425,6 +430,9 @@ public class CustomerInvoiceService {
htmlBuilder.append( htmlBuilder.append(
"<div style='width:100%;height:100%;background:#f0f0f0;display:flex;align-items:center;justify-content:center;font-size:10pt;color:#666;'>[Bild]</div>"); "<div style='width:100%;height:100%;background:#f0f0f0;display:flex;align-items:center;justify-content:center;font-size:10pt;color:#666;'>[Bild]</div>");
} }
} else if ("services.list".equals(variable)) {
// Render services list as a table
htmlBuilder.append(generateServicesTableHtml(mmWidth));
} else { } else {
// Wrap text in a span to prevent flexbox issues // Wrap text in a span to prevent flexbox issues
htmlBuilder.append("<span style='white-space:nowrap;'>").append(text).append("</span>"); htmlBuilder.append("<span style='white-space:nowrap;'>").append(text).append("</span>");
@@ -443,4 +451,81 @@ public class CustomerInvoiceService {
private String safe(String value) { private String safe(String value) {
return value != null ? value : ""; return value != null ? value : "";
} }
/**
* Generate HTML table for services list with summary section below.
*/
private String generateServicesTableHtml(double widthMm) {
StringBuilder html = new StringBuilder();
// Sample data for preview (will be replaced with actual job data later)
String[][] sampleData = {
{"Umzugsleistung inkl. Verpackung", "19%", "450,00 €"},
{"Entsorgung Möbel", "19%", "85,00 €"},
{"Montage/De-Montage", "19%", "120,00 €"}
};
// Calculate totals
double netTotal = 655.00;
double vatTotal = 124.45;
double grossTotal = 779.45;
// Wrapper div
html.append("<div style='width:100%;box-sizing:border-box;'>");
// Table
html.append("<table style='width:100%;border-collapse:collapse;font-size:inherit;table-layout:fixed;'>");
// Header row
html.append("<tr style='background-color:#f5f5f5;border-bottom:1px solid #cccccc;'>");
html.append("<th style='text-align:left;padding:4px 8px;font-weight:bold;width:55%;white-space:nowrap;'>Name</th>");
html.append("<th style='text-align:right;padding:4px 8px;font-weight:bold;width:20%;white-space:nowrap;'>Steuersatz</th>");
html.append("<th style='text-align:right;padding:4px 8px;font-weight:bold;width:25%;white-space:nowrap;'>Nettobetrag</th>");
html.append("</tr>");
// Data rows
for (int i = 0; i < sampleData.length; i++) {
String bgColor = (i % 2 == 1) ? "background-color:rgba(0,0,0,0.02);" : "";
html.append("<tr style='").append(bgColor).append("border-bottom:1px solid #eeeeee;'>");
html.append("<td style='text-align:left;padding:4px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'>").append(sampleData[i][0]).append("</td>");
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(sampleData[i][1]).append("</td>");
html.append("<td style='text-align:right;padding:4px 8px;white-space:nowrap;'>").append(sampleData[i][2]).append("</td>");
html.append("</tr>");
}
html.append("</table>");
// Summary section
html.append("<div style='margin-top:8px;width:100%;'>");
// Summary table for alignment with proper column separation
html.append("<table style='width:100%;border-collapse:collapse;font-size:inherit;table-layout:fixed;'>");
// Nettosumme - label in col 2, value in col 3
html.append("<tr>");
html.append("<td style='width:55%;padding:2px 0;'></td>");
html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>Nettosumme:</td>");
html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>").append(String.format(java.util.Locale.GERMANY, "%,.2f €", netTotal)).append("</td>");
html.append("</tr>");
// Umsatzsteuer - label in col 2, value in col 3
html.append("<tr>");
html.append("<td style='width:55%;padding:2px 0;'></td>");
html.append("<td style='width:20%;text-align:left;padding:2px 8px;white-space:nowrap;'>zzgl. 19% USt:</td>");
html.append("<td style='width:25%;text-align:right;padding:2px 8px;white-space:nowrap;font-weight:bold;'>").append(String.format(java.util.Locale.GERMANY, "%,.2f €", vatTotal)).append("</td>");
html.append("</tr>");
// Gesamtsumme - label in col 2, value in col 3
html.append("<tr>");
html.append("<td style='width:55%;padding:2px 0;'></td>");
html.append("<td style='width:20%;text-align:left;padding:4px 8px;white-space:nowrap;font-weight:bold;font-size:1.05em;'>Gesamtsumme:</td>");
html.append("<td style='width:25%;text-align:right;padding:4px 8px;white-space:nowrap;font-weight:bold;font-size:1.05em;'>").append(String.format(java.util.Locale.GERMANY, "%,.2f €", grossTotal)).append("</td>");
html.append("</tr>");
html.append("</table>");
html.append("</div>");
html.append("</div>");
return html.toString();
}
} }