Erweiterungen

This commit is contained in:
2025-09-16 11:36:27 +02:00
parent 5d0b59c46f
commit 9d0045e655
6 changed files with 1009 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
package de.assecutor.votianlt.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
@Data
@NoArgsConstructor
public class InvoiceData {
// Invoice details
private String invoiceNumber;
private LocalDate invoiceDate;
private LocalDate dueDate;
// Company information (sender)
private String companyName;
private String companyStreet;
private String companyHouseNumber;
private String companyZip;
private String companyCity;
private String companyPhone;
private String companyEmail;
private String companyWebsite;
// Tax information
private String taxNumber; // Steuernummer
private String vatId; // USt-IdNr
private String commercialRegister; // Handelsregistereintrag
private String managingDirector; // Geschäftsführer
// Bank details
private String bankName;
private String iban;
private String bic;
// Customer information (recipient)
private String customerName;
private String customerStreet;
private String customerHouseNumber;
private String customerZip;
private String customerCity;
private String customerCountry;
// Invoice items
private List<InvoiceItem> items;
// Totals
private BigDecimal subtotal;
private BigDecimal vatAmount;
private BigDecimal totalAmount;
private BigDecimal vatRate = BigDecimal.valueOf(19.0); // 19% standard VAT rate
@Data
@NoArgsConstructor
public static class InvoiceItem {
private String description;
private BigDecimal quantity;
private String unit; // e.g., "Stück", "km", "h"
private BigDecimal unitPrice;
private BigDecimal totalPrice;
private BigDecimal vatRate;
public InvoiceItem(String description, BigDecimal quantity, String unit,
BigDecimal unitPrice, BigDecimal vatRate) {
this.description = description;
this.quantity = quantity;
this.unit = unit;
this.unitPrice = unitPrice;
this.vatRate = vatRate;
this.totalPrice = quantity.multiply(unitPrice);
}
}
}

View File

@@ -0,0 +1,56 @@
package de.assecutor.votianlt.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "pending_mqtt_messages")
public class PendingMqttMessage {
@Id
private ObjectId id;
@Field("topic")
private String topic;
@Field("payload")
private byte[] payload;
@Field("qos")
private int qos;
@Field("retained")
private boolean retained;
@Field("created_at")
private LocalDateTime createdAt;
@Field("retry_count")
private int retryCount = 0;
@Field("last_retry_at")
private LocalDateTime lastRetryAt;
public PendingMqttMessage(String topic, byte[] payload, int qos, boolean retained) {
this.topic = topic;
this.payload = payload;
this.qos = qos;
this.retained = retained;
this.createdAt = LocalDateTime.now();
this.retryCount = 0;
}
public void incrementRetryCount() {
this.retryCount++;
this.lastRetryAt = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,132 @@
package de.assecutor.votianlt.pages.base.ui.view;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.avatar.AvatarVariant;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.menubar.MenuBar;
import com.vaadin.flow.component.menubar.MenuBarVariant;
import com.vaadin.flow.component.orderedlayout.Scroller;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.sidenav.SideNav;
import com.vaadin.flow.component.sidenav.SideNavItem;
import com.vaadin.flow.router.Layout;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import de.assecutor.votianlt.pages.view.EditProfileView;
import de.assecutor.votianlt.security.SecurityService;
import static com.vaadin.flow.theme.lumo.LumoUtility.*;
@AnonymousAllowed
@Layout("admin")
public final class AdminLayout extends AppLayout {
private final SecurityService securityService;
private Div headerRef;
private Scroller navRef;
private Component userMenuRef;
public AdminLayout(SecurityService securityService) {
this.securityService = securityService;
setPrimarySection(Section.DRAWER);
// Always build the drawer; keep references and toggle visibility on attach and
// after navigation
headerRef = createHeader();
navRef = new Scroller(createSideNav());
userMenuRef = createUserMenu();
addToDrawer(headerRef, navRef, userMenuRef);
updateDrawerVisibility();
// Re-check on attach (new UI/session) and on every navigation cycle
addAttachListener(e -> updateDrawerVisibility());
}
private void updateDrawerVisibility() {
boolean loggedIn = securityService.isUserLoggedIn();
if (headerRef != null)
headerRef.setVisible(loggedIn);
if (navRef != null)
navRef.setVisible(loggedIn);
if (userMenuRef != null)
userMenuRef.setVisible(loggedIn);
setDrawerOpened(loggedIn);
}
private Div createHeader() {
var appLogo = VaadinIcon.SHIELD.create();
appLogo.addClassNames(TextColor.PRIMARY, IconSize.LARGE);
var appName = new Span("VotianLT Admin");
appName.addClassNames(FontWeight.SEMIBOLD, FontSize.LARGE);
var header = new Div(appLogo, appName);
header.addClassNames(Display.FLEX, Padding.MEDIUM, Gap.MEDIUM, AlignItems.CENTER);
return header;
}
private Component createSideNav() {
var nav = new SideNav();
nav.addClassNames(Margin.Horizontal.MEDIUM);
// Only admin-specific menu items
SideNavItem dashboard = new SideNavItem("Dashboard", "admin-dashboard", new Icon(VaadinIcon.DASHBOARD));
SideNavItem systemSettings = new SideNavItem("Systemeinstellungen", "admin-settings", new Icon(VaadinIcon.COG));
SideNavItem userManagement = new SideNavItem("Benutzerverwaltung", "admin-users", new Icon(VaadinIcon.USERS));
SideNavItem systemLogs = new SideNavItem("System-Logs", "admin-logs", new Icon(VaadinIcon.FILE_TEXT));
nav.addItem(dashboard);
nav.addItem(systemSettings);
nav.addItem(userManagement);
nav.addItem(systemLogs);
// Create a vertical layout to hold menu items
VerticalLayout navContainer = new VerticalLayout();
navContainer.setPadding(false);
navContainer.setSpacing(false);
navContainer.add(nav);
return navContainer;
}
private Component createUserMenu() {
var userMenu = new MenuBar();
userMenu.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE);
userMenu.addClassNames(Margin.MEDIUM);
// Dynamically updatable components
var avatar = new Avatar();
avatar.addThemeVariants(AvatarVariant.LUMO_XSMALL);
avatar.addClassNames(Margin.Right.SMALL);
avatar.setColorIndex(1); // Different color for admin
var userNameSpan = new Span();
var userMenuItem = userMenu.addItem(avatar);
userMenuItem.add(userNameSpan);
// Profile display with navigation
userMenuItem.getSubMenu().addItem("Profil anzeigen", e -> UI.getCurrent().navigate(EditProfileView.class));
userMenuItem.getSubMenu().addItem("Admin-Einstellungen");
userMenuItem.getSubMenu().addItem("Abmelden", e -> securityService.logout());
// Update function for username and avatar
Runnable updateUserInfo = () -> {
String currentUser = securityService.getCurrentUsername();
avatar.setName(currentUser + " (Admin)");
userNameSpan.setText(currentUser);
};
// Update initially and on attach
updateUserInfo.run();
addAttachListener(e -> updateUserInfo.run());
return userMenu;
}
}

View File

@@ -0,0 +1,357 @@
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.html.Div;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Menu;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
import de.assecutor.votianlt.model.JobStatus;
import de.assecutor.votianlt.repository.*;
import jakarta.annotation.security.RolesAllowed;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.CompletableFuture;
@Route(value = "admin-dashboard", layout = de.assecutor.votianlt.pages.base.ui.view.AdminLayout.class)
@PageTitle("Admin Dashboard")
@RolesAllowed("ADMIN")
@Menu(order = 1, icon = "lumo:edit")
@Slf4j
public class AdminDashboardView extends Main {
private final JobRepository jobRepository;
private final TaskRepository taskRepository;
private final UserRepository userRepository;
private final AppUserRepository appUserRepository;
private final CargoItemRepository cargoItemRepository;
private final PhotoRepository photoRepository;
private final BarcodeRepository barcodeRepository;
private final SignatureRepository signatureRepository;
private final CommentRepository commentRepository;
private final PendingMqttMessageRepository pendingMqttMessageRepository;
private final Div statisticsContainer;
private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
@Autowired
public AdminDashboardView(
JobRepository jobRepository,
TaskRepository taskRepository,
UserRepository userRepository,
AppUserRepository appUserRepository,
CargoItemRepository cargoItemRepository,
PhotoRepository photoRepository,
BarcodeRepository barcodeRepository,
SignatureRepository signatureRepository,
CommentRepository commentRepository,
PendingMqttMessageRepository pendingMqttMessageRepository) {
this.jobRepository = jobRepository;
this.taskRepository = taskRepository;
this.userRepository = userRepository;
this.appUserRepository = appUserRepository;
this.cargoItemRepository = cargoItemRepository;
this.photoRepository = photoRepository;
this.barcodeRepository = barcodeRepository;
this.signatureRepository = signatureRepository;
this.commentRepository = commentRepository;
this.pendingMqttMessageRepository = pendingMqttMessageRepository;
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX,
LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM);
// Header
H1 title = new H1("Administrator Dashboard");
title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, LumoUtility.Margin.Top.NONE);
HorizontalLayout header = new HorizontalLayout(title);
header.setAlignItems(FlexComponent.Alignment.CENTER);
header.setWidthFull();
// Statistics container
statisticsContainer = new Div();
statisticsContainer.setSizeFull();
// Content container
VerticalLayout content = new VerticalLayout(header, statisticsContainer);
content.setSizeFull();
content.setPadding(false);
content.setSpacing(true);
add(content);
// Load initial statistics
loadStatistics();
}
private void loadStatistics() {
log.info("Loading dashboard statistics for admin user");
// Show loading indicator
statisticsContainer.removeAll();
statisticsContainer.add(new Span("Lade Statistiken..."));
// Load statistics asynchronously
CompletableFuture.runAsync(() -> {
getUI().ifPresent(ui -> ui.access(() -> {
try {
displayStatistics();
} catch (Exception e) {
log.error("Error loading dashboard statistics", e);
statisticsContainer.removeAll();
statisticsContainer.add(new Span("Fehler beim Laden der Statistiken: " + e.getMessage()));
}
}));
});
}
private void displayStatistics() {
statisticsContainer.removeAll();
// Create main layout
VerticalLayout mainLayout = new VerticalLayout();
mainLayout.setPadding(false);
mainLayout.setSpacing(true);
// System overview section
mainLayout.add(createSystemOverviewSection());
// Job statistics section
mainLayout.add(createJobStatisticsSection());
// Task statistics section
mainLayout.add(createTaskStatisticsSection());
// User statistics section
mainLayout.add(createUserStatisticsSection());
// System health section
mainLayout.add(createSystemHealthSection());
statisticsContainer.add(mainLayout);
}
private VerticalLayout createSystemOverviewSection() {
VerticalLayout section = new VerticalLayout();
section.setPadding(false);
section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem");
H3 title = new H3("System-Übersicht");
title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
HorizontalLayout cards = new HorizontalLayout();
cards.setWidthFull();
cards.setSpacing(true);
// Total jobs card
long totalJobs = jobRepository.count();
cards.add(createStatCard("Gesamt Jobs", String.valueOf(totalJobs), VaadinIcon.PACKAGE, "blue"));
// Total users card
long totalUsers = userRepository.count();
cards.add(createStatCard("Benutzer", String.valueOf(totalUsers), VaadinIcon.USERS, "green"));
// Total app users card
long totalAppUsers = appUserRepository.count();
cards.add(createStatCard("App-Benutzer", String.valueOf(totalAppUsers), VaadinIcon.MOBILE, "purple"));
// Current time
String currentTime = LocalDateTime.now().format(dateTimeFormatter);
cards.add(createStatCard("Letzte Aktualisierung", currentTime, VaadinIcon.CLOCK, "gray"));
section.add(title, cards);
return section;
}
private VerticalLayout createJobStatisticsSection() {
VerticalLayout section = new VerticalLayout();
section.setPadding(false);
section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem");
H3 title = new H3("Job-Statistiken");
title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
HorizontalLayout cards = new HorizontalLayout();
cards.setWidthFull();
cards.setSpacing(true);
// Jobs by status
try {
long openJobs = jobRepository.countByStatus(JobStatus.CREATED);
long inProgressJobs = jobRepository.countByStatus(JobStatus.IN_PROGRESS);
long completedJobs = jobRepository.countByStatus(JobStatus.COMPLETED);
cards.add(createStatCard("Offene Jobs", String.valueOf(openJobs), VaadinIcon.HOURGLASS_START, "orange"));
cards.add(createStatCard("In Bearbeitung", String.valueOf(inProgressJobs), VaadinIcon.PLAY, "blue"));
cards.add(createStatCard("Abgeschlossen", String.valueOf(completedJobs), VaadinIcon.CHECK_CIRCLE, "green"));
// Total cargo items
long totalCargoItems = cargoItemRepository.count();
cards.add(createStatCard("Frachtgüter", String.valueOf(totalCargoItems), VaadinIcon.CUBE, "purple"));
} catch (Exception e) {
log.warn("Could not load job statistics by status", e);
cards.add(createStatCard("Status-Info", "Nicht verfügbar", VaadinIcon.WARNING, "red"));
}
section.add(title, cards);
return section;
}
private VerticalLayout createTaskStatisticsSection() {
VerticalLayout section = new VerticalLayout();
section.setPadding(false);
section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem");
H3 title = new H3("Aufgaben-Statistiken");
title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
HorizontalLayout cards = new HorizontalLayout();
cards.setWidthFull();
cards.setSpacing(true);
// Total tasks
long totalTasks = taskRepository.count();
cards.add(createStatCard("Gesamt Aufgaben", String.valueOf(totalTasks), VaadinIcon.TASKS, "blue"));
// Completed tasks
long completedTasks = taskRepository.countByCompleted(true);
cards.add(createStatCard("Abgeschlossen", String.valueOf(completedTasks), VaadinIcon.CHECK, "green"));
// Pending tasks
long pendingTasks = totalTasks - completedTasks;
cards.add(createStatCard("Offen", String.valueOf(pendingTasks), VaadinIcon.CLOCK, "orange"));
// Completion rate
double completionRate = totalTasks > 0 ? (completedTasks * 100.0 / totalTasks) : 0;
cards.add(createStatCard("Erfolgsquote", String.format("%.1f%%", completionRate), VaadinIcon.TRENDING_UP, "purple"));
section.add(title, cards);
return section;
}
private VerticalLayout createUserStatisticsSection() {
VerticalLayout section = new VerticalLayout();
section.setPadding(false);
section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem");
H3 title = new H3("Benutzer-Aktivität");
title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
HorizontalLayout cards = new HorizontalLayout();
cards.setWidthFull();
cards.setSpacing(true);
// Content statistics
long totalPhotos = photoRepository.count();
cards.add(createStatCard("Fotos", String.valueOf(totalPhotos), VaadinIcon.CAMERA, "blue"));
long totalBarcodes = barcodeRepository.count();
cards.add(createStatCard("Barcodes", String.valueOf(totalBarcodes), VaadinIcon.BARCODE, "green"));
long totalSignatures = signatureRepository.count();
cards.add(createStatCard("Unterschriften", String.valueOf(totalSignatures), VaadinIcon.EDIT, "purple"));
long totalComments = commentRepository.count();
cards.add(createStatCard("Kommentare", String.valueOf(totalComments), VaadinIcon.COMMENT, "orange"));
section.add(title, cards);
return section;
}
private VerticalLayout createSystemHealthSection() {
VerticalLayout section = new VerticalLayout();
section.setPadding(false);
section.addClassName(LumoUtility.Background.CONTRAST_5);
section.getStyle().set("border-radius", "8px").set("padding", "1rem");
H3 title = new H3("System-Status");
title.addClassName(LumoUtility.Margin.Bottom.MEDIUM);
HorizontalLayout cards = new HorizontalLayout();
cards.setWidthFull();
cards.setSpacing(true);
// Database connection status
try {
userRepository.count(); // Test database connection
cards.add(createStatCard("Datenbank", "Verbunden", VaadinIcon.DATABASE, "green"));
} catch (Exception e) {
cards.add(createStatCard("Datenbank", "Fehler", VaadinIcon.DATABASE, "red"));
}
// Pending MQTT messages
long pendingMqttMessages = pendingMqttMessageRepository.count();
String mqttStatus = pendingMqttMessages == 0 ? "OK" : "Warteschlange: " + pendingMqttMessages;
String mqttColor = pendingMqttMessages == 0 ? "green" : "orange";
cards.add(createStatCard("MQTT", mqttStatus, VaadinIcon.CONNECT, mqttColor));
// System uptime (placeholder)
cards.add(createStatCard("Anwendung", "Läuft", VaadinIcon.HEART, "green"));
// Memory usage (placeholder)
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory() / 1024 / 1024; // MB
long totalMemory = runtime.totalMemory() / 1024 / 1024; // MB
long usedMemory = totalMemory - (runtime.freeMemory() / 1024 / 1024); // MB
String memoryInfo = usedMemory + "/" + maxMemory + " MB";
cards.add(createStatCard("Speicher", memoryInfo, VaadinIcon.SERVER, "blue"));
section.add(title, cards);
return section;
}
private Div createStatCard(String title, String value, VaadinIcon icon, String color) {
Div card = new Div();
card.addClassName(LumoUtility.Background.BASE);
card.getStyle()
.set("border-radius", "8px")
.set("padding", "1rem")
.set("box-shadow", "0 2px 4px rgba(0,0,0,0.1)")
.set("min-width", "200px")
.set("border-left", "4px solid var(--lumo-" + color + "-color, #007bff)");
HorizontalLayout header = new HorizontalLayout();
header.setAlignItems(FlexComponent.Alignment.CENTER);
header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
Icon cardIcon = icon.create();
cardIcon.getStyle().set("color", "var(--lumo-" + color + "-color, #007bff)");
Span titleSpan = new Span(title);
titleSpan.addClassName(LumoUtility.FontSize.SMALL);
titleSpan.getStyle().set("color", "var(--lumo-secondary-text-color)");
header.add(titleSpan, cardIcon);
Span valueSpan = new Span(value);
valueSpan.addClassName(LumoUtility.FontSize.XLARGE);
valueSpan.addClassName(LumoUtility.FontWeight.BOLD);
VerticalLayout content = new VerticalLayout(header, valueSpan);
content.setPadding(false);
content.setSpacing(false);
card.add(content);
return card;
}
}

View File

@@ -0,0 +1,33 @@
package de.assecutor.votianlt.repository;
import de.assecutor.votianlt.model.PendingMqttMessage;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
@Repository
public interface PendingMqttMessageRepository extends MongoRepository<PendingMqttMessage, ObjectId> {
/**
* Find all pending messages ordered by creation time (oldest first)
*/
List<PendingMqttMessage> findAllByOrderByCreatedAtAsc();
/**
* Find messages that haven't been retried for a while (for cleanup)
*/
List<PendingMqttMessage> findByLastRetryAtBeforeOrLastRetryAtIsNull(LocalDateTime before);
/**
* Count pending messages
*/
long count();
/**
* Delete messages older than specified date (for cleanup)
*/
void deleteByCreatedAtBefore(LocalDateTime before);
}

View File

@@ -0,0 +1,354 @@
package de.assecutor.votianlt.service;
import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.borders.Border;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import com.itextpdf.layout.properties.TextAlignment;
import com.itextpdf.layout.properties.UnitValue;
import de.assecutor.votianlt.model.InvoiceData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
@Service
@Slf4j
public class InvoicePdfGenerator {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMANY);
private static final int ITEMS_PER_PAGE = 15; // Maximum items per page before page break
public byte[] generateInvoicePdf(InvoiceData invoiceData) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (PdfWriter writer = new PdfWriter(baos);
PdfDocument pdfDoc = new PdfDocument(writer);
Document document = new Document(pdfDoc, PageSize.A4)) {
// Set document margins
document.setMargins(50, 50, 80, 50); // top, right, bottom, left
PdfFont font = PdfFontFactory.createFont("Helvetica");
PdfFont boldFont = PdfFontFactory.createFont("Helvetica-Bold");
document.setFont(font);
// Calculate totals
calculateTotals(invoiceData);
// Add header with company and customer info
addHeader(document, invoiceData, boldFont, font);
// Add invoice details
addInvoiceDetails(document, invoiceData, boldFont, font);
// Add invoice items (with page breaks if necessary)
addInvoiceItems(document, invoiceData, boldFont, font);
// Add totals
addTotals(document, invoiceData, boldFont, font);
// Add footer on all pages
addFooter(pdfDoc, invoiceData, font);
document.close();
}
log.info("Generated invoice PDF for invoice number: {}", invoiceData.getInvoiceNumber());
return baos.toByteArray();
}
private void calculateTotals(InvoiceData invoiceData) {
BigDecimal subtotal = BigDecimal.ZERO;
BigDecimal vatAmount = BigDecimal.ZERO;
for (InvoiceData.InvoiceItem item : invoiceData.getItems()) {
BigDecimal itemTotal = item.getQuantity().multiply(item.getUnitPrice());
item.setTotalPrice(itemTotal);
subtotal = subtotal.add(itemTotal);
// Calculate VAT for this item
BigDecimal itemVat = itemTotal.multiply(item.getVatRate())
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
vatAmount = vatAmount.add(itemVat);
}
invoiceData.setSubtotal(subtotal);
invoiceData.setVatAmount(vatAmount);
invoiceData.setTotalAmount(subtotal.add(vatAmount));
}
private void addHeader(Document document, InvoiceData invoiceData, PdfFont boldFont, PdfFont font) {
// Create header table with company info (left) and customer info (right)
Table headerTable = new Table(UnitValue.createPercentArray(new float[]{50, 50}))
.setWidth(UnitValue.createPercentValue(100))
.setMarginBottom(20);
// Company information (left side)
Cell companyCell = new Cell()
.setBorder(Border.NO_BORDER)
.setVerticalAlignment(com.itextpdf.layout.properties.VerticalAlignment.TOP);
companyCell.add(new Paragraph(invoiceData.getCompanyName())
.setFont(boldFont).setFontSize(14));
companyCell.add(new Paragraph(invoiceData.getCompanyStreet() + " " +
invoiceData.getCompanyHouseNumber()).setFont(font).setFontSize(10));
companyCell.add(new Paragraph(invoiceData.getCompanyZip() + " " +
invoiceData.getCompanyCity()).setFont(font).setFontSize(10));
if (invoiceData.getCompanyPhone() != null) {
companyCell.add(new Paragraph("Tel: " + invoiceData.getCompanyPhone())
.setFont(font).setFontSize(10));
}
if (invoiceData.getCompanyEmail() != null) {
companyCell.add(new Paragraph("E-Mail: " + invoiceData.getCompanyEmail())
.setFont(font).setFontSize(10));
}
// Customer information (right side)
Cell customerCell = new Cell()
.setBorder(Border.NO_BORDER)
.setVerticalAlignment(com.itextpdf.layout.properties.VerticalAlignment.TOP);
customerCell.add(new Paragraph("Rechnungsempfänger:")
.setFont(boldFont).setFontSize(10));
customerCell.add(new Paragraph(invoiceData.getCustomerName())
.setFont(boldFont).setFontSize(12));
customerCell.add(new Paragraph(invoiceData.getCustomerStreet() + " " +
invoiceData.getCustomerHouseNumber()).setFont(font).setFontSize(10));
customerCell.add(new Paragraph(invoiceData.getCustomerZip() + " " +
invoiceData.getCustomerCity()).setFont(font).setFontSize(10));
if (invoiceData.getCustomerCountry() != null &&
!invoiceData.getCustomerCountry().equalsIgnoreCase("Deutschland")) {
customerCell.add(new Paragraph(invoiceData.getCustomerCountry())
.setFont(font).setFontSize(10));
}
headerTable.addCell(companyCell);
headerTable.addCell(customerCell);
document.add(headerTable);
}
private void addInvoiceDetails(Document document, InvoiceData invoiceData, PdfFont boldFont, PdfFont font) {
// Invoice title
document.add(new Paragraph("RECHNUNG")
.setFont(boldFont)
.setFontSize(20)
.setTextAlignment(TextAlignment.CENTER)
.setMarginTop(20)
.setMarginBottom(20));
// Invoice details table
Table detailsTable = new Table(UnitValue.createPercentArray(new float[]{30, 20, 30, 20}))
.setWidth(UnitValue.createPercentValue(100))
.setMarginBottom(20);
detailsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph("Rechnungsnummer:").setFont(boldFont).setFontSize(10)));
detailsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph(invoiceData.getInvoiceNumber()).setFont(font).setFontSize(10)));
detailsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph("Rechnungsdatum:").setFont(boldFont).setFontSize(10)));
detailsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph(invoiceData.getInvoiceDate().format(DATE_FORMATTER)).setFont(font).setFontSize(10)));
if (invoiceData.getDueDate() != null) {
detailsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph("Fälligkeitsdatum:").setFont(boldFont).setFontSize(10)));
detailsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph(invoiceData.getDueDate().format(DATE_FORMATTER)).setFont(font).setFontSize(10)));
}
document.add(detailsTable);
}
private void addInvoiceItems(Document document, InvoiceData invoiceData, PdfFont boldFont, PdfFont font) {
// Items table header
Table itemsTable = new Table(UnitValue.createPercentArray(new float[]{40, 10, 10, 15, 10, 15}))
.setWidth(UnitValue.createPercentValue(100))
.setMarginBottom(10);
// Header row
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Beschreibung").setFont(boldFont).setFontSize(10)));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Menge").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Einheit").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.CENTER));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Einzelpreis").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("MwSt%").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Gesamtpreis").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
// Add items
int itemCount = 0;
for (InvoiceData.InvoiceItem item : invoiceData.getItems()) {
if (itemCount > 0 && itemCount % ITEMS_PER_PAGE == 0) {
// Add current table and start new page
document.add(itemsTable);
document.add(new com.itextpdf.layout.element.AreaBreak());
// Create new table with header
itemsTable = new Table(UnitValue.createPercentArray(new float[]{40, 10, 10, 15, 10, 15}))
.setWidth(UnitValue.createPercentValue(100))
.setMarginBottom(10);
// Re-add header
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Beschreibung").setFont(boldFont).setFontSize(10)));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Menge").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Einheit").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.CENTER));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Einzelpreis").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("MwSt%").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addHeaderCell(new Cell().setBackgroundColor(ColorConstants.LIGHT_GRAY)
.add(new Paragraph("Gesamtpreis").setFont(boldFont).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
}
itemsTable.addCell(new Cell().add(new Paragraph(item.getDescription()).setFont(font).setFontSize(9)));
itemsTable.addCell(new Cell().add(new Paragraph(formatDecimal(item.getQuantity())).setFont(font).setFontSize(9))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addCell(new Cell().add(new Paragraph(item.getUnit() != null ? item.getUnit() : "").setFont(font).setFontSize(9))
.setTextAlignment(TextAlignment.CENTER));
itemsTable.addCell(new Cell().add(new Paragraph(formatCurrency(item.getUnitPrice())).setFont(font).setFontSize(9))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addCell(new Cell().add(new Paragraph(formatDecimal(item.getVatRate()) + "%").setFont(font).setFontSize(9))
.setTextAlignment(TextAlignment.RIGHT));
itemsTable.addCell(new Cell().add(new Paragraph(formatCurrency(item.getTotalPrice())).setFont(font).setFontSize(9))
.setTextAlignment(TextAlignment.RIGHT));
itemCount++;
}
document.add(itemsTable);
}
private void addTotals(Document document, InvoiceData invoiceData, PdfFont boldFont, PdfFont font) {
// Totals table (right-aligned)
Table totalsTable = new Table(UnitValue.createPercentArray(new float[]{70, 30}))
.setWidth(UnitValue.createPercentValue(100))
.setMarginTop(20);
// Empty cell for spacing
totalsTable.addCell(new Cell().setBorder(Border.NO_BORDER));
// Totals cell
Cell totalsCell = new Cell().setBorder(Border.NO_BORDER);
Table innerTotalsTable = new Table(UnitValue.createPercentArray(new float[]{70, 30}))
.setWidth(UnitValue.createPercentValue(100));
innerTotalsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph("Nettobetrag:").setFont(font).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
innerTotalsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph(formatCurrency(invoiceData.getSubtotal())).setFont(font).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
innerTotalsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph("MwSt (" + formatDecimal(invoiceData.getVatRate()) + "%):").setFont(font).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
innerTotalsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph(formatCurrency(invoiceData.getVatAmount())).setFont(font).setFontSize(10))
.setTextAlignment(TextAlignment.RIGHT));
innerTotalsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph("Rechnungsbetrag:").setFont(boldFont).setFontSize(12))
.setTextAlignment(TextAlignment.RIGHT));
innerTotalsTable.addCell(new Cell().setBorder(Border.NO_BORDER)
.add(new Paragraph(formatCurrency(invoiceData.getTotalAmount())).setFont(boldFont).setFontSize(12))
.setTextAlignment(TextAlignment.RIGHT));
totalsCell.add(innerTotalsTable);
totalsTable.addCell(totalsCell);
document.add(totalsTable);
}
private void addFooter(PdfDocument pdfDoc, InvoiceData invoiceData, PdfFont font) {
int numberOfPages = pdfDoc.getNumberOfPages();
for (int i = 1; i <= numberOfPages; i++) {
com.itextpdf.kernel.pdf.PdfPage page = pdfDoc.getPage(i);
Document footerDoc = new Document(pdfDoc, PageSize.A4);
footerDoc.setFont(font);
// Footer content
Table footerTable = new Table(UnitValue.createPercentArray(new float[]{33, 33, 34}))
.setWidth(UnitValue.createPercentValue(100))
.setFixedPosition(50, 20, 495); // x, y, width
// Bank details
Cell bankCell = new Cell().setBorder(Border.NO_BORDER);
bankCell.add(new Paragraph("Bankverbindung:").setFont(font).setFontSize(8).setBold());
bankCell.add(new Paragraph(invoiceData.getBankName()).setFont(font).setFontSize(7));
bankCell.add(new Paragraph("IBAN: " + invoiceData.getIban()).setFont(font).setFontSize(7));
bankCell.add(new Paragraph("BIC: " + invoiceData.getBic()).setFont(font).setFontSize(7));
// Tax information
Cell taxCell = new Cell().setBorder(Border.NO_BORDER);
taxCell.add(new Paragraph("Steuerliche Angaben:").setFont(font).setFontSize(8).setBold());
if (invoiceData.getTaxNumber() != null) {
taxCell.add(new Paragraph("Steuernr.: " + invoiceData.getTaxNumber()).setFont(font).setFontSize(7));
}
if (invoiceData.getVatId() != null) {
taxCell.add(new Paragraph("USt-IdNr.: " + invoiceData.getVatId()).setFont(font).setFontSize(7));
}
// Company details
Cell companyCell = new Cell().setBorder(Border.NO_BORDER);
if (invoiceData.getCommercialRegister() != null) {
companyCell.add(new Paragraph(invoiceData.getCommercialRegister()).setFont(font).setFontSize(7));
}
if (invoiceData.getManagingDirector() != null) {
companyCell.add(new Paragraph("Geschäftsführer: " + invoiceData.getManagingDirector()).setFont(font).setFontSize(7));
}
footerTable.addCell(bankCell);
footerTable.addCell(taxCell);
footerTable.addCell(companyCell);
footerDoc.add(footerTable);
footerDoc.close();
}
}
private String formatCurrency(BigDecimal amount) {
return String.format(Locale.GERMANY, "%.2f €", amount);
}
private String formatDecimal(BigDecimal amount) {
return String.format(Locale.GERMANY, "%.2f", amount);
}
}