From b430741b5701d165f913f2c97204069d14c40637 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Mon, 22 Sep 2025 10:15:00 +0200 Subject: [PATCH] Erweiterungen --- .../votianlt/pages/view/InvoicesView.java | 104 ++++------- .../votianlt/pages/view/MyInvoicesView.java | 168 +++++++++++++++--- .../service/SystemInvoiceService.java | 9 + 3 files changed, 188 insertions(+), 93 deletions(-) diff --git a/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java b/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java index 5fea7be..c7ebe5b 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java @@ -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 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 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 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); } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java b/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java index 9812212..8177268 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java @@ -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 grid = new Grid<>(MyInvoiceRow.class, false); private final List 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 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 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 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); + } } diff --git a/src/main/java/de/assecutor/votianlt/service/SystemInvoiceService.java b/src/main/java/de/assecutor/votianlt/service/SystemInvoiceService.java index 0e80811..9899a60 100644 --- a/src/main/java/de/assecutor/votianlt/service/SystemInvoiceService.java +++ b/src/main/java/de/assecutor/votianlt/service/SystemInvoiceService.java @@ -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();