Erweiterungen

This commit is contained in:
2025-09-19 14:31:51 +02:00
parent 892ff83124
commit a1b4b3035d
6 changed files with 24 additions and 492 deletions

21
pom.xml
View File

@@ -104,27 +104,6 @@
<version>1.3.30</version>
</dependency>
<!-- iText 7 Core -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>7.2.5</version>
<type>pom</type>
</dependency>
<!-- iText 7 Kernel -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>kernel</artifactId>
<version>7.2.5</version>
</dependency>
<!-- iText 7 Layout -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>layout</artifactId>
<version>7.2.5</version>
</dependency>
<!-- Spring Boot Mail Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -420,32 +420,20 @@ public class EditProfileView extends HorizontalLayout {
}
}
// Einfache PDF-Vorschau generieren (kann später durch echte Logik ersetzt
// werden)
// Einfache Text-Vorschau generieren (PDF-Funktionalität entfernt)
private byte[] generatePreviewPdf() {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
com.lowagie.text.Document document = new com.lowagie.text.Document();
com.lowagie.text.pdf.PdfWriter.getInstance(document, out);
document.open();
document.add(new com.lowagie.text.Paragraph("Rechnungsvorschau"));
document.add(new com.lowagie.text.Paragraph(" "));
document.add(new com.lowagie.text.Paragraph("Rechnungs Präfix: " + safe(prefixField)));
document.add(new com.lowagie.text.Paragraph("USt-IdNr.: " + safe(ustIdField)));
document.add(new com.lowagie.text.Paragraph("Steuernummer: " + safe(taxNumberField)));
document.add(new com.lowagie.text.Paragraph("Bankname: " + safe(bankNameField)));
document.add(new com.lowagie.text.Paragraph("IBAN: " + safe(ibanField)));
document.add(new com.lowagie.text.Paragraph("Steuersatz: " + safe(taxRateField)));
document.add(new com.lowagie.text.Paragraph(" "));
document.add(new com.lowagie.text.Paragraph("Einleitungstext:"));
document.add(new com.lowagie.text.Paragraph(safe(introTextArea)));
document.add(new com.lowagie.text.Paragraph(" "));
document.add(new com.lowagie.text.Paragraph("Zahlungsbedingungen:"));
document.add(new com.lowagie.text.Paragraph(safe(termsTextArea)));
document.close();
return out.toByteArray();
} catch (Exception e) {
throw new RuntimeException("PDF-Generierung fehlgeschlagen: " + e.getMessage(), e);
}
StringBuilder content = new StringBuilder();
content.append("RECHNUNGSVORSCHAU\n\n");
content.append("Rechnungs Präfix: ").append(safe(prefixField)).append("\n");
content.append("USt-IdNr.: ").append(safe(ustIdField)).append("\n");
content.append("Steuernummer: ").append(safe(taxNumberField)).append("\n");
content.append("Bankname: ").append(safe(bankNameField)).append("\n");
content.append("IBAN: ").append(safe(ibanField)).append("\n");
content.append("Steuersatz: ").append(safe(taxRateField)).append("\n\n");
content.append("Einleitungstext:\n").append(safe(introTextArea)).append("\n\n");
content.append("Zahlungsbedingungen:\n").append(safe(termsTextArea)).append("\n");
return content.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
}
// Utility: safe getter für TextField/TextArea

View File

@@ -9,6 +9,7 @@ import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.component.UI;
import de.assecutor.votianlt.model.Invoice;
import de.assecutor.votianlt.service.PdfBoxService;
import jakarta.annotation.security.RolesAllowed;
import java.io.ByteArrayInputStream;
@@ -18,12 +19,14 @@ import java.util.List;
import com.vaadin.flow.server.StreamResource;
import com.vaadin.flow.server.StreamRegistration;
import org.springframework.beans.factory.annotation.Autowired;
@PageTitle("Rechnungen")
@Route(value = "invoices", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
@RolesAllowed({ "USER", "ADMIN" })
public class InvoicesView extends VerticalLayout {
private final Grid<Invoice> invoiceGrid;
public InvoicesView() {

View File

@@ -9,21 +9,19 @@ import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.StreamResource;
import de.assecutor.votianlt.model.InvoiceData;
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
import de.assecutor.votianlt.service.PdfService;
import de.assecutor.votianlt.service.PdfBoxService;
import jakarta.annotation.security.RolesAllowed;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
@Route(value = "pdf-test", layout = AdminLayout.class)
@PageTitle("PDF Test")
@RolesAllowed("ADMIN")
public class PdfTestView extends VerticalLayout {
private final PdfBoxService pdfBoxService;
private final PdfService pdfService;
public PdfTestView(PdfService pdfService) {
this.pdfService = pdfService;
public PdfTestView(PdfBoxService pdfBoxService) {
this.pdfBoxService = pdfBoxService;
setSpacing(false);
setPadding(false);
@@ -36,16 +34,13 @@ public class PdfTestView extends VerticalLayout {
Button generatePdfButton = new Button("Test-Rechnung PDF generieren");
generatePdfButton.addClickListener(e -> generateTestPdf());
Button generateHtmlButton = new Button("HTML-Vorschau generieren");
generateHtmlButton.addClickListener(e -> generateTestHtml());
add(generatePdfButton, generateHtmlButton);
add(generatePdfButton);
}
private void generateTestPdf() {
try {
InvoiceData sampleData = pdfService.createSampleInvoiceData();
byte[] pdfBytes = pdfService.generateInvoicePdf(sampleData);
InvoiceData sampleData = pdfBoxService.createSampleInvoiceData();
byte[] pdfBytes = pdfBoxService.generateInvoicePdf(sampleData);
StreamResource resource = new StreamResource("test-rechnung.pdf",
() -> new ByteArrayInputStream(pdfBytes));
@@ -63,22 +58,5 @@ public class PdfTestView extends VerticalLayout {
}
}
private void generateTestHtml() {
try {
InvoiceData sampleData = pdfService.createSampleInvoiceData();
byte[] htmlBytes = pdfService.generateInvoiceHtml(sampleData);
// Create anchor for download
String htmlContent = new String(htmlBytes, StandardCharsets.UTF_8);
String dataUrl = "data:text/html;charset=utf-8," +
java.net.URLEncoder.encode(htmlContent, StandardCharsets.UTF_8);
getUI().ifPresent(ui -> ui.getPage().open(dataUrl, "_blank"));
Notification.show("HTML-Vorschau erfolgreich generiert!", 3000, Notification.Position.BOTTOM_CENTER);
} catch (Exception ex) {
Notification.show("Fehler beim Generieren der Vorschau: " + ex.getMessage(),
5000, Notification.Position.BOTTOM_CENTER);
}
}
}

View File

@@ -1,146 +0,0 @@
package de.assecutor.votianlt.service;
import de.assecutor.votianlt.model.InvoiceData;
import de.assecutor.votianlt.model.InvoiceItem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@Service
public class PdfService {
@Autowired
private PdfBoxService pdfBoxService;
public byte[] generateInvoicePdf(InvoiceData invoiceData) throws IOException {
// Use PDFBox for actual PDF generation
return pdfBoxService.generateInvoicePdf(invoiceData);
}
public byte[] generateInvoiceHtml(InvoiceData invoiceData) throws IOException {
// Keep HTML generation for preview purposes
String htmlTemplate = loadTemplate();
String processedHtml = processTemplate(htmlTemplate, invoiceData);
return processedHtml.getBytes(StandardCharsets.UTF_8);
}
private String loadTemplate() throws IOException {
ClassPathResource resource = new ClassPathResource("templates/invoice-template.html");
try (InputStream inputStream = resource.getInputStream()) {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
}
private String processTemplate(String template, InvoiceData data) {
String processed = template;
// Replace company information
processed = processed.replace("{{companyName}}", escapeHtml(safeString(data.getCompanyName())));
processed = processed.replace("{{companySubtitle}}", escapeHtml(safeString(data.getCompanySubtitle())));
processed = processed.replace("{{companyStreet}}", escapeHtml(safeString(data.getCompanyStreet())));
processed = processed.replace("{{companyCity}}", escapeHtml(safeString(data.getCompanyCity())));
processed = processed.replace("{{companyPhone}}", escapeHtml(safeString(data.getCompanyPhone())));
processed = processed.replace("{{companyFax}}", escapeHtml(safeString(data.getCompanyFax())));
processed = processed.replace("{{companyEmail}}", escapeHtml(safeString(data.getCompanyEmail())));
processed = processed.replace("{{companyWebsite}}", escapeHtml(safeString(data.getCompanyWebsite())));
// Replace invoice details
processed = processed.replace("{{invoiceNumber}}", escapeHtml(safeString(data.getInvoiceNumber())));
processed = processed.replace("{{invoiceDate}}", escapeHtml(safeString(data.getInvoiceDate())));
processed = processed.replace("{{invoiceText}}", escapeHtml(safeString(data.getInvoiceText())));
// Replace recipient information
processed = processed.replace("{{senderLine}}", escapeHtml(safeString(data.getSenderLine())));
processed = processed.replace("{{recipientName}}", escapeHtml(safeString(data.getRecipientName())));
processed = processed.replace("{{recipientDepartment}}", escapeHtml(safeString(data.getRecipientDepartment())));
processed = processed.replace("{{recipientStreet}}", escapeHtml(safeString(data.getRecipientStreet())));
processed = processed.replace("{{recipientCity}}", escapeHtml(safeString(data.getRecipientCity())));
// Replace financial information
processed = processed.replace("{{netAmount}}", escapeHtml(safeString(data.getNetAmount())));
processed = processed.replace("{{vatRate}}", escapeHtml(safeString(data.getVatRate())));
processed = processed.replace("{{vatAmount}}", escapeHtml(safeString(data.getVatAmount())));
processed = processed.replace("{{totalAmount}}", escapeHtml(safeString(data.getTotalAmount())));
// Replace terms and footer (allow HTML for line breaks)
processed = processed.replace("{{paymentTerms}}", safeString(data.getPaymentTerms()));
processed = processed.replace("{{footerText}}", safeString(data.getFooterText()));
// Process invoice items
processed = processInvoiceItems(processed, data.getInvoiceItems());
return processed;
}
private String processInvoiceItems(String template, List<InvoiceItem> items) {
StringBuilder itemsHtml = new StringBuilder();
if (items != null) {
for (InvoiceItem item : items) {
itemsHtml.append("<tr>")
.append("<td class=\"table-cell table-cell-center\" style=\"width:2cm;\">")
.append(escapeHtml(safeString(item.getQuantity())))
.append("</td>")
.append("<td class=\"table-cell\" style=\"width:9.999cm;\">")
.append(escapeHtml(safeString(item.getDescription())))
.append("</td>")
.append("<td class=\"table-cell table-cell-right\" style=\"width:3cm;\">")
.append(escapeHtml(safeString(item.getUnitPrice())))
.append("</td>")
.append("<td class=\"table-cell table-cell-right\" style=\"width:3cm;\">")
.append(escapeHtml(safeString(item.getTotalPrice())))
.append("</td>")
.append("</tr>");
}
}
// Add empty rows to fill the table (up to 12 rows total as in original)
int emptyRowsNeeded = Math.max(0, 12 - (items != null ? items.size() : 0));
StringBuilder emptyRowsHtml = new StringBuilder();
for (int i = 0; i < emptyRowsNeeded; i++) {
emptyRowsHtml.append("<tr>")
.append("<td class=\"table-cell\" style=\"width:2cm;\">&nbsp;</td>")
.append("<td class=\"table-cell\" style=\"width:9.999cm;\">&nbsp;</td>")
.append("<td class=\"table-cell\" style=\"width:3cm;\">&nbsp;</td>")
.append("<td class=\"table-cell\" style=\"width:3cm;\">&nbsp;</td>")
.append("</tr>");
}
// Remove template placeholders
String result = template.replace("{{#invoiceItems}}", "")
.replace("{{/invoiceItems}}", "")
.replace("{{#emptyRows}}", "")
.replace("{{/emptyRows}}", "");
// Replace the placeholder with actual items
String itemsPlaceholder = "{{invoiceItems}}";
if (result.contains(itemsPlaceholder)) {
result = result.replace(itemsPlaceholder, itemsHtml.toString() + emptyRowsHtml.toString());
}
return result;
}
private String safeString(String value) {
return value != null ? value : "";
}
private String escapeHtml(String value) {
if (value == null) return "";
return value.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
public InvoiceData createSampleInvoiceData() {
return pdfBoxService.createSampleInvoiceData();
}
}

View File

@@ -1,270 +0,0 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="application/xhtml+xml; charset=utf-8" http-equiv="Content-Type"/>
<title>Rechnung {{invoiceNumber}}</title>
<style type="text/css">
@page {
size: A4;
margin: 1.5cm 1cm 1.5cm 2cm;
}
html,
body {
height: 100%;
}
body {
font-family: Arial, sans-serif;
font-size: 12pt;
margin: 0;
padding: 0;
border: 2px solid #1b12b9;
max-width: 21.001cm;
background-color: transparent;
display: flex;
flex-direction: column;
}
.header {
position: absolute;
top: 1.5cm;
right: 0;
}
.company-name {
font-family: Verdana;
font-size: 24pt;
color: #1b12b9;
text-align: center;
margin-left: 14cm;
font-weight: bold;
}
.company-subtitle {
font-family: Verdana;
font-size: 9pt;
color: #1b12b9;
text-align: center;
margin-left: 14cm;
margin-top: 0.2cm;
font-weight: bold;
}
hr {
border: 0;
border-top: 2px solid #1b12b9;
width: 27cm;
position: absolute;
margin-top: 0cm;
left: 0;
}
.recipient-box {
height: 4.5cm;
width: 8.999cm;
padding: 0;
float: left;
left: 0cm;
margin-top: 5.5cm;
}
.sender-line {
font-size: 8pt;
line-height: 100%;
margin-bottom: 0.106cm;
margin-top: 0cm;
font-family: Arial;
}
.recipient-content {
font-size: 12pt;
line-height: 100%;
margin-bottom: 0cm;
margin-top: 0cm;
font-family: Arial;
}
.company-address {
width: 5.701cm;
padding: 0;
float: right;
margin-left: 14.25cm;
text-align: left;
top: 4cm;
min-height: 3cm;
font-size: 10pt;
font-family: Arial;
line-height: 100%;
}
.contact-info {
position: relative;
left: 110px;
margin-top: -270px;
font-size: 8pt;
line-height: 120%;
font-family: Arial;
}
.invoice-date {
float: right;
margin-right: 150px;
font-size: 8pt;
line-height: 120%;
font-family: Arial;
}
.subject {
font-size: 12pt;
line-height: 120%;
margin-bottom: 0.64cm;
margin-top: 100px;
font-family: Arial;
font-weight: bold;
}
.text-body {
font-size: 12pt;
font-family: Arial;
margin-top: 0cm;
margin-bottom: 0.212cm;
line-height: 120%;
}
.invoice-table {
width: 18cm;
margin-top: 0cm;
margin-bottom: 0.635cm;
border-collapse: collapse;
border-spacing: 0;
}
.table-header {
background-color: #e6e6e6;
padding: 0.097cm;
border-left: 0.018cm solid #808080;
font-size: 8pt;
font-family: Arial;
text-align: left;
}
.table-cell {
padding: 0.097cm;
border-left: 0.018cm solid #808080;
vertical-align: top;
font-size: 12pt;
font-family: Arial;
}
.table-cell-right {
text-align: right;
}
.table-cell-center {
text-align: center;
}
.summary-row {
background-color: #e6e6e6;
}
.page-content {
flex: 1 0 auto;
}
.footer-text {
text-align: center;
font-size: 8pt;
font-family: Arial;
margin-top: auto;
padding: 0.5cm 1cm;
}
.clear {
clear: both;
}
</style>
</head>
<body>
<div class="page-content">
<div class="header">
<p class="company-name">
<b>{{companyName}}</b>
</p>
<hr>
<p class="company-subtitle">
<b>{{companySubtitle}}</b>
</p>
</div>
<div class="recipient-box">
<p class="sender-line">{{senderLine}}</p>
<p class="recipient-content">{{recipientName}}</p>
<p class="recipient-content">{{recipientDepartment}}</p>
<p class="recipient-content">{{recipientStreet}}</p>
<p class="recipient-content">{{recipientCity}}</p>
</div>
<div class="company-address">
<p>{{companyStreet}}</p>
<p>{{companyCity}}</p>
<p> </p>
<p>Tel.: {{companyPhone}}</p>
<p>{{companyWebsite}}</p>
</div>
<div class="clear"></div>
<p class="contact-info">
Fax
<span style="margin-left: 60px">eMail</span>
<br/>
{{companyFax}}
<span>{{companyEmail}}</span>
</p>
<p class="invoice-date">
<span>Datum<br></span>
<span>{{invoiceDate}}</span>
</p>
<p class="subject">Rechnung {{invoiceNumber}}</p>
<p class="text-body">{{invoiceText}}</p>
<table class="invoice-table">
<tr>
<td class="table-header" style="width:2cm;">Menge</td>
<td class="table-header" style="width:9.999cm;">Bezeichnung</td>
<td class="table-header" style="width:3cm;">Einzelpreis</td>
<td class="table-header" style="width:3cm;">Gesamt</td>
</tr>
{{invoiceItems}}
<tr>
<td class="table-cell" style="width:2cm;"> </td>
<td class="table-cell" style="width:9.999cm;"> </td>
<td class="table-header" style="width:3cm;">Nettobetrag</td>
<td class="table-cell summary-row table-cell-right" style="width:3cm;">{{netAmount}}</td>
</tr>
<tr>
<td class="table-cell" style="width:2cm;"> </td>
<td class="table-cell" style="width:9.999cm;"> </td>
<td class="table-cell table-cell-right" style="width:3cm;">+ {{vatRate}}% MwSt.</td>
<td class="table-cell table-cell-right" style="width:3cm;">{{vatAmount}}</td>
</tr>
<tr>
<td class="table-cell" style="width:2cm;"> </td>
<td class="table-cell" style="width:9.999cm;"> </td>
<td class="table-header" style="width:3cm;">Endbetrag</td>
<td class="table-cell summary-row table-cell-right" style="width:3cm;">{{totalAmount}}</td>
</tr>
</table>
<p class="text-body">{{paymentTerms}}</p>
</div>
<p class="footer-text">
<span>{{footerText}}</span>
</p>
</body>
</html>