From 454dd0b6cc126b767de124b5d9741d927c51c156 Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 23 Sep 2026 15:33:17 +0200 Subject: [PATCH] feat: implement PDF preview popup on sender dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the placeholder preview popup with a fully functional DxPdfViewer popup: - Popup resized to 80vw × 90vh with drag, resize and background shading. - OpenPreviewAsync: unmounts viewer, fetches PDF bytes via GetDocumentOfSenderAsync, then mounts DxPdfViewer only after bytes are ready (prevents DevExpress ProcessDocumentChangedAsync NRE). - Loading spinner shown while fetching; typed error panel shown on NotFoundException or general failure. - OnPreviewClosed: unmounts viewer before nulling byte array so DevExpress async render never touches a disposed reference. - Inject ReceiverPageDataService, EnvelopeAuthService and ILogger. --- .../Components/Pages/EnvelopeSenderPage.razor | 136 ++++++++++++++++-- 1 file changed, 121 insertions(+), 15 deletions(-) diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor index 6a86442c..55576f49 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor @@ -4,13 +4,18 @@ @using EnvelopeGenerator.Domain.Constants @using EnvelopeGenerator.Server.Client.Models @using DevExpress.Blazor +@using DevExpress.Blazor.PdfViewer @using EnvelopeGenerator.Server.Client.Services @using EnvelopeGenerator.Application.Common.Dto +@using EnvelopeGenerator.Server.Services @inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService @inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService @inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService @inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService @inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService +@inject EnvelopeReceiverPageDataService ReceiverPageDataService +@inject IEnvelopeAuthService EnvelopeAuthService +@inject ILogger Logger @inject NavigationManager Navigation @inject IJSRuntime JSRuntime @inject AppVersionService AppVersion @@ -428,22 +433,63 @@ @* ── PDF Vorschau Popup ────────────────────────────────────────────────── *@ + ShowFooter="false" + CssClass="sender-preview-popup" + Closed="OnPreviewClosed" + AllowResize="true" + ApplyBackgroundShading="true" + Scrollable="true" + AllowDrag=true + EnableAdaptivity=true + ShowHeader=true> -
-

Umschlag-ID: @_selectedEnvelope?.Id

-

Titel: @_selectedEnvelope?.Title

-
+ @if (_previewLoading) + { +
+
+
+ Lädt... +
+

Dokument wird geladen…

+
+
+ } + else if (_previewErrorMessage is not null) + { +
+
+ + + + +
+
+

+ Dokument konnte nicht geladen werden +

+

+ @_previewErrorMessage +

+
+
+ } + else if (_previewPdfBytes is not null && _previewViewerMounted) + { +
+ +
+ }
- - -
@code { @@ -464,6 +510,10 @@ // Preview popup private bool _previewVisible = false; + private bool _previewLoading = false; + private bool _previewViewerMounted = false; + private byte[]? _previewPdfBytes = null; + private string? _previewErrorMessage = null; // Stable keys used to store/restore each grid's layout in UserPreferences.GridLayouts private const string GridKeyActive = "sender.active-envelopes"; @@ -599,13 +649,69 @@ var clicked = e.Grid.GetDataItem(e.VisibleIndex) as EnvelopeDto; if (clicked == null) return; _selectedEnvelope = clicked; - _previewVisible = true; + _ = OpenPreviewAsync(clicked); } - void ShowPreview() + async Task ShowPreview() { if (_selectedEnvelope == null) return; + await OpenPreviewAsync(_selectedEnvelope); + } + + private async Task OpenPreviewAsync(EnvelopeDto envelope) + { + // Unmount the viewer first — let the previous instance fully dispose + _previewViewerMounted = false; + _previewPdfBytes = null; + _previewErrorMessage = null; + _previewLoading = true; _previewVisible = true; + await InvokeAsync(StateHasChanged); + + try + { + var user = EnvelopeAuthService.GetCurrentUser(); + if (user?.Identity is null || !user.Identity.IsAuthenticated) + { + _previewErrorMessage = "Nicht authentifiziert. Bitte melden Sie sich erneut an."; + return; + } + + _previewPdfBytes = await ReceiverPageDataService.GetDocumentOfSenderAsync(user, envelope.Id); + + // PDF ready — mount the viewer now + _previewViewerMounted = true; + } + catch (DigitalData.Core.Exceptions.NotFoundException) + { + _previewErrorMessage = $"Das Dokument für Umschlag #{envelope.Id} wurde nicht gefunden oder Sie haben keinen Zugriff darauf."; + Logger.LogWarning("Preview: document not found for envelope {EnvelopeId}", envelope.Id); + } + catch (Exception ex) + { + _previewErrorMessage = $"Fehler beim Laden des Dokuments: {ex.Message}"; + Logger.LogError(ex, "Preview: failed to load document for envelope {EnvelopeId}", envelope.Id); + } + finally + { + _previewLoading = false; + await InvokeAsync(StateHasChanged); + } + } + + void OnPreviewClosed() + { + // Unmount the viewer first — prevent DevExpress async render from + // accessing the already-disposed byte array + _previewViewerMounted = false; + _ = InvokeAsync(async () => + { + await Task.Yield(); // wait one render cycle for the viewer to be removed from the DOM + _previewPdfBytes = null; + _previewErrorMessage = null; + _previewLoading = false; + await InvokeAsync(StateHasChanged); + }); } // ── Grid Layout Persistence ───────────────────────────────────────────────