Erweiterungen

This commit is contained in:
2025-09-22 10:15:00 +02:00
parent 57066e22c3
commit b430741b57
3 changed files with 188 additions and 93 deletions

View File

@@ -9,17 +9,18 @@ import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.component.UI;
import de.assecutor.votianlt.model.invoices.SystemInvoice;
import de.assecutor.votianlt.model.invoices.CustomerInvoiceData;
import de.assecutor.votianlt.model.invoices.CustomerInvoiceItem;
import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.service.CustomerInvoiceService;
import de.assecutor.votianlt.model.invoices.SystemInvoiceData;
import de.assecutor.votianlt.model.invoices.SystemInvoiceItem;
import de.assecutor.votianlt.service.SystemInvoiceService;
import jakarta.annotation.security.RolesAllowed;
import java.io.ByteArrayInputStream;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import com.vaadin.flow.server.StreamResource;
import com.vaadin.flow.server.StreamRegistration;
@@ -31,12 +32,10 @@ public class InvoicesView extends VerticalLayout {
private final Grid<SystemInvoice> invoiceGrid;
private final CustomerInvoiceService customerInvoiceService;
private final SecurityService securityService;
private final SystemInvoiceService systemInvoiceService;
public InvoicesView(CustomerInvoiceService customerInvoiceService, SecurityService securityService) {
this.customerInvoiceService = customerInvoiceService;
this.securityService = securityService;
public InvoicesView(SystemInvoiceService systemInvoiceService) {
this.systemInvoiceService = systemInvoiceService;
setSizeFull();
setPadding(true);
@@ -77,8 +76,8 @@ public class InvoicesView extends VerticalLayout {
private void downloadInvoicePdf(SystemInvoice systemInvoice) {
try {
// PDF generieren mit CustomerInvoice (HTML Template)
byte[] pdfBytes = generateCustomerInvoicePdf(systemInvoice);
// PDF generieren mit SystemInvoice (HTML Template)
byte[] pdfBytes = generateSystemInvoicePdf(systemInvoice);
StreamResource resource = new StreamResource(systemInvoice.getId() + ".pdf",
() -> new ByteArrayInputStream(pdfBytes));
resource.setContentType("application/pdf");
@@ -94,72 +93,35 @@ public class InvoicesView extends VerticalLayout {
}
}
private byte[] generateCustomerInvoicePdf(SystemInvoice systemInvoice) throws Exception {
// Aktuellen Benutzer als Rechnungssteller ermitteln
de.assecutor.votianlt.model.User user = securityService.getCurrentDatabaseUser();
private byte[] generateSystemInvoicePdf(SystemInvoice systemInvoice) throws Exception {
DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("dd.MM.yyyy");
NumberFormat CURRENCY_FMT = NumberFormat.getCurrencyInstance(Locale.GERMANY);
CustomerInvoiceData data = new CustomerInvoiceData();
// Kopf
SystemInvoiceData data = new SystemInvoiceData();
data.setInvoiceNumber(systemInvoice.getId());
data.setInvoiceDate(systemInvoice.getDatum());
data.setDeliveryDate(systemInvoice.getDatum());
data.setDescription(systemInvoice.getBeschreibung());
data.setInvoiceDate(DATE_FMT.format(systemInvoice.getDatum()));
data.setInvoiceText(systemInvoice.getBeschreibung());
// Rechnungssteller = eingeloggter Benutzer
String senderName = (nullToEmpty(user.getFirstname()) + " " + nullToEmpty(user.getName())).trim();
if (senderName.isBlank() && user.getEmail() != null)
senderName = user.getEmail();
data.setSenderName(user.getCompany() != null && !user.getCompany().isBlank() ? user.getCompany() : senderName);
data.setSenderAddress((nullToEmpty(user.getStreet()) + " " + nullToEmpty(user.getHouseNumber())).trim());
data.setSenderPostcode(nullToEmpty(user.getZip()));
data.setSenderCity(nullToEmpty(user.getCity()));
data.setSenderCountry("Deutschland");
data.setSenderTaxNumber("");
data.setSenderVatId("");
data.setSenderPhone(nullToEmpty(user.getPhone()));
data.setSenderEmail(nullToEmpty(user.getEmail()));
data.setSenderWebsite("");
// Empfänger = Kunde aus der Zeile (nur Name vorhanden in Testdaten)
data.setRecipientCompany("");
// Empfänger aus der Zeile (nur Name in den Testdaten vorhanden)
data.setRecipientName(systemInvoice.getKunde());
data.setRecipientAddress("");
data.setRecipientPostcode("");
data.setRecipientDepartment("");
data.setRecipientStreet("");
data.setRecipientCity("");
data.setRecipientCountry("Deutschland");
data.setRecipientVatId("");
// Positionen (eine einfache Position aus Betrag/Beschreibung)
List<CustomerInvoiceItem> items = new ArrayList<>();
BigDecimal vatRate = new BigDecimal("0.19");
BigDecimal unitPrice = BigDecimal.valueOf(systemInvoice.getBetrag());
items.add(new CustomerInvoiceItem(BigDecimal.ONE, "Stk.", systemInvoice.getBeschreibung(), unitPrice, vatRate));
data.setItems(items);
// Eine Position mit dem Betrag/Beschreibung
List<SystemInvoiceItem> items = new ArrayList<>();
String netStr = CURRENCY_FMT.format(systemInvoice.getBetrag());
items.add(new SystemInvoiceItem("1", systemInvoice.getBeschreibung(), netStr, netStr));
data.setInvoiceItems(items);
// Summen berechnen
BigDecimal netAmount = items.stream().map(CustomerInvoiceItem::getNetTotal).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal vatAmount = netAmount.multiply(vatRate);
BigDecimal totalAmount = netAmount.add(vatAmount);
data.setNetAmount(netAmount);
data.setVatRate(vatRate);
data.setVatAmount(vatAmount);
data.setTotalAmount(totalAmount);
// Summen berechnen (Betrag als Nettobetrag interpretieren)
double net = systemInvoice.getBetrag();
double vat = Math.round(net * 0.19 * 100.0) / 100.0;
double total = net + vat;
data.setNetAmount(CURRENCY_FMT.format(net));
data.setVatAmount(CURRENCY_FMT.format(vat));
data.setTotalAmount(CURRENCY_FMT.format(total));
// Zahlung
data.setPaymentTerms("Zahlbar innerhalb von 14 Tagen netto ohne Abzug.");
data.setPaymentDueDate(systemInvoice.getDatum().plusDays(14));
data.setBankAccount(data.getSenderName());
data.setIban("");
data.setBic("");
// Rechtliches
data.setLegalNotes("");
data.setReverseChargeNote("");
return customerInvoiceService.generateCustomerInvoicePdf(data);
}
private String nullToEmpty(String s) {
return s == null ? "" : s;
return systemInvoiceService.generateInvoicePdfFromHtml(data);
}
}

View File

@@ -3,6 +3,8 @@ package de.assecutor.votianlt.pages.view;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.GridVariant;
import com.vaadin.flow.component.grid.ColumnTextAlign;
import com.vaadin.flow.component.html.*;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
@@ -10,23 +12,33 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar;
import de.assecutor.votianlt.pages.base.ui.view.MainLayout;
import jakarta.annotation.security.RolesAllowed;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.server.StreamResource;
import com.vaadin.flow.server.StreamRegistration;
import de.assecutor.votianlt.service.SystemInvoiceService;
import de.assecutor.votianlt.model.invoices.SystemInvoiceData;
import de.assecutor.votianlt.model.invoices.SystemInvoiceItem;
import java.io.ByteArrayInputStream;
import java.text.NumberFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Meine Rechnungen nutzerzentrierte Übersicht.
*
* Layout orientiert am bereitgestellten Screenshot: - Zwei Karten oben (Offene
* Rechnungen, Bankverbindung) - Darunter ein Bereich „Rechnungen" mit Grid,
* Suche und Seitengröße
* Modernisierte Optik: Responsive Karten, Lumo-Theme-Varianten, Status-Badges,
* Suche und leere Zustandsanzeige.
*/
@PageTitle("Meine Rechnungen")
@Route(value = "my-invoices", layout = MainLayout.class)
@@ -35,11 +47,18 @@ public class MyInvoicesView extends Main {
private final Grid<MyInvoiceRow> grid = new Grid<>(MyInvoiceRow.class, false);
private final List<MyInvoiceRow> allRows = new ArrayList<>(); // zunächst leer
private final Div emptyState = new Div();
private final SystemInvoiceService systemInvoiceService;
public MyInvoicesView() {
getStyle().set("max-width", "90%");
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("dd.MM.yyyy");
private static final NumberFormat CURRENCY_FMT = NumberFormat.getCurrencyInstance(Locale.GERMANY);
public MyInvoicesView(SystemInvoiceService systemInvoiceService) {
this.systemInvoiceService = systemInvoiceService;
getStyle().set("max-width", "1100px");
getStyle().set("margin-left", "auto");
getStyle().set("margin-right", "auto");
getStyle().set("padding", "var(--lumo-space-m)");
// Toolbar / Titel
add(new ViewToolbar("Meine Rechnungen"));
@@ -49,15 +68,18 @@ public class MyInvoicesView extends Main {
// Rechnungsbereich unten
add(createInvoicesSection());
// Testdaten hinzufügen
addTestInvoices();
}
private Component createTopCards() {
// Container mit zwei Spalten (responsiv)
// Container mit responsiven Spalten
Div container = new Div();
container.getStyle().set("display", "grid").set("grid-template-columns", "48% 2% 48%");
// .set("gap", "10px");
// Spaltenabstände: 2% zwischen den beiden Spalten
container.getStyle().set("column-gap", "0");
container.getStyle()
.set("display", "grid")
.set("grid-template-columns", "repeat(auto-fit, minmax(280px, 1fr))")
.set("gap", "var(--lumo-space-m)");
// Karte: Offene Rechnungen
Paragraph hint = new Paragraph("Momentan sind keine neuen Rechnungen für Sie im System gespeichert.");
@@ -80,7 +102,7 @@ public class MyInvoicesView extends Main {
private Component createInvoicesSection() {
Div card = new Div();
// Abstand zur oberen Zeile
card.getStyle().set("margin-top", "30px");
card.getStyle().set("margin-top", "var(--lumo-space-l)");
styleCard(card);
@@ -93,12 +115,14 @@ public class MyInvoicesView extends Main {
pageSize.setItems(10, 25, 50);
pageSize.setLabel("Einträge anzeigen");
pageSize.setValue(10);
pageSize.setWidth("140px");
pageSize.setWidth("160px");
TextField search = new TextField();
search.setLabel("Suchen");
search.setPlaceholder("Rechnungsnr., Datum, Betrag...");
search.setClearButtonVisible(true);
search.setValueChangeMode(ValueChangeMode.EAGER);
search.setWidth("300px");
// Layout Kopf + Controls
HorizontalLayout header = new HorizontalLayout();
@@ -110,14 +134,36 @@ public class MyInvoicesView extends Main {
HorizontalLayout controls = new HorizontalLayout(pageSize, search);
controls.setWidthFull();
controls.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
controls.setAlignItems(FlexComponent.Alignment.END);
// Grid konfigurieren
grid.addColumn(MyInvoiceRow::status).setHeader("Status").setAutoWidth(true);
grid.addColumn(MyInvoiceRow::invoiceNumber).setHeader("Rechnungsnummer").setAutoWidth(true);
grid.addColumn(MyInvoiceRow::date).setHeader("Datum").setAutoWidth(true);
grid.addColumn(MyInvoiceRow::amount).setHeader("Betrag").setAutoWidth(true);
grid.addThemeVariants(GridVariant.LUMO_ROW_STRIPES, GridVariant.LUMO_COMPACT,
GridVariant.LUMO_WRAP_CELL_CONTENT, GridVariant.LUMO_COLUMN_BORDERS);
grid.setWidthFull();
grid.addColumn(new ComponentRenderer<>(row -> statusBadge(row.status()))).setHeader("Status").setAutoWidth(true)
.setFlexGrow(0);
grid.addColumn(MyInvoicesView::formatInvoiceNumber).setHeader("Rechnungsnummer").setAutoWidth(true);
grid.addColumn(row -> DATE_FMT.format(row.date())).setHeader("Datum").setAutoWidth(true).setFlexGrow(0);
grid.addColumn(row -> CURRENCY_FMT.format(row.amount())).setHeader("Betrag").setAutoWidth(true)
.setTextAlign(ColumnTextAlign.END).setFlexGrow(0);
grid.setAllRowsVisible(true);
grid.setItems(allRows); // zunächst leer
grid.addItemClickListener(event -> {
MyInvoiceRow row = event.getItem();
if (row != null) {
downloadInvoicePdf(row);
}
});
// Leerer Zustand
emptyState.removeAll();
H4 emptyTitle = new H4("Keine Rechnungen vorhanden");
Paragraph emptyDesc = new Paragraph("Sobald Rechnungen vorliegen, erscheinen sie hier.");
emptyState.add(emptyTitle, emptyDesc);
emptyState.getStyle().set("text-align", "center").set("color", "var(--lumo-secondary-text-color)")
.set("padding", "var(--lumo-space-m)").set("border", "1px dashed var(--lumo-contrast-20pct)")
.set("border-radius", "var(--lumo-border-radius-m)");
updateEmptyStateVisibility(allRows);
// Suche (einfacher Text-Filter über alle sichtbaren Felder)
search.addValueChangeListener(e -> applyFilter(e.getValue()));
@@ -132,17 +178,51 @@ public class MyInvoicesView extends Main {
pager.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
// Zusammenbauen
card.add(header, controls, grid, pager);
card.add(header, controls, grid, emptyState, pager);
return card;
}
private void applyFilter(String filter) {
String f = filter == null ? "" : filter.toLowerCase();
grid.setItems(allRows.stream()
.filter(row -> row.status.toLowerCase().contains(f) || row.invoiceNumber.toLowerCase().contains(f)
|| row.date.toString().toLowerCase().contains(f)
|| String.valueOf(row.amount).toLowerCase().contains(f))
.toList());
List<MyInvoiceRow> filtered = allRows.stream()
.filter(row -> row.status().toLowerCase().contains(f) || row.invoiceNumber().toLowerCase().contains(f)
|| row.date().toString().toLowerCase().contains(f)
|| String.valueOf(row.amount()).toLowerCase().contains(f))
.toList();
grid.setItems(filtered);
updateEmptyStateVisibility(filtered);
}
private void updateEmptyStateVisibility(List<MyInvoiceRow> current) {
emptyState.setVisible(current == null || current.isEmpty());
}
private static String formatInvoiceNumber(MyInvoiceRow row) {
return row.invoiceNumber();
}
private Span statusBadge(String status) {
Span badge = new Span(status);
badge.getElement().getThemeList().add("badge pill contrast");
String s = status == null ? "" : status.toLowerCase(Locale.ROOT);
if (s.contains("bezahlt") || s.contains("paid")) {
badge.getElement().getThemeList().add("success");
} else if (s.contains("fällig") || s.contains("überfällig") || s.contains("overdue")) {
badge.getElement().getThemeList().add("error");
} else if (s.contains("offen") || s.contains("open")) {
badge.getElement().getThemeList().add("warning");
}
return badge;
}
private void addTestInvoices() {
// Drei Test-Rechnungen hinzufügen
allRows.clear();
allRows.add(new MyInvoiceRow("Offen", "MI-2025-001", LocalDate.now().minusDays(10), 199.99));
allRows.add(new MyInvoiceRow("Bezahlt", "MI-2025-002", LocalDate.now().minusDays(5), 299.49));
allRows.add(new MyInvoiceRow("Überfällig", "MI-2025-003", LocalDate.now().minusDays(20), 149.00));
grid.setItems(allRows);
updateEmptyStateVisibility(allRows);
}
private Div createCard(String title, Component content) {
@@ -176,4 +256,48 @@ public class MyInvoicesView extends Main {
// Schlanke lokale Repräsentation für das Grid
public record MyInvoiceRow(String status, String invoiceNumber, LocalDate date, double amount) {
}
private void downloadInvoicePdf(MyInvoiceRow row) {
try {
byte[] pdfBytes = generateSystemInvoicePdf(row);
StreamResource resource = new StreamResource(row.invoiceNumber() + ".pdf",
() -> new ByteArrayInputStream(pdfBytes));
resource.setContentType("application/pdf");
resource.setCacheTime(0);
StreamRegistration registration = UI.getCurrent().getSession().getResourceRegistry()
.registerResource(resource);
UI.getCurrent().getPage().open(registration.getResourceUri().toString());
} catch (Exception e) {
// Optional: could log or show a simple notification
}
}
private byte[] generateSystemInvoicePdf(MyInvoiceRow row) throws Exception {
SystemInvoiceData data = new SystemInvoiceData();
data.setInvoiceNumber(row.invoiceNumber());
data.setInvoiceDate(DATE_FMT.format(row.date()));
data.setInvoiceText("Rechnung " + row.invoiceNumber());
// Minimal recipient information
data.setRecipientName("Kunde");
data.setRecipientDepartment("");
data.setRecipientStreet("");
data.setRecipientCity("");
// One simple item based on row
List<SystemInvoiceItem> items = new ArrayList<>();
String netStr = CURRENCY_FMT.format(row.amount());
items.add(new SystemInvoiceItem("1", "Position: " + row.invoiceNumber(), netStr, netStr));
data.setInvoiceItems(items);
double net = row.amount();
double vat = Math.round(net * 0.19 * 100.0) / 100.0;
double total = net + vat;
data.setNetAmount(CURRENCY_FMT.format(net));
data.setVatAmount(CURRENCY_FMT.format(vat));
data.setTotalAmount(CURRENCY_FMT.format(total));
return systemInvoiceService.generateInvoicePdfFromHtml(data);
}
}

View File

@@ -25,6 +25,15 @@ public class SystemInvoiceService {
return generatePdfFromHtmlString(filledHtml);
}
/**
* Generate a SystemInvoice PDF using provided data and the HTML template.
*/
public byte[] generateInvoicePdfFromHtml(SystemInvoiceData data) throws Exception {
String htmlContent = readHtmlTemplate();
String filledHtml = fillHtmlWithInvoiceData(htmlContent, data);
return generatePdfFromHtmlString(filledHtml);
}
public SystemInvoiceData createSampleInvoiceData() {
SystemInvoiceData data = new SystemInvoiceData();