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.Domain.Constants
|
||||||
@using EnvelopeGenerator.Server.Client.Models
|
@using EnvelopeGenerator.Server.Client.Models
|
||||||
@using DevExpress.Blazor
|
@using DevExpress.Blazor
|
||||||
|
@using DevExpress.Blazor.PdfViewer
|
||||||
@using EnvelopeGenerator.Server.Client.Services
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
@using EnvelopeGenerator.Application.Common.Dto
|
@using EnvelopeGenerator.Application.Common.Dto
|
||||||
|
@using EnvelopeGenerator.Server.Services
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService
|
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService
|
@inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService
|
@inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
|
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
|
||||||
|
@inject EnvelopeReceiverPageDataService ReceiverPageDataService
|
||||||
|
@inject IEnvelopeAuthService EnvelopeAuthService
|
||||||
|
@inject ILogger<EnvelopeSenderPage> Logger
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@inject AppVersionService AppVersion
|
@inject AppVersionService AppVersion
|
||||||
@@ -428,22 +433,63 @@
|
|||||||
|
|
||||||
@* ── PDF Vorschau Popup ────────────────────────────────────────────────── *@
|
@* ── PDF Vorschau Popup ────────────────────────────────────────────────── *@
|
||||||
<DxPopup @bind-Visible="@_previewVisible"
|
<DxPopup @bind-Visible="@_previewVisible"
|
||||||
HeaderText="@($"Vorschau — Umschlag #{_selectedEnvelope?.Id}")"
|
HeaderText="@($"Vorschau — {_selectedEnvelope?.Title} (#{_selectedEnvelope?.Id})")"
|
||||||
Width="480px"
|
Width="80vw"
|
||||||
|
MaxWidth="80vw"
|
||||||
|
Height="90vh"
|
||||||
|
MaxHeight="90vh"
|
||||||
CloseOnOutsideClick="true"
|
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>
|
<BodyContentTemplate>
|
||||||
<div style="padding: 1rem;">
|
@if (_previewLoading)
|
||||||
<p><strong>Umschlag-ID:</strong> @_selectedEnvelope?.Id</p>
|
{
|
||||||
<p><strong>Titel:</strong> @_selectedEnvelope?.Title</p>
|
<div style="display:flex;justify-content:center;align-items:center;height:100%;min-height:300px;">
|
||||||
</div>
|
<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>
|
</BodyContentTemplate>
|
||||||
<FooterContentTemplate>
|
|
||||||
<DxButton CssClass="popup-button my-1 ms-2"
|
|
||||||
RenderStyle="ButtonRenderStyle.Secondary"
|
|
||||||
Text="Schließen"
|
|
||||||
Click="@(() => _previewVisible = false)" />
|
|
||||||
</FooterContentTemplate>
|
|
||||||
</DxPopup>
|
</DxPopup>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
@@ -464,6 +510,10 @@
|
|||||||
|
|
||||||
// Preview popup
|
// Preview popup
|
||||||
private bool _previewVisible = false;
|
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
|
// Stable keys used to store/restore each grid's layout in UserPreferences.GridLayouts
|
||||||
private const string GridKeyActive = "sender.active-envelopes";
|
private const string GridKeyActive = "sender.active-envelopes";
|
||||||
@@ -599,13 +649,69 @@
|
|||||||
var clicked = e.Grid.GetDataItem(e.VisibleIndex) as EnvelopeDto;
|
var clicked = e.Grid.GetDataItem(e.VisibleIndex) as EnvelopeDto;
|
||||||
if (clicked == null) return;
|
if (clicked == null) return;
|
||||||
_selectedEnvelope = clicked;
|
_selectedEnvelope = clicked;
|
||||||
_previewVisible = true;
|
_ = OpenPreviewAsync(clicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShowPreview()
|
async Task ShowPreview()
|
||||||
{
|
{
|
||||||
if (_selectedEnvelope == null) return;
|
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;
|
_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 ───────────────────────────────────────────────
|
// ── Grid Layout Persistence ───────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user