Erweiterungen

This commit is contained in:
2026-02-26 11:03:29 +01:00
parent 7da7c71315
commit 98230670ca
5 changed files with 125 additions and 40 deletions

View File

@@ -63,6 +63,9 @@ public class CustomerInvoice {
private String jobId; // Referenz auf den Auftrag
private String userId; // Referenz auf den Benutzer (Rechnungsersteller)
// Gespeicherte PDF-Daten (Base64-kodiert)
private byte[] pdfData;
// Constructors
public CustomerInvoice() {
}
@@ -361,4 +364,12 @@ public class CustomerInvoice {
public void setUserId(String userId) {
this.userId = userId;
}
public byte[] getPdfData() {
return pdfData;
}
public void setPdfData(byte[] pdfData) {
this.pdfData = pdfData;
}
}

View File

@@ -44,6 +44,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.vaadin.flow.component.confirmdialog.ConfirmDialog;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.html.IFrame;
import com.vaadin.flow.server.StreamResource;
@@ -435,7 +436,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
}
try {
// Get current user
Optional<User> currentUserOpt = securityService.getAuthenticatedUser()
.flatMap(auth -> userRepository.findByEmail(auth.getUsername()));
@@ -447,7 +447,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
currentUser = currentUserOpt.get();
// Load invoice template from service
Optional<InvoiceTemplate> templateOpt = invoiceTemplateService
.getTemplateByUserId(currentUser.getId().toString());
if (templateOpt.isEmpty()) {
@@ -463,38 +462,51 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
return;
}
// Rechnungsnummer generieren (atomar, Präfix + Zähler)
String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(currentUser.getId());
// Vorschau-PDF mit Platzhalter-Nummer generieren (noch kein Speichern)
byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, currentUser,
getTranslation("createinvoice.preview.number"));
showPdfPreviewDialog(pdfBytes, templateData, currentUser);
} catch (Exception ex) {
log.error("Fehler beim Erstellen der Rechnungsvorschau", ex);
Notification.show(getTranslation("createinvoice.notification.error", ex.getMessage()), 5000,
Notification.Position.BOTTOM_END);
}
}
private void saveInvoice(String templateData, User user, Dialog previewDialog) {
try {
String invoiceNumber = userInvoiceDataService.generateNextInvoiceNumber(user.getId());
// Rechnung in MongoDB speichern
BigDecimal netAmount = calculateNetAmount();
BigDecimal vatRate = calculateAverageVatRate();
BigDecimal vatAmount = netAmount.multiply(vatRate);
BigDecimal totalAmount = netAmount.add(vatAmount);
byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, user, invoiceNumber);
CustomerInvoice invoice = new CustomerInvoice();
invoice.setInvoiceNumber(invoiceNumber);
invoice.setInvoiceDate(java.time.LocalDate.now());
invoice.setJobId(currentJob.getId().toHexString());
invoice.setUserId(currentUser.getId().toHexString());
invoice.setUserId(user.getId().toHexString());
invoice.setNetAmount(netAmount);
invoice.setVatRate(vatRate);
invoice.setVatAmount(vatAmount);
invoice.setTotalAmount(totalAmount);
invoice.setPdfData(pdfBytes);
CustomerInvoice savedInvoice = customerInvoiceRepository.save(invoice);
// Job mit Rechnungs-ID verknüpfen und speichern
currentJob.setInvoiceId(savedInvoice.getId());
jobRepository.save(currentJob);
// PDF mit Rechnungsnummer generieren
byte[] pdfBytes = generateInvoicePdfFromTemplate(templateData, currentUser, invoiceNumber);
// PDF im Dialog anzeigen
showPdfInDialog(pdfBytes, "Rechnung " + invoiceNumber);
previewDialog.close();
Notification.show(getTranslation("createinvoice.notification.saved", invoiceNumber), 4000,
Notification.Position.BOTTOM_END);
} catch (Exception ex) {
log.error("Fehler beim Erstellen der Rechnung", ex);
log.error("Fehler beim Speichern der Rechnung", ex);
Notification.show(getTranslation("createinvoice.notification.error", ex.getMessage()), 5000,
Notification.Position.BOTTOM_END);
}
@@ -617,45 +629,70 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
return value != null ? value : "";
}
private void showPdfInDialog(byte[] pdfBytes, String title) {
// Create a stream resource for the PDF
StreamResource resource = new StreamResource(title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf",
() -> new java.io.ByteArrayInputStream(pdfBytes));
resource.setContentType("application/pdf");
resource.setCacheTime(0);
private void showPdfPreviewDialog(byte[] pdfBytes, String templateData, User user) {
String title = getTranslation("createinvoice.preview.title");
// Store resource in session for access
VaadinSession.getCurrent().setAttribute("currentInvoicePdf", resource);
// Create dialog
Dialog pdfDialog = new Dialog();
pdfDialog.setHeaderTitle(title);
pdfDialog.setWidth("90vw");
pdfDialog.setHeight("90vh");
// Create iframe for PDF
IFrame pdfFrame = new IFrame();
pdfFrame.setWidth("100%");
pdfFrame.setHeight("100%");
// Use data URL for PDF display
String base64Pdf = java.util.Base64.getEncoder().encodeToString(pdfBytes);
String dataUrl = "data:application/pdf;base64," + base64Pdf;
pdfFrame.getElement().setAttribute("src", dataUrl);
pdfFrame.getElement().setAttribute("src", "data:application/pdf;base64," + base64Pdf);
pdfFrame.getStyle().set("border", "none");
// Close button
Button closeButton = new Button(getTranslation("button.close"), e -> pdfDialog.close());
closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
closeButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
// Download button
Button downloadButton = new Button(getTranslation("button.download"), e -> {
getElement().executeJs("const link = document.createElement('a');"
+ "link.href = 'data:application/pdf;base64," + base64Pdf + "';" + "link.download = '"
+ title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf';" + "link.click();");
Button saveButton = new Button(getTranslation("createinvoice.button.save"), e -> {
ConfirmDialog confirm = new ConfirmDialog();
confirm.setHeader(getTranslation("createinvoice.confirm.save.title"));
confirm.setText(getTranslation("createinvoice.confirm.save.message"));
confirm.setConfirmText(getTranslation("createinvoice.confirm.save.confirm"));
confirm.setConfirmButtonTheme("primary");
confirm.setCancelText(getTranslation("button.cancel"));
confirm.setCancelable(true);
confirm.addConfirmListener(ev -> saveInvoice(templateData, user, pdfDialog));
confirm.open();
});
saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS);
pdfDialog.add(pdfFrame);
pdfDialog.getFooter().add(closeButton, saveButton);
pdfDialog.open();
}
public static void showSavedInvoiceDialog(byte[] pdfBytes, String invoiceNumber,
com.vaadin.flow.component.Component parent) {
String title = "Rechnung " + invoiceNumber;
Dialog pdfDialog = new Dialog();
pdfDialog.setHeaderTitle(title);
pdfDialog.setWidth("90vw");
pdfDialog.setHeight("90vh");
IFrame pdfFrame = new IFrame();
pdfFrame.setWidth("100%");
pdfFrame.setHeight("100%");
String base64Pdf = java.util.Base64.getEncoder().encodeToString(pdfBytes);
pdfFrame.getElement().setAttribute("src", "data:application/pdf;base64," + base64Pdf);
pdfFrame.getStyle().set("border", "none");
Button downloadButton = new Button("Herunterladen", e -> {
parent.getElement().executeJs("const link = document.createElement('a');"
+ "link.href = 'data:application/pdf;base64," + base64Pdf + "';"
+ "link.download = '" + title.replaceAll("[^a-zA-Z0-9\\-]", "_") + ".pdf';"
+ "link.click();");
});
downloadButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
Button closeButton = new Button("Schließen", e -> pdfDialog.close());
closeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
pdfDialog.add(pdfFrame);
pdfDialog.getFooter().add(downloadButton, closeButton);
pdfDialog.open();

View File

@@ -22,6 +22,8 @@ import de.assecutor.votianlt.model.Job;
import de.assecutor.votianlt.model.JobStatus;
import de.assecutor.votianlt.messaging.MessagingPublisher;
import de.assecutor.votianlt.util.DateTimeFormatUtil;
import de.assecutor.votianlt.model.invoices.CustomerInvoice;
import de.assecutor.votianlt.repository.CustomerInvoiceRepository;
import de.assecutor.votianlt.repository.JobRepository;
import de.assecutor.votianlt.security.SecurityService;
import de.assecutor.votianlt.service.ClientConnectionService;
@@ -47,17 +49,19 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
private final SecurityService securityService;
private final ClientConnectionService clientConnectionService;
private final MessagingPublisher messagingPublisher;
private final CustomerInvoiceRepository customerInvoiceRepository;
private final Grid<Job> grid = new Grid<>(Job.class, false);
@Autowired
public ShowJobsView(JobRepository jobRepository, JobHistoryService jobHistoryService,
SecurityService securityService, ClientConnectionService clientConnectionService,
MessagingPublisher messagingPublisher) {
MessagingPublisher messagingPublisher, CustomerInvoiceRepository customerInvoiceRepository) {
this.jobRepository = jobRepository;
this.jobHistoryService = jobHistoryService;
this.securityService = securityService;
this.clientConnectionService = clientConnectionService;
this.messagingPublisher = messagingPublisher;
this.customerInvoiceRepository = customerInvoiceRepository;
setSizeFull();
setPadding(true);
setSpacing(true);
@@ -139,11 +143,28 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle {
if (job.getStatus() == JobStatus.COMPLETED) {
Button invoiceBtn = new Button(new Icon(VaadinIcon.DOLLAR));
invoiceBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_SUCCESS);
invoiceBtn.setTooltipText(getTranslation("jobs.tooltip.createinvoice"));
invoiceBtn.addClickListener(e -> {
e.getSource().getElement().getNode(); // prevent row click
getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString()));
});
if (job.getInvoiceId() != null) {
invoiceBtn.setTooltipText(getTranslation("jobs.tooltip.showinvoice"));
invoiceBtn.addClickListener(e -> {
e.getSource().getElement().getNode();
customerInvoiceRepository.findById(job.getInvoiceId()).ifPresentOrElse(
invoice -> {
if (invoice.getPdfData() != null) {
CreateInvoiceView.showSavedInvoiceDialog(invoice.getPdfData(),
invoice.getInvoiceNumber(), this);
} else {
getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString()));
}
},
() -> getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString())));
});
} else {
invoiceBtn.setTooltipText(getTranslation("jobs.tooltip.createinvoice"));
invoiceBtn.addClickListener(e -> {
e.getSource().getElement().getNode();
getUI().ifPresent(ui -> ui.navigate("create_invoice/" + job.getId().toHexString()));
});
}
return invoiceBtn;
}
return new com.vaadin.flow.component.html.Span();

View File

@@ -613,6 +613,7 @@ jobs.historie.manuell=Manuell
jobs.button.csvexport=CSV Export
jobs.tooltip.complete=Auftrag abschließen
jobs.tooltip.createinvoice=Rechnung erstellen
jobs.tooltip.showinvoice=Rechnung anzeigen
jobs.tooltip.delete=Auftrag löschen
jobs.dialog.complete.title=Auftrag abschließen
jobs.dialog.complete.text=Möchten Sie den Auftrag {0} manuell abschließen?
@@ -648,6 +649,13 @@ createinvoice.notification.noservices=Bitte wählen Sie mindestens eine Leistung
createinvoice.notification.nouser=Benutzer nicht gefunden
createinvoice.notification.notemplate=Kein Rechnungstemplate gefunden
createinvoice.notification.error=Fehler beim Erstellen der Rechnung: {0}
createinvoice.notification.saved=Rechnung {0} wurde gespeichert
createinvoice.preview.title=Rechnungsvorschau
createinvoice.preview.number=VORSCHAU
createinvoice.button.save=Speichern
createinvoice.confirm.save.title=Rechnung speichern
createinvoice.confirm.save.message=Diese Rechnung wird dauerhaft gespeichert und kann anschlie\u00dfend nicht mehr ver\u00e4ndert werden. Fortfahren?
createinvoice.confirm.save.confirm=Ja, speichern
# Invoices
invoices.title=Rechnungen

View File

@@ -613,6 +613,7 @@ jobs.historie.manuell=Manual
jobs.button.csvexport=CSV Export
jobs.tooltip.complete=Complete Job
jobs.tooltip.createinvoice=Create Invoice
jobs.tooltip.showinvoice=Show Invoice
jobs.tooltip.delete=Delete Job
jobs.dialog.complete.title=Complete Job
jobs.dialog.complete.text=Do you want to manually complete job {0}?
@@ -648,6 +649,13 @@ createinvoice.notification.noservices=Please select at least one service
createinvoice.notification.nouser=User not found
createinvoice.notification.notemplate=No invoice template found
createinvoice.notification.error=Error creating invoice: {0}
createinvoice.notification.saved=Invoice {0} has been saved
createinvoice.preview.title=Invoice Preview
createinvoice.preview.number=PREVIEW
createinvoice.button.save=Save
createinvoice.confirm.save.title=Save Invoice
createinvoice.confirm.save.message=This invoice will be permanently saved and can no longer be modified. Continue?
createinvoice.confirm.save.confirm=Yes, save
# Invoices
invoices.title=Invoices