diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverReportSignedPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverReportSignedPage.razor index a5036070..97f925fb 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverReportSignedPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverReportSignedPage.razor @@ -3,149 +3,46 @@ @using DevExpress.Blazor.Reporting @using DevExpress.XtraReports.UI @using EnvelopeGenerator.Server.Client.Models -@using EnvelopeGenerator.Server.Client.Models.Constants @using EnvelopeGenerator.Server.Client.Services @using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver -@using Microsoft.JSInterop -@using DevExpress.Blazor -@using System.Drawing +@using Microsoft.Extensions.Caching.Memory @using System.Security.Claims @inject NavigationManager Navigation @inject IJSRuntime JSRuntime -@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService @inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService @inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService @inject AppVersionService AppVersion -@inject ILogger Logger +@inject IMemoryCache MemoryCache +@inject ILogger Logger @implements IDisposable -
- @* Row 1: Title + Sender + Badges *@
- @* Left: Title + Sender *@
@if (_envelopeReceiver is not null) {
@(_envelopeReceiver.Envelope?.Title ?? "Dokument")
- @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName) || !string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) + @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName)) { - Von - @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName)) - { - @_envelopeReceiver.Envelope.User.FullName - } - @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) - { - <@_envelopeReceiver.Envelope.User.Email> - } - @if (_envelopeReceiver.Envelope?.AddedWhen != null) - { -  · @_envelopeReceiver.Envelope.AddedWhen.ToString("dd.MM.yyyy") - } + Von @_envelopeReceiver.Envelope.User.FullName } } else { -
Dokumentenansicht
- } -
- - @* Right: Badges + Signature status *@ -
- @if (_envelopeReceiver is not null) - { -
- @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Name)) - { - - - - - @_envelopeReceiver.Name - - } - @if (_signatures.Count > 0) - { - - - - - @_signatures.Count Unterschrift@(_signatures.Count != 1 ? "en" : "") - @if (_capturedSignature is not null) - { - - } - - } - @if (_envelopeReceiver.Envelope?.UseAccessCode ?? false) - { - - - - - Code - - } - @if (_envelopeReceiver.Envelope?.TFAEnabled ?? false) - { - - - - - - 2FA - - } -
- } - - @* Unterschrift ändern button (when signature captured) *@ - @if (_capturedSignature is not null) - { - +
Signiertes Dokument
}
- - @* Row 2: Messages *@ - @if (_envelopeReceiver is not null && (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message) || !string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage))) - { -
- @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message)) - { -
- 📧 - @_envelopeReceiver.Envelope.Message -
- } - @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage)) - { -
- 🔒 - @_envelopeReceiver.PrivateMessage -
- } -
- }
@@ -171,7 +68,7 @@
-
Fehler beim Laden des Dokuments
+
Fehler

@_errorMessage

@@ -180,212 +77,24 @@ } else if (_report is not null) { - + } -@* Signature Popup *@ - - - - - @if (_activeSignatureTab == SignatureTabDraw) - { -

Bitte unterschreiben Sie im folgenden Feld.

- - } - else if (_activeSignatureTab == SignatureTabText) - { -

Geben Sie Ihre Unterschrift als Text ein und wählen Sie eine Schriftart.

-
-
- -
-
- -
-
- - } - else - { -

Laden Sie ein Bild Ihrer Unterschrift hoch.

- - - } - -
-
-
- - -
-
- - -
-
- - -
-
-
- - @if (!string.IsNullOrWhiteSpace(_popupValidationMessage)) - { -
- @_popupValidationMessage -
- } -
- -
- - -
-
-
- @code { - // ----- Constants ----- - const string SignatureTabDraw = "draw"; - const string SignatureTabText = "text"; - const string SignatureTabImage = "image"; - const string DrawCanvasId = "rp-signature-pad"; - const string TypedCanvasId = "rp-typed-signature-pad"; - const string ImageInputId = "rp-signature-image-input"; - const string ImageCanvasId = "rp-image-signature-pad"; - - readonly (string Text, string Value)[] TypedSignatureFonts = - [ - ("Brush Script", "'Brush Script MT', cursive"), - ("Segoe Script", "'Segoe Script', cursive"), - ("Lucida Handwriting", "'Lucida Handwriting', cursive"), - ("Comic Sans", "'Comic Sans MS', cursive"), - ("Cursive", "cursive"), - ]; - - // ----- Parameters ----- [Parameter] public string? EnvelopeKey { get; set; } - // ----- Page state ----- + [SupplyParameterFromQuery(Name = "sid")] + public string? Sid { get; set; } + bool _isLoading = true; string? _errorMessage; - byte[]? _pdfBytes; - IReadOnlyList _signatures = []; - EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver; ClaimsPrincipal? _receiverUser; - - // ----- Report viewer ----- - DxReportViewer? _reportViewer; + EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver; XtraReport? _report; + SignatureCaptureDto? _sig; - // ----- Signature popup state ----- - SignatureCaptureDto? _capturedSignature; - bool _signaturePopupVisible = false; - string? _popupValidationMessage; - string _activeSignatureTab = SignatureTabDraw; - string _typedSignatureText = string.Empty; - string _typedSignatureFont = "'Brush Script MT', cursive"; - string _signerFullName = string.Empty; - string _signerPosition = string.Empty; - string _signaturePlace = string.Empty; - - // ----- Lifecycle ----- protected override async Task OnInitializedAsync() { if (string.IsNullOrWhiteSpace(EnvelopeKey)) @@ -395,7 +104,6 @@ return; } - // Authorization — same pattern as EnvelopeReceiverPage _receiverUser = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey); if (_receiverUser is null) { @@ -403,281 +111,67 @@ return; } + // Read signature from IMemoryCache + if (!string.IsNullOrWhiteSpace(Sid) + && MemoryCache.TryGetValue(Sid, out SignatureCaptureDto? cached) + && cached is not null) + { + _sig = cached; + } + try { - // Load PDF bytes via MediatR (uses authenticated user's claims) - _pdfBytes = await PageDataService.GetDocumentAsync(_receiverUser); - if (_pdfBytes is not { Length: > 0 }) + var pdfBytes = await PageDataService.GetDocumentAsync(_receiverUser); + if (pdfBytes is not { Length: > 0 }) { - _errorMessage = "Dokument konnte nicht geladen werden: Keine Daten empfangen."; + _errorMessage = "Dokument konnte nicht geladen werden."; _isLoading = false; return; } - // Load signature fields for this receiver - _signatures = await PageDataService.GetSignaturesAsync(_receiverUser); - - // Load envelope receiver metadata _envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey); - if (_envelopeReceiver is null) - Logger.LogWarning("Envelope receiver data is null for {EnvelopeKey}", EnvelopeKey); - // Build initial report (no signature image yet) - _report = BuildReport(_pdfBytes, _signatures, capturedSignature: null); - - // Try to restore cached signature - try + var report = new XtraReport { - var cachedSignature = await PageDataService.GetCachedSignatureAsync(_receiverUser); - if (cachedSignature is not null) - { - _capturedSignature = cachedSignature; - _signerFullName = cachedSignature.FullName; - _signerPosition = cachedSignature.Position; - _signaturePlace = cachedSignature.Place; - _signaturePopupVisible = false; - - // Rebuild with cached signature overlaid - _report = BuildReport(_pdfBytes, _signatures, _capturedSignature); - } - else - { - _activeSignatureTab = SignatureTabDraw; - _signaturePopupVisible = _signatures.Count > 0; - _popupValidationMessage = null; - } - } - catch (Exception ex) + PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4, + Landscape = false, + Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0), + }; + var detail = new DetailBand(); + report.Bands.Add(detail); + detail.Controls.Add(new XRPdfContent { - Logger.LogWarning(ex, "Failed to load cached signature for {EnvelopeKey}", EnvelopeKey); - _activeSignatureTab = SignatureTabDraw; - _signaturePopupVisible = _signatures.Count > 0; - _popupValidationMessage = null; - } + Source = pdfBytes, + GenerateOwnPages = true, + }); + _report = report; } catch (Exception ex) { - _errorMessage = $"Fehler beim Laden des Dokuments: {ex.Message}"; - Logger.LogError(ex, "Unexpected error for {EnvelopeKey}", EnvelopeKey); + _errorMessage = $"Fehler: {ex.Message}"; + Logger.LogError(ex, "Error loading signed page for {EnvelopeKey}", EnvelopeKey); } _isLoading = false; await InvokeAsync(StateHasChanged); } - // ----- Report builder ----- - /// - /// Builds an XtraReport wrapping the PDF bytes. - /// If a signature is captured and there are signature fields, the signature image is - /// first burned into the PDF via DevExpress PdfDocumentProcessor, then the modified - /// PDF is handed to XRPdfContent with GenerateOwnPages = true so that all pages appear. - /// - static XtraReport BuildReport( - byte[] pdfBytes, - IReadOnlyList signatures, - SignatureCaptureDto? capturedSignature) + protected override async Task OnAfterRenderAsync(bool firstRender) { - // Burn signatures into PDF bytes when a captured signature is available - byte[] sourcePdf = pdfBytes; - if (capturedSignature is not null - && !string.IsNullOrWhiteSpace(capturedSignature.DataUrl) - && signatures.Count > 0) + if (!firstRender) return; + + if (_sig is not null) { - sourcePdf = BurnSignaturesIntoPdf(pdfBytes, signatures, capturedSignature); - } - - var report = new XtraReport - { - PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4, - Landscape = false, - Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0), - }; - - var detail = new DetailBand { HeightF = 0f }; - report.Bands.Add(detail); - - // GenerateOwnPages = true (default): each PDF page becomes a separate report page - var pdfContent = new XRPdfContent - { - Source = sourcePdf, - GenerateOwnPages = true, - }; - detail.Controls.Add(pdfContent); - - return report; - } - - /// - /// Burns signature images directly into the PDF using DevExpress PdfGraphics API. - /// Coordinates: DB stores INCHES with top-left origin, Y down. - /// PDF coordinate system: bottom-left origin, Y up, unit = points (1/72 inch). - /// Note: Implementation placeholder — requires DevExpress.Pdf.Drawing API wiring (Problem 2). - /// - static byte[] BurnSignaturesIntoPdf( - byte[] pdfBytes, - IReadOnlyList signatures, - SignatureCaptureDto capturedSignature) - { - // TODO: Implement with PdfGraphics when Problem 2 is addressed. - // For now return unmodified PDF so Problem 1 (all pages) can be verified first. - return pdfBytes; - } - - /// Converts a base64 data URL (data:image/...;base64,...) to raw bytes. - static byte[]? DataUrlToBytes(string dataUrl) - { - try - { - var commaIndex = dataUrl.IndexOf(','); - if (commaIndex < 0) return null; - return Convert.FromBase64String(dataUrl[(commaIndex + 1)..]); - } - catch - { - return null; - } - } - - // ----- Signature popup handlers ----- - void OpenSignaturePopup() - { - _activeSignatureTab = SignatureTabDraw; - _signaturePopupVisible = true; - _popupValidationMessage = null; - } - - async Task OnPopupShownAsync() - { - await InitializeActiveSignatureTabAsync(); - } - - async Task SetSignatureTabAsync(string tab) - { - _activeSignatureTab = tab; - _popupValidationMessage = null; - await InvokeAsync(StateHasChanged); - await Task.Delay(50); - await InitializeActiveSignatureTabAsync(); - } - - async Task InitializeActiveSignatureTabAsync() - { - if (_activeSignatureTab == SignatureTabDraw) - await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", DrawCanvasId); - else if (_activeSignatureTab == SignatureTabText) - { - await JSRuntime.InvokeVoidAsync("receiverSignature.initializeTyped", TypedCanvasId); - await RenderTypedSignatureAsync(); + await JSRuntime.InvokeVoidAsync("console.log", + $"[SignedPage] sid={Sid} | FullName={_sig.FullName} | Place={_sig.Place} | Position={_sig.Position} | DataUrl.Length={_sig.DataUrl?.Length ?? 0}"); } else - await JSRuntime.InvokeVoidAsync("receiverSignature.initializeImage", ImageInputId, ImageCanvasId); + { + await JSRuntime.InvokeVoidAsync("console.log", + $"[SignedPage] Cache miss or no sid. sid={Sid}"); + } } - async Task RenewSignatureAsync() - { - _popupValidationMessage = null; - if (_activeSignatureTab == SignatureTabDraw) - await JSRuntime.InvokeVoidAsync("receiverSignature.clear", DrawCanvasId); - else if (_activeSignatureTab == SignatureTabText) - { - _typedSignatureText = string.Empty; - await JSRuntime.InvokeVoidAsync("receiverSignature.clearTyped", TypedCanvasId); - } - else - await JSRuntime.InvokeVoidAsync("receiverSignature.clearImage", ImageInputId, ImageCanvasId); - } - - async Task OnTypedSignatureChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) - { - _typedSignatureText = args.Value?.ToString() ?? string.Empty; - await RenderTypedSignatureAsync(); - } - - async Task OnTypedSignatureFontChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) - { - _typedSignatureFont = args.Value?.ToString() ?? _typedSignatureFont; - await RenderTypedSignatureAsync(); - } - - async Task RenderTypedSignatureAsync() - { - await JSRuntime.InvokeVoidAsync("receiverSignature.renderTypedSignature", - TypedCanvasId, _typedSignatureText, _typedSignatureFont); - } - - async Task SaveSignatureAsync() - { - if (string.IsNullOrWhiteSpace(_signerFullName)) - { - _popupValidationMessage = "Bitte geben Sie Vor- und Nachname ein."; - return; - } - if (string.IsNullOrWhiteSpace(_signaturePlace)) - { - _popupValidationMessage = "Bitte geben Sie den Ort ein."; - return; - } - - var signatureDataUrl = await GetActiveSignatureDataUrlAsync(); - if (string.IsNullOrWhiteSpace(signatureDataUrl)) - { - _popupValidationMessage = "Die Unterschrift ist erforderlich."; - return; - } - - _popupValidationMessage = null; - _capturedSignature = new SignatureCaptureDto - { - DataUrl = signatureDataUrl, - FullName = _signerFullName.Trim(), - Position = _signerPosition.Trim(), - Place = _signaturePlace.Trim(), - }; - _signaturePopupVisible = false; - - // Persist to cache (fire-and-forget) - if (_receiverUser is not null) - { - _ = Task.Run(async () => - { - try { await PageDataService.SaveCachedSignatureAsync(_receiverUser, _capturedSignature); } - catch { /* non-critical */ } - }); - } - - // Rebuild the report with signatures overlaid - if (_pdfBytes is not null) - { - var newReport = BuildReport(_pdfBytes, _signatures, _capturedSignature); - - if (_reportViewer is not null) - { - await _reportViewer.OpenReportAsync(newReport); - // Dispose previous report after opening new one - _report?.Dispose(); - } - - _report = newReport; - } - - await InvokeAsync(StateHasChanged); - } - - async Task GetActiveSignatureDataUrlAsync() - { - if (_activeSignatureTab == SignatureTabDraw) - return await JSRuntime.InvokeAsync("receiverSignature.getDataUrl", DrawCanvasId); - - if (_activeSignatureTab == SignatureTabText) - { - await RenderTypedSignatureAsync(); - return await JSRuntime.InvokeAsync("receiverSignature.getTypedDataUrl", TypedCanvasId); - } - - return await JSRuntime.InvokeAsync("receiverSignature.getImageDataUrl", ImageCanvasId); - } - - // ----- Disposal ----- public void Dispose() { _report?.Dispose();