diff --git a/src/main/java/de/assecutor/votianlt/model/invoices/CustomerInvoice.java b/src/main/java/de/assecutor/votianlt/model/invoices/CustomerInvoice.java index 8595809..ed7044a 100644 --- a/src/main/java/de/assecutor/votianlt/model/invoices/CustomerInvoice.java +++ b/src/main/java/de/assecutor/votianlt/model/invoices/CustomerInvoice.java @@ -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; + } } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java index 8dcb797..4443354 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java @@ -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 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 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(); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java b/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java index ba9a8cf..eb58d50 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java @@ -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 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(); diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 308a840..4cf784c 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -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 diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index 830b5f2..41310d6 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -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