diff --git a/src/main/bundles/dev.bundle b/src/main/bundles/dev.bundle index 44e56d7..fbb4a31 100644 Binary files a/src/main/bundles/dev.bundle and b/src/main/bundles/dev.bundle differ diff --git a/src/main/frontend/invoice-generator/profile-invoice-generator.js b/src/main/frontend/invoice-generator/profile-invoice-generator.js index 3f18533..d80570f 100644 --- a/src/main/frontend/invoice-generator/profile-invoice-generator.js +++ b/src/main/frontend/invoice-generator/profile-invoice-generator.js @@ -239,6 +239,9 @@ window.initProfileInvoiceGenerator = function() { 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'); @@ -274,6 +277,169 @@ window.initProfileInvoiceGenerator = function() { 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) { var x = pageX + (el.x * zoomFactor); var y = pageY + (el.y * zoomFactor); @@ -285,20 +451,33 @@ window.initProfileInvoiceGenerator = function() { var fontSize = (el.fontSize || 14) * zoomFactor; ctx.font = (el.fontStyle || '') + ' ' + fontSize + 'px Arial'; - 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)); + // 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'; @@ -307,8 +486,8 @@ window.initProfileInvoiceGenerator = function() { ctx.strokeRect(x, y, w, h); ctx.setLineDash([]); - // Don't show resize handles for static elements - if (el.isStatic) { + // Don't show resize handles for static elements (except services.list) + if (el.isStatic && el.variable !== 'services.list') { return; } @@ -346,18 +525,30 @@ window.initProfileInvoiceGenerator = function() { var fontSize = (el.fontSize || 14) * zoomFactor; ctx.font = (el.fontStyle || '') + ' ' + fontSize + 'px Arial'; - 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)); + // 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; @@ -370,12 +561,28 @@ window.initProfileInvoiceGenerator = function() { // Helper function to check if a point is on a resize handle 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 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; @@ -657,7 +864,21 @@ window.initProfileInvoiceGenerator = function() { el.text = staticText; el.color = '#000000'; // Black text for static elements el.fontStyle = 'bold'; - el.height = calculateHeight(el.fontSize, 1); + // 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': diff --git a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java index 2725883..b3d2221 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java @@ -2,7 +2,7 @@ package de.assecutor.votianlt.pages.view; import com.vaadin.flow.component.button.Button; 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.html.Div; 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.orderedlayout.HorizontalLayout; 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.Route; import com.vaadin.flow.router.BeforeEvent; @@ -39,8 +39,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter private final JobRepository jobRepository; private final ServiceRepository serviceRepository; - private final SecurityService securityService; - private Job currentJob; private List gridRows = new ArrayList<>(); private Grid servicesGrid; @@ -78,8 +76,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter SecurityService securityService) { this.jobRepository = jobRepository; this.serviceRepository = serviceRepository; - this.securityService = securityService; - setSizeFull(); setPadding(true); setSpacing(true); @@ -256,12 +252,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter return servicesSection; } - private void refreshServicesGrid() { - if (servicesGrid != null) { - servicesGrid.getDataProvider().refreshAll(); - } - } - private List getSelectedServices() { 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 } - 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) { if (customerSelection == null || customerSelection.isBlank()) { return ""; diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java index edc554d..a39cc74 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditProfileView.java @@ -863,8 +863,8 @@ public class EditProfileView extends HorizontalLayout { panel.setSpacing(true); panel.setHeightFull(); - // Bereich 1: Meine Stammdaten (Variablen) - Span invoiceHeader = new Span("Meine Stammdaten (Variablen)"); + // Bereich 1: Meine Stammdaten + Span invoiceHeader = new Span("Meine Stammdaten"); invoiceHeader.getStyle().set("font-weight", "bold").set("font-size", "var(--lumo-font-size-m)") .set("margin-top", "var(--lumo-space-s)"); @@ -889,8 +889,23 @@ public class EditProfileView extends HorizontalLayout { Div senderPhone = createVariableTemplate("Telefon", VaadinIcon.PHONE, "masterdata.phone", phone.isEmpty() ? "Ihre Telefonnummer" : phone); - // Bereich 2: Kundendaten (Variablen) - Span customerHeader = new Span("Kundendaten (Variablen)"); + // Bereich 2: Leistungen + 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)") .set("margin-top", "var(--lumo-space-m)"); @@ -924,6 +939,7 @@ public class EditProfileView extends HorizontalLayout { Div imageBlock = createDraggableTemplate("Bild", VaadinIcon.PICTURE, "image"); panel.add(invoiceHeader, senderCompany, senderName, senderAddress, senderCity, senderEmail, senderPhone, + servicesHeader, servicesListBlock, servicesNetBlock, servicesVatBlock, servicesGrossBlock, customerHeader, customerCompany, customerName, customerAddress, customerCity, customerEmail, customerPhone, freeHeader, textBlock, headerBlock, dateBlock, customerBlock, companyBlock, amountBlock, lineBlock, imageBlock); @@ -1000,6 +1016,43 @@ public class EditProfileView extends HorizontalLayout { 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) { Div template = new Div(); template.setText(label); @@ -1328,6 +1381,10 @@ public class EditProfileView extends HorizontalLayout { case "customer.city" -> "PLZ und Ort des Kunden"; case "customer.email" -> "E-Mail 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; }; } diff --git a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java index f20fa0e..af856da 100644 --- a/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java +++ b/src/main/java/de/assecutor/votianlt/service/CustomerInvoiceService.java @@ -372,8 +372,13 @@ public class CustomerInvoiceService { if (fontStyle.contains("bold")) htmlBuilder.append("font-weight:bold;"); } - // Vertically center content - htmlBuilder.append("display:flex;align-items:center;"); + // 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("'"); htmlBuilder.append(">"); @@ -425,6 +430,9 @@ public class CustomerInvoiceService { htmlBuilder.append( "
[Bild]
"); } + } else if ("services.list".equals(variable)) { + // Render services list as a table + htmlBuilder.append(generateServicesTableHtml(mmWidth)); } else { // Wrap text in a span to prevent flexbox issues htmlBuilder.append("").append(text).append(""); @@ -443,4 +451,81 @@ public class CustomerInvoiceService { private String safe(String 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("
"); + + // Table + html.append(""); + + // Header row + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + + // 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(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + } + + html.append("
NameSteuersatzNettobetrag
").append(sampleData[i][0]).append("").append(sampleData[i][1]).append("").append(sampleData[i][2]).append("
"); + + // Summary section + html.append("
"); + + // Summary table for alignment with proper column separation + html.append(""); + + // Nettosumme - label in col 2, value in col 3 + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + + // Umsatzsteuer - label in col 2, value in col 3 + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + + // Gesamtsumme - label in col 2, value in col 3 + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + + html.append("
Nettosumme:").append(String.format(java.util.Locale.GERMANY, "%,.2f €", netTotal)).append("
zzgl. 19% USt:").append(String.format(java.util.Locale.GERMANY, "%,.2f €", vatTotal)).append("
Gesamtsumme:").append(String.format(java.util.Locale.GERMANY, "%,.2f €", grossTotal)).append("
"); + html.append("
"); + html.append("
"); + + return html.toString(); + } }