Erweiterungen
This commit is contained in:
77
src/main/java/de/assecutor/votianlt/model/InvoiceData.java
Normal file
77
src/main/java/de/assecutor/votianlt/model/InvoiceData.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user