diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverReportPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverReportPage.razor new file mode 100644 index 00000000..06b44f44 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverReportPage.razor @@ -0,0 +1,771 @@ +@page "/envelope/{EnvelopeKey}/report" +@rendermode InteractiveServer +@using DevExpress.Blazor.Reporting +@using DevExpress.XtraReports.UI +@using DevExpress.XtraPrinting +@using DevExpress.XtraPrinting.Drawing +@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 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 +@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)) + { + + 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") + } + + } + } + 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) + { + + } +
+
+ + @* 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 +
+ } +
+ } +
+
+ +
+ @if (_isLoading) + { +
+
+
+ Lädt... +
+

Dokument wird geladen...

+
+
+ } + else if (_errorMessage is not null) + { +
+
+
+ + + + +
+
Fehler beim Laden des Dokuments
+

@_errorMessage

+
+
+
+
+ } + 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"; + + // A4 page dimensions in DX units (1/100 inch). + // 8.27" × 11.69" → 827 × 1169 + const float PageWidthDx = 827f; + const float PageHeightDx = 1169f; + + // Fixed signature field size in DX units: 1.77" × 1.96" + const float SigWidthDx = 177f; + const float SigHeightDx = 196f; + + 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 ----- + bool _isLoading = true; + string? _errorMessage; + byte[]? _pdfBytes; + IReadOnlyList _signatures = []; + EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver; + ClaimsPrincipal? _receiverUser; + + // ----- Report viewer ----- + DxReportViewer? _reportViewer; + XtraReport? _report; + + // ----- 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)) + { + _errorMessage = "Envelope-Schlüssel fehlt."; + _isLoading = false; + return; + } + + // Authorization — same pattern as EnvelopeReceiverPage + _receiverUser = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey); + if (_receiverUser is null) + { + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}"); + return; + } + + try + { + // Load PDF bytes via MediatR (uses authenticated user's claims) + _pdfBytes = await PageDataService.GetDocumentAsync(_receiverUser); + if (_pdfBytes is not { Length: > 0 }) + { + _errorMessage = "Dokument konnte nicht geladen werden: Keine Daten empfangen."; + _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 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) + { + Logger.LogWarning(ex, "Failed to load cached signature for {EnvelopeKey}", EnvelopeKey); + _activeSignatureTab = SignatureTabDraw; + _signaturePopupVisible = _signatures.Count > 0; + _popupValidationMessage = null; + } + } + catch (Exception ex) + { + _errorMessage = $"Fehler beim Laden des Dokuments: {ex.Message}"; + Logger.LogError(ex, "Unexpected error for {EnvelopeKey}", EnvelopeKey); + } + + _isLoading = false; + await InvokeAsync(StateHasChanged); + } + + // ----- Report builder ----- + /// + /// Builds an XtraReport that displays the PDF pages via XRPdfContent (embedded mode, + /// GenerateOwnPages = false). Each PDF page is wrapped in its own subreport so that + /// XRPictureBox overlays can be positioned accurately per page. + /// + static XtraReport BuildReport( + byte[] pdfBytes, + IReadOnlyList signatures, + SignatureCaptureDto? capturedSignature) + { + // Determine the number of pages using DevExpress PDF processor + int pageCount = GetPdfPageCount(pdfBytes); + if (pageCount < 1) pageCount = 1; + + // Outer (main) report - acts as container for subreports + var mainReport = new XtraReport + { + PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4, + Landscape = false, + Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0), + }; + + var mainDetail = new DetailBand { HeightF = 0f }; + mainReport.Bands.Add(mainDetail); + + for (int page = 1; page <= pageCount; page++) + { + // Build a subreport for this PDF page + var pageReport = BuildPageSubreport(pdfBytes, page, signatures, capturedSignature); + + var subreport = new XRSubreport + { + ReportSource = pageReport, + GenerateOwnPages = true, + LocationF = new PointF(0f, 0f), + SizeF = new SizeF(PageWidthDx, PageHeightDx), + }; + + mainDetail.Controls.Add(subreport); + } + + return mainReport; + } + + /// + /// Builds a single-page subreport: one DetailBand containing the PDF page (via + /// XRPdfContent with GenerateOwnPages = false) plus XRPictureBox overlays for + /// any signatures placed on this page. + /// + static XtraReport BuildPageSubreport( + byte[] pdfBytes, + int pageNumber, + IReadOnlyList signatures, + SignatureCaptureDto? 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 = PageHeightDx, + Name = $"DetailBand_Page{pageNumber}", + }; + report.Bands.Add(detail); + + // --- PDF content (embedded, no own pages) --- + var pdfContent = new XRPdfContent + { + Source = pdfBytes, + PageRange = pageNumber.ToString(), + GenerateOwnPages = false, + LocationF = new PointF(0f, 0f), + SizeF = new SizeF(PageWidthDx, PageHeightDx), + }; + detail.Controls.Add(pdfContent); + + // --- Signature overlays --- + if (capturedSignature is not null && !string.IsNullOrWhiteSpace(capturedSignature.DataUrl)) + { + var signaturesOnPage = signatures.Where(s => s.Page == pageNumber).ToList(); + foreach (var sig in signaturesOnPage) + { + try + { + var imgBytes = DataUrlToBytes(capturedSignature.DataUrl); + if (imgBytes is { Length: > 0 }) + { + using var imgStream = new System.IO.MemoryStream(imgBytes); + var img = System.Drawing.Image.FromStream(imgStream); + var picBox = new XRPictureBox + { + // DB stores INCHES; DX unit = 1/100 inch → multiply by 100 + LocationF = new PointF((float)(sig.X * 100), (float)(sig.Y * 100)), + SizeF = new SizeF(SigWidthDx, SigHeightDx), + Image = img, + Sizing = ImageSizeMode.Squeeze, + CanGrow = false, + CanShrink = false, + }; + detail.Controls.Add(picBox); + } + } + catch + { + // Non-critical: skip overlay on error + } + } + } + + return report; + } + + /// Reads the page count of a PDF using iText7 (already referenced in the server project). + static int GetPdfPageCount(byte[] pdfBytes) + { + try + { + using var ms = new System.IO.MemoryStream(pdfBytes); + using var reader = new iText.Kernel.Pdf.PdfReader(ms); + using var pdfDoc = new iText.Kernel.Pdf.PdfDocument(reader); + return pdfDoc.GetNumberOfPages(); + } + catch + { + return 1; + } + } + + /// 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(); + } + else + await JSRuntime.InvokeVoidAsync("receiverSignature.initializeImage", ImageInputId, ImageCanvasId); + } + + 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(); + } +}