Simplify signed document viewer logic

Removed all signature capture and validation functionality, including the signature popup, JavaScript interop, and related backend logic. Simplified the `DxReportViewer` initialization to directly display the signed document. Added support for retrieving cached signatures via the `sid` query parameter. Streamlined error handling, logging, and page metadata. Cleaned up unused imports, constants, and methods to reduce complexity.
This commit is contained in:
2026-07-01 09:57:19 +02:00
parent 185c783824
commit 2d22bfcd06

View File

@@ -3,149 +3,46 @@
@using DevExpress.Blazor.Reporting @using DevExpress.Blazor.Reporting
@using DevExpress.XtraReports.UI @using DevExpress.XtraReports.UI
@using EnvelopeGenerator.Server.Client.Models @using EnvelopeGenerator.Server.Client.Models
@using EnvelopeGenerator.Server.Client.Models.Constants
@using EnvelopeGenerator.Server.Client.Services @using EnvelopeGenerator.Server.Client.Services
@using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver @using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver
@using Microsoft.JSInterop @using Microsoft.Extensions.Caching.Memory
@using DevExpress.Blazor
@using System.Drawing
@using System.Security.Claims @using System.Security.Claims
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService @inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService @inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
@inject AppVersionService AppVersion @inject AppVersionService AppVersion
@inject ILogger<EnvelopeReceiverReportPage> Logger @inject IMemoryCache MemoryCache
@inject ILogger<EnvelopeReceiverReportSignedPage> Logger
@implements IDisposable @implements IDisposable
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" />
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" /> <link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
<script src="@AppVersion.GetVersionedUrl("js/receiver-signature.js")"></script>
<div class="envelope-viewer-layout"> <div class="envelope-viewer-layout">
<div class="envelope-action-bar"> <div class="envelope-action-bar">
<div class="envelope-action-bar__inner" style="flex-direction: column; align-items: stretch; padding: 0.35rem 1.5rem; gap: 0.35rem;"> <div class="envelope-action-bar__inner" style="flex-direction: column; align-items: stretch; padding: 0.35rem 1.5rem; gap: 0.35rem;">
@* Row 1: Title + Sender + Badges *@
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem;"> <div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem;">
@* Left: Title + Sender *@
<div style="flex: 0 1 auto; min-width: 0; display: flex; align-items: center; gap: 0.75rem;"> <div style="flex: 0 1 auto; min-width: 0; display: flex; align-items: center; gap: 0.75rem;">
@if (_envelopeReceiver is not null) @if (_envelopeReceiver is not null)
{ {
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"> <div style="font-size: 0.9rem; font-weight: 600; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
@(_envelopeReceiver.Envelope?.Title ?? "Dokument") @(_envelopeReceiver.Envelope?.Title ?? "Dokument")
</div> </div>
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName) || !string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName))
{ {
<span style="font-size: 0.7rem; color: #6b7280; white-space: nowrap;"> <span style="font-size: 0.7rem; color: #6b7280; white-space: nowrap;">
Von Von <span style="font-weight: 500; color: #374151;">@_envelopeReceiver.Envelope.User.FullName</span>
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName))
{
<span style="font-weight: 500; color: #374151;">@_envelopeReceiver.Envelope.User.FullName</span>
}
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email))
{
<span>&lt;@_envelopeReceiver.Envelope.User.Email&gt;</span>
}
@if (_envelopeReceiver.Envelope?.AddedWhen != null)
{
<span>&nbsp;·&nbsp;@_envelopeReceiver.Envelope.AddedWhen.ToString("dd.MM.yyyy")</span>
}
</span> </span>
} }
} }
else else
{ {
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937;">Dokumentenansicht</div> <div style="font-size: 0.9rem; font-weight: 600; color: #1f2937;">Signiertes Dokument</div>
}
</div>
@* Right: Badges + Signature status *@
<div class="d-flex align-items-center" style="gap: 0.75rem; flex: 0 0 auto;">
@if (_envelopeReceiver is not null)
{
<div class="d-flex flex-wrap align-items-center" style="gap: 0.3rem; font-size: 0.7rem;">
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Name))
{
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: #f3f4f6; border-radius: 0.25rem; color: #374151; white-space: nowrap;">
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Z" />
</svg>
@_envelopeReceiver.Name
</span>
}
@if (_signatures.Count > 0)
{
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: @(_capturedSignature is not null ? "#d1fae5" : "#ede9fe"); border-radius: 0.25rem; color: @(_capturedSignature is not null ? "#065f46" : "#6d28d9"); font-weight: 500; white-space: nowrap;">
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
</svg>
@_signatures.Count Unterschrift@(_signatures.Count != 1 ? "en" : "")
@if (_capturedSignature is not null)
{
<span class="ms-1">✓</span>
}
</span>
}
@if (_envelopeReceiver.Envelope?.UseAccessCode ?? false)
{
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: #fef3c7; border-radius: 0.25rem; color: #92400e; font-weight: 500; white-space: nowrap;">
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" />
</svg>
Code
</span>
}
@if (_envelopeReceiver.Envelope?.TFAEnabled ?? false)
{
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: #dbeafe; border-radius: 0.25rem; color: #1e40af; font-weight: 500; white-space: nowrap;">
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M5.338 1.59a61.44 61.44 0 0 0-2.837.856.481.481 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.725 10.725 0 0 0 2.287 2.233c.346.244.652.42.893.533.12.057.218.095.293.118a.55.55 0 0 0 .101.025.615.615 0 0 0 .1-.025c.076-.023.174-.061.294-.118.24-.113.547-.29.893-.533a10.726 10.726 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.775 11.775 0 0 1-2.517 2.453 7.159 7.159 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7.158 7.158 0 0 1-1.048-.625 11.777 11.777 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 62.456 62.456 0 0 1 5.072.56z" />
<path d="M10.854 5.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 7.793l2.646-2.647a.5.5 0 0 1 .708 0z" />
</svg>
2FA
</span>
}
</div>
}
@* Unterschrift ändern button (when signature captured) *@
@if (_capturedSignature is not null)
{
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change pdf-toolbar__btn--signature-change-active"
@onclick="OpenSignaturePopup"
title="Unterschrift ändern"
style="flex-shrink: 0;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
</svg>
<span class="pdf-toolbar__btn-text">Unterschrift</span>
</button>
} }
</div> </div>
</div> </div>
@* Row 2: Messages *@
@if (_envelopeReceiver is not null && (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message) || !string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage)))
{
<div style="display: flex; align-items: flex-start; gap: 0.5rem; font-size: 0.7rem; padding-top: 0.15rem; border-top: 1px solid #e5e7eb;">
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message))
{
<div style="flex: 1; min-width: 0; padding: 0.2rem 0.4rem; background: #f9fafb; border-radius: 0.25rem; border-left: 2px solid #9ca3af; display: flex; align-items: flex-start; gap: 0.25rem;">
<span style="font-weight: 500; color: #374151; flex-shrink: 0;">📧</span>
<span style="color: #6b7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">@_envelopeReceiver.Envelope.Message</span>
</div>
}
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage))
{
<div style="flex: 1; min-width: 0; padding: 0.2rem 0.4rem; background: #fef3c7; border-radius: 0.25rem; border-left: 2px solid #f59e0b; display: flex; align-items: flex-start; gap: 0.25rem;">
<span style="font-weight: 500; color: #92400e; flex-shrink: 0;">🔒</span>
<span style="color: #92400e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">@_envelopeReceiver.PrivateMessage</span>
</div>
}
</div>
}
</div> </div>
</div> </div>
@@ -171,7 +68,7 @@
<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" /> <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> </svg>
<div> <div>
<h5 class="mb-2">Fehler beim Laden des Dokuments</h5> <h5 class="mb-2">Fehler</h5>
<p class="mb-0">@_errorMessage</p> <p class="mb-0">@_errorMessage</p>
</div> </div>
</div> </div>
@@ -180,212 +77,24 @@
} }
else if (_report is not null) else if (_report is not null)
{ {
<DxReportViewer @ref="_reportViewer" <DxReportViewer Report="_report" RootCssClasses="w-100 h-100" />
Report="_report"
RootCssClasses="w-100 h-100" />
} }
</div> </div>
</div> </div>
@* Signature Popup *@
<DxPopup @bind-Visible="_signaturePopupVisible"
HeaderText="Unterschrift erstellen"
Width="620px"
MaxWidth="95vw"
ShowFooter="true"
CloseOnOutsideClick="false"
ShowCloseButton="false"
CloseOnEscape="false"
Shown="OnPopupShownAsync">
<BodyContentTemplate>
<ul class="nav nav-tabs mb-3" style="border-bottom: 2px solid #e9ecef;">
<li class="nav-item">
<button type="button"
class="nav-link @(_activeSignatureTab == SignatureTabDraw ? "active" : "")"
style="@(_activeSignatureTab == SignatureTabDraw ? "border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;" : "color: #6c757d;")"
@onclick="() => SetSignatureTabAsync(SignatureTabDraw)">
Zeichnen
</button>
</li>
<li class="nav-item">
<button type="button"
class="nav-link @(_activeSignatureTab == SignatureTabText ? "active" : "")"
style="@(_activeSignatureTab == SignatureTabText ? "border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;" : "color: #6c757d;")"
@onclick="() => SetSignatureTabAsync(SignatureTabText)">
Text
</button>
</li>
<li class="nav-item">
<button type="button"
class="nav-link @(_activeSignatureTab == SignatureTabImage ? "active" : "")"
style="@(_activeSignatureTab == SignatureTabImage ? "border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;" : "color: #6c757d;")"
@onclick="() => SetSignatureTabAsync(SignatureTabImage)">
Bild
</button>
</li>
</ul>
@if (_activeSignatureTab == SignatureTabDraw)
{
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.75rem;">Bitte unterschreiben Sie im folgenden Feld.</p>
<canvas id="rp-signature-pad"
width="560"
height="180"
style="border: 2px solid #e9ecef; border-radius: 8px; background: white; width: 100%; max-width: 560px; touch-action: none; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"></canvas>
}
else if (_activeSignatureTab == SignatureTabText)
{
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.75rem;">Geben Sie Ihre Unterschrift als Text ein und wählen Sie eine Schriftart.</p>
<div class="row g-3 mb-3">
<div class="col-12 col-md-7">
<input class="form-control"
placeholder="Ihre Unterschrift"
value="@_typedSignatureText"
@oninput="OnTypedSignatureChanged"
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
</div>
<div class="col-12 col-md-5">
<select class="form-select"
value="@_typedSignatureFont"
@onchange="OnTypedSignatureFontChanged"
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;">
@foreach (var font in TypedSignatureFonts)
{
<option value="@font.Value" style="font-family: @font.Value">@font.Text</option>
}
</select>
</div>
</div>
<canvas id="rp-typed-signature-pad"
width="560"
height="180"
style="border: 2px solid #e9ecef; border-radius: 8px; background: white; width: 100%; max-width: 560px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"></canvas>
}
else
{
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.75rem;">Laden Sie ein Bild Ihrer Unterschrift hoch.</p>
<input id="rp-signature-image-input"
class="form-control mb-3"
type="file"
accept="image/png,image/jpeg,image/webp"
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
<canvas id="rp-image-signature-pad"
width="560"
height="180"
style="border: 2px solid #e9ecef; border-radius: 8px; background: white; width: 100%; max-width: 560px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"></canvas>
}
<div style="border-top: 2px solid #e9ecef; margin-top: 1.5rem; padding-top: 1.5rem;">
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label" for="rp-signer-name" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
Vor- und Nachname <span style="color: #dc3545;">*</span>
</label>
<input id="rp-signer-name"
class="form-control"
value="@_signerFullName"
@oninput="args => _signerFullName = args.Value?.ToString() ?? string.Empty"
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
</div>
<div class="col-12 col-md-6">
<label class="form-label" for="rp-signer-position" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
Position <span style="color: #6c757d; font-weight: 400;">(optional)</span>
</label>
<input id="rp-signer-position"
class="form-control"
value="@_signerPosition"
@oninput="args => _signerPosition = args.Value?.ToString() ?? string.Empty"
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
</div>
<div class="col-12 col-md-6">
<label class="form-label" for="rp-signature-place" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
Ort <span style="color: #dc3545;">*</span>
</label>
<input id="rp-signature-place"
class="form-control"
value="@_signaturePlace"
@oninput="args => _signaturePlace = args.Value?.ToString() ?? string.Empty"
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
</div>
</div>
</div>
@if (!string.IsNullOrWhiteSpace(_popupValidationMessage))
{
<div style="background: #fee; border-left: 4px solid #dc3545; padding: 0.75rem 1rem; margin-top: 1rem; border-radius: 4px;">
<span style="color: #dc3545; font-size: 0.875rem; font-weight: 500;">@_popupValidationMessage</span>
</div>
}
</BodyContentTemplate>
<FooterContentTemplate>
<div class="d-flex gap-2 justify-content-between w-100" style="padding: 0.5rem 0;">
<button class="btn btn-outline-secondary"
@onclick="RenewSignatureAsync"
style="border-radius: 6px; padding: 0.625rem 1.25rem; font-weight: 500;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
</svg>
Erneuern
</button>
<button class="btn btn-primary"
@onclick="SaveSignatureAsync"
style="background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%); border: none; border-radius: 6px; padding: 0.625rem 2rem; font-weight: 600; box-shadow: 0 2px 4px rgba(79, 70, 229, 0.3);">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
</svg>
Speichern
</button>
</div>
</FooterContentTemplate>
</DxPopup>
@code { @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; } [Parameter] public string? EnvelopeKey { get; set; }
// ----- Page state ----- [SupplyParameterFromQuery(Name = "sid")]
public string? Sid { get; set; }
bool _isLoading = true; bool _isLoading = true;
string? _errorMessage; string? _errorMessage;
byte[]? _pdfBytes;
IReadOnlyList<SignatureDto> _signatures = [];
EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver;
ClaimsPrincipal? _receiverUser; ClaimsPrincipal? _receiverUser;
EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver;
// ----- Report viewer -----
DxReportViewer? _reportViewer;
XtraReport? _report; 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() protected override async Task OnInitializedAsync()
{ {
if (string.IsNullOrWhiteSpace(EnvelopeKey)) if (string.IsNullOrWhiteSpace(EnvelopeKey))
@@ -395,7 +104,6 @@
return; return;
} }
// Authorization — same pattern as EnvelopeReceiverPage
_receiverUser = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey); _receiverUser = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey);
if (_receiverUser is null) if (_receiverUser is null)
{ {
@@ -403,281 +111,67 @@
return; return;
} }
// Read signature from IMemoryCache
if (!string.IsNullOrWhiteSpace(Sid)
&& MemoryCache.TryGetValue(Sid, out SignatureCaptureDto? cached)
&& cached is not null)
{
_sig = cached;
}
try try
{ {
// Load PDF bytes via MediatR (uses authenticated user's claims) var pdfBytes = await PageDataService.GetDocumentAsync(_receiverUser);
_pdfBytes = await PageDataService.GetDocumentAsync(_receiverUser); if (pdfBytes is not { Length: > 0 })
if (_pdfBytes is not { Length: > 0 })
{ {
_errorMessage = "Dokument konnte nicht geladen werden: Keine Daten empfangen."; _errorMessage = "Dokument konnte nicht geladen werden.";
_isLoading = false; _isLoading = false;
return; return;
} }
// Load signature fields for this receiver
_signatures = await PageDataService.GetSignaturesAsync(_receiverUser);
// Load envelope receiver metadata
_envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey); _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) var report = new XtraReport
_report = BuildReport(_pdfBytes, _signatures, capturedSignature: null);
// Try to restore cached signature
try
{ {
var cachedSignature = await PageDataService.GetCachedSignatureAsync(_receiverUser); PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4,
if (cachedSignature is not null) Landscape = false,
{ Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0),
_capturedSignature = cachedSignature; };
_signerFullName = cachedSignature.FullName; var detail = new DetailBand();
_signerPosition = cachedSignature.Position; report.Bands.Add(detail);
_signaturePlace = cachedSignature.Place; detail.Controls.Add(new XRPdfContent
_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); Source = pdfBytes,
_activeSignatureTab = SignatureTabDraw; GenerateOwnPages = true,
_signaturePopupVisible = _signatures.Count > 0; });
_popupValidationMessage = null; _report = report;
}
} }
catch (Exception ex) catch (Exception ex)
{ {
_errorMessage = $"Fehler beim Laden des Dokuments: {ex.Message}"; _errorMessage = $"Fehler: {ex.Message}";
Logger.LogError(ex, "Unexpected error for {EnvelopeKey}", EnvelopeKey); Logger.LogError(ex, "Error loading signed page for {EnvelopeKey}", EnvelopeKey);
} }
_isLoading = false; _isLoading = false;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
// ----- Report builder ----- protected override async Task OnAfterRenderAsync(bool firstRender)
/// <summary>
/// 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.
/// </summary>
static XtraReport BuildReport(
byte[] pdfBytes,
IReadOnlyList<SignatureDto> signatures,
SignatureCaptureDto? capturedSignature)
{ {
// Burn signatures into PDF bytes when a captured signature is available if (!firstRender) return;
byte[] sourcePdf = pdfBytes;
if (capturedSignature is not null if (_sig is not null)
&& !string.IsNullOrWhiteSpace(capturedSignature.DataUrl)
&& signatures.Count > 0)
{ {
sourcePdf = BurnSignaturesIntoPdf(pdfBytes, signatures, capturedSignature); await JSRuntime.InvokeVoidAsync("console.log",
} $"[SignedPage] sid={Sid} | FullName={_sig.FullName} | Place={_sig.Place} | Position={_sig.Position} | DataUrl.Length={_sig.DataUrl?.Length ?? 0}");
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;
}
/// <summary>
/// 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).
/// </summary>
static byte[] BurnSignaturesIntoPdf(
byte[] pdfBytes,
IReadOnlyList<SignatureDto> 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;
}
/// <summary>Converts a base64 data URL (data:image/...;base64,...) to raw bytes.</summary>
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 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<string?> GetActiveSignatureDataUrlAsync()
{
if (_activeSignatureTab == SignatureTabDraw)
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getDataUrl", DrawCanvasId);
if (_activeSignatureTab == SignatureTabText)
{
await RenderTypedSignatureAsync();
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getTypedDataUrl", TypedCanvasId);
}
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getImageDataUrl", ImageCanvasId);
}
// ----- Disposal -----
public void Dispose() public void Dispose()
{ {
_report?.Dispose(); _report?.Dispose();