feat: implement PDF preview popup on sender dashboard
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.
This commit is contained in:
@@ -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<EnvelopeSenderPage> Logger
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject AppVersionService AppVersion
|
||||
@@ -428,22 +433,63 @@
|
||||
|
||||
@* ── PDF Vorschau Popup ────────────────────────────────────────────────── *@
|
||||
<DxPopup @bind-Visible="@_previewVisible"
|
||||
HeaderText="@($"Vorschau — Umschlag #{_selectedEnvelope?.Id}")"
|
||||
Width="480px"
|
||||
HeaderText="@($"Vorschau — {_selectedEnvelope?.Title} (#{_selectedEnvelope?.Id})")"
|
||||
Width="80vw"
|
||||
MaxWidth="80vw"
|
||||
Height="90vh"
|
||||
MaxHeight="90vh"
|
||||
CloseOnOutsideClick="true"
|
||||
ShowFooter="true">
|
||||
ShowFooter="false"
|
||||
CssClass="sender-preview-popup"
|
||||
Closed="OnPreviewClosed"
|
||||
AllowResize="true"
|
||||
ApplyBackgroundShading="true"
|
||||
Scrollable="true"
|
||||
AllowDrag=true
|
||||
EnableAdaptivity=true
|
||||
ShowHeader=true>
|
||||
<BodyContentTemplate>
|
||||
<div style="padding: 1rem;">
|
||||
<p><strong>Umschlag-ID:</strong> @_selectedEnvelope?.Id</p>
|
||||
<p><strong>Titel:</strong> @_selectedEnvelope?.Title</p>
|
||||
</div>
|
||||
@if (_previewLoading)
|
||||
{
|
||||
<div style="display:flex;justify-content:center;align-items:center;height:100%;min-height:300px;">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border mb-3" style="width:3rem;height:3rem;" role="status">
|
||||
<span class="visually-hidden">Lädt...</span>
|
||||
</div>
|
||||
<p style="color:#6b7280;font-size:0.875rem;">Dokument wird geladen…</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (_previewErrorMessage is not null)
|
||||
{
|
||||
<div style="display:flex;gap:1rem;align-items:flex-start;padding:1.5rem;">
|
||||
<div style="flex-shrink:0;width:40px;height:40px;border-radius:50%;
|
||||
background:#fee2e2;display:flex;align-items:center;justify-content:center;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#dc2626" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p style="margin:0 0 0.4rem;font-weight:600;color:#1f2937;font-size:0.95rem;">
|
||||
Dokument konnte nicht geladen werden
|
||||
</p>
|
||||
<p style="margin:0;color:#6b7280;font-size:0.85rem;line-height:1.5;word-break:break-word;">
|
||||
@_previewErrorMessage
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (_previewPdfBytes is not null && _previewViewerMounted)
|
||||
{
|
||||
<div class="sender-preview-viewer-wrap" style="padding: 0 5.5rem;">
|
||||
<DxPdfViewer DocumentContent="@_previewPdfBytes"
|
||||
ZoomLevel="-2"
|
||||
IsSinglePagePreview="true"
|
||||
CssClass="sender-preview-pdf-viewer" />
|
||||
</div>
|
||||
}
|
||||
</BodyContentTemplate>
|
||||
<FooterContentTemplate>
|
||||
<DxButton CssClass="popup-button my-1 ms-2"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
Text="Schließen"
|
||||
Click="@(() => _previewVisible = false)" />
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
@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 ───────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user