Updated the method name in EnvelopeReceiverPageDataService to clarify its purpose of retrieving documents for a receiver. Refactored all references in Razor pages (ReceiverPage.razor, ReceiverSignedPage.razor, EnvelopeReceiverPage.razor) to use the new method name. No changes were made to the method's logic or functionality. This improves code clarity and consistency.
397 lines
17 KiB
Plaintext
397 lines
17 KiB
Plaintext
@page "/envelope/{EnvelopeKey}/signed"
|
|
@rendermode InteractiveServer
|
|
@using DevExpress.Blazor.Reporting
|
|
@using DevExpress.XtraReports.UI
|
|
@using EnvelopeGenerator.Server.Client.Models
|
|
@using EnvelopeGenerator.Server.Client.Services
|
|
@using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver
|
|
@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 IMemoryCache MemoryCache
|
|
@inject ILogger<ReceiverSignedPage> Logger
|
|
@implements IDisposable
|
|
|
|
<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="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
|
|
|
<div class="envelope-viewer-layout">
|
|
<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 style="display: flex; align-items: center; justify-content: space-between; gap: 1rem;">
|
|
<div style="flex: 0 1 auto; min-width: 0; display: flex; align-items: center; gap: 0.75rem;">
|
|
@if (_envelopeReceiver is not null)
|
|
{
|
|
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
|
@(_envelopeReceiver.Envelope?.Title ?? "Dokument")
|
|
</div>
|
|
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName))
|
|
{
|
|
<span style="font-size: 0.7rem; color: #6b7280; white-space: nowrap;">
|
|
Von <span style="font-weight: 500; color: #374151;">@_envelopeReceiver.Envelope.User.FullName</span>
|
|
</span>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937;">Signiertes Dokument</div>
|
|
}
|
|
</div>
|
|
|
|
@* Right: Submit button *@
|
|
<div style="flex: 0 0 auto;">
|
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change pdf-toolbar__btn--signature-change-active"
|
|
@onclick="OpenSubmitConfirmPopup"
|
|
disabled="@_isLoggingOut"
|
|
title="Abschließen"
|
|
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="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>
|
|
<span class="pdf-toolbar__btn-text">Abschließen</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="envelope-content" style="padding: 0; overflow: hidden;">
|
|
@if (_isLoading)
|
|
{
|
|
<div class="d-flex justify-content-center align-items-center h-100">
|
|
<div class="text-center">
|
|
<div class="spinner-border text-white mb-3" style="width: 3.5rem; height: 3.5rem;" role="status">
|
|
<span class="visually-hidden">Lädt...</span>
|
|
</div>
|
|
<p class="text-white fw-semibold">Dokument wird geladen...</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
else if (_errorMessage is not null)
|
|
{
|
|
<div class="error-container">
|
|
<div class="alert alert-danger shadow-lg">
|
|
<div class="d-flex align-items-start">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3 flex-shrink-0" 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>
|
|
<h5 class="mb-2">Fehler</h5>
|
|
<p class="mb-0">@_errorMessage</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
else if (_report is not null)
|
|
{
|
|
<DxReportViewer Report="_report" RootCssClasses="w-100 h-100" />
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@* Submit confirmation popup *@
|
|
<DxPopup @bind-Visible="_submitConfirmVisible"
|
|
HeaderText="Unterschrift bestätigen"
|
|
Width="440px"
|
|
MaxWidth="95vw"
|
|
ShowFooter="true"
|
|
CloseOnOutsideClick="false"
|
|
ShowCloseButton="false"
|
|
CloseOnEscape="false">
|
|
<BodyContentTemplate>
|
|
<div style="display: flex; align-items: flex-start; gap: 1rem; padding: 0.5rem 0;">
|
|
<div style="flex-shrink: 0; width: 40px; height: 40px; background: #d1fae5; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#065f46" 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>
|
|
</div>
|
|
<div>
|
|
<p style="margin: 0 0 0.4rem; font-weight: 600; color: #1f2937; font-size: 0.95rem;">
|
|
Möchten Sie das Dokument verbindlich unterschreiben?
|
|
</p>
|
|
<p style="margin: 0; color: #6b7280; font-size: 0.85rem; line-height: 1.5;">
|
|
Diese Aktion kann nicht rückgängig gemacht werden. Mit der Bestätigung erklären Sie, das oben angezeigte Dokument elektronisch unterzeichnet zu haben. Das unterzeichnete Dokument wird anschließend an alle beteiligten Parteien übermittelt.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</BodyContentTemplate>
|
|
<FooterContentTemplate>
|
|
<div class="d-flex gap-2 justify-content-end w-100" style="padding: 0.5rem 0;">
|
|
<button class="btn btn-outline-secondary"
|
|
@onclick="() => _submitConfirmVisible = false"
|
|
style="border-radius: 6px; padding: 0.5rem 1.25rem; font-weight: 500;">
|
|
Abbrechen
|
|
</button>
|
|
<button class="btn btn-primary"
|
|
@onclick="SubmitAndLogoutAsync"
|
|
disabled="@_isLoggingOut"
|
|
style="background: linear-gradient(135deg, #059669 0%, #047857 100%); border: none; border-radius: 6px; padding: 0.5rem 1.5rem; font-weight: 600; box-shadow: 0 2px 4px rgba(5, 150, 105, 0.3);">
|
|
@if (_isLoggingOut)
|
|
{
|
|
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
|
|
}
|
|
Abschließen
|
|
</button>
|
|
</div>
|
|
</FooterContentTemplate>
|
|
</DxPopup>
|
|
|
|
@code {
|
|
[Parameter] public string? EnvelopeKey { get; set; }
|
|
|
|
[SupplyParameterFromQuery(Name = "sid")]
|
|
public string? Sid { get; set; }
|
|
|
|
bool _isLoading = true;
|
|
string? _errorMessage;
|
|
ClaimsPrincipal? _receiverUser;
|
|
EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver;
|
|
IReadOnlyList<SignatureDto> _signatures = [];
|
|
XtraReport? _report;
|
|
SignatureCaptureDto? _sig;
|
|
|
|
// ----- Submit / logout state -----
|
|
bool _isLoggingOut = false;
|
|
bool _submitConfirmVisible = false;
|
|
|
|
void OpenSubmitConfirmPopup() => _submitConfirmVisible = true;
|
|
|
|
async Task SubmitAndLogoutAsync()
|
|
{
|
|
if (_isLoggingOut) return;
|
|
_isLoggingOut = true;
|
|
_submitConfirmVisible = false;
|
|
await InvokeAsync(StateHasChanged);
|
|
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey!);
|
|
Navigation.NavigateTo("/", forceLoad: true);
|
|
}
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(EnvelopeKey))
|
|
{
|
|
_errorMessage = "Envelope-Schlüssel fehlt.";
|
|
_isLoading = false;
|
|
return;
|
|
}
|
|
|
|
_receiverUser = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey);
|
|
if (_receiverUser is null)
|
|
{
|
|
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
|
return;
|
|
}
|
|
|
|
// Read signature from IMemoryCache
|
|
if (!string.IsNullOrWhiteSpace(Sid)
|
|
&& MemoryCache.TryGetValue(Sid, out SignatureCaptureDto? cached)
|
|
&& cached is not null)
|
|
{
|
|
_sig = cached;
|
|
}
|
|
|
|
// Cache miss or missing sid — redirect back to report page
|
|
if (_sig is null)
|
|
{
|
|
Logger.LogWarning(
|
|
"[SignedPage] Cache miss or no sid={Sid} for {EnvelopeKey}, redirecting to report page.",
|
|
Sid, EnvelopeKey);
|
|
Navigation.NavigateTo(
|
|
$"/envelope/{Uri.EscapeDataString(EnvelopeKey)}",
|
|
forceLoad: true);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var pdfBytes = await PageDataService.GetDocumentOfReceiverAsync(_receiverUser);
|
|
if (pdfBytes is not { Length: > 0 })
|
|
{
|
|
_errorMessage = "Dokument konnte nicht geladen werden.";
|
|
_isLoading = false;
|
|
return;
|
|
}
|
|
|
|
_envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey);
|
|
_signatures = await PageDataService.GetSignaturesAsync(_receiverUser);
|
|
|
|
// Burn signature image + info onto PDF via PdfSharp
|
|
if (_sig is not null && _signatures.Count > 0)
|
|
pdfBytes = DrawSignaturesOnPdf(pdfBytes, _signatures, _sig);
|
|
|
|
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();
|
|
report.Bands.Add(detail);
|
|
detail.Controls.Add(new XRPdfContent
|
|
{
|
|
Source = pdfBytes,
|
|
GenerateOwnPages = true,
|
|
});
|
|
_report = report;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_errorMessage = $"Fehler: {ex.Message}";
|
|
Logger.LogError(ex, "Error loading signed page for {EnvelopeKey}", EnvelopeKey);
|
|
}
|
|
|
|
_isLoading = false;
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (!firstRender) return;
|
|
|
|
if (_sig is not null)
|
|
{
|
|
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("console.log",
|
|
$"[SignedPage] Cache miss or no sid. sid={Sid}");
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_report?.Dispose();
|
|
}
|
|
|
|
// ----- PDF signature rendering -----
|
|
|
|
/// <summary>
|
|
/// Uses PdfSharp to burn the captured signature onto the PDF at each signature field.
|
|
/// Layout per field (top-left origin, Y down, units = PDF points):
|
|
/// [top 65%] signature image
|
|
/// [separator line]
|
|
/// [bottom 35%] FullName (bold) / Position (optional) / Place, Date
|
|
/// </summary>
|
|
static byte[] DrawSignaturesOnPdf(
|
|
byte[] pdfBytes,
|
|
IReadOnlyList<SignatureDto> signatures,
|
|
SignatureCaptureDto sig)
|
|
{
|
|
var imgBytes = DataUrlToBytes(sig.DataUrl);
|
|
if (imgBytes is not { Length: > 0 }) return pdfBytes;
|
|
|
|
using var inputMs = new System.IO.MemoryStream(pdfBytes);
|
|
using var outputMs = new System.IO.MemoryStream();
|
|
|
|
var document = PdfSharp.Pdf.IO.PdfReader.Open(
|
|
inputMs, PdfSharp.Pdf.IO.PdfDocumentOpenMode.Modify);
|
|
|
|
const double sigW = 1.77 * 72; // 127.44 pt
|
|
const double sigH = 1.96 * 72; // 141.12 pt
|
|
const double imgRatio = 0.52; // top 52% = image
|
|
const double lineH = 11.5; // fixed row height matching font size (bold 7.5pt + normal 6.5pt)
|
|
const double bgPad = 3.0; // background box padding around content (pt)
|
|
|
|
var black = PdfSharp.Drawing.XColor.FromArgb(255, 20, 20, 20);
|
|
var darkGray = PdfSharp.Drawing.XColor.FromArgb(255, 80, 80, 80);
|
|
var lineColor = PdfSharp.Drawing.XColor.FromArgb(180, 100, 100, 120);
|
|
|
|
var bgColor = PdfSharp.Drawing.XColor.FromArgb(255, 255, 253, 240);
|
|
var bgBrush = new PdfSharp.Drawing.XSolidBrush(bgColor);
|
|
|
|
var fontBold = new PdfSharp.Drawing.XFont("Arial", 7.5, PdfSharp.Drawing.XFontStyleEx.Bold);
|
|
var fontNormal = new PdfSharp.Drawing.XFont("Arial", 6.5, PdfSharp.Drawing.XFontStyleEx.Regular);
|
|
var linePen = new PdfSharp.Drawing.XPen(lineColor, 0.5);
|
|
|
|
var fmtLeft = new PdfSharp.Drawing.XStringFormat
|
|
{
|
|
Alignment = PdfSharp.Drawing.XStringAlignment.Near,
|
|
LineAlignment = PdfSharp.Drawing.XLineAlignment.Near,
|
|
};
|
|
|
|
var date = DateTime.Now.ToString("dd.MM.yyyy");
|
|
|
|
foreach (var field in signatures)
|
|
{
|
|
int pageIndex = field.Page - 1;
|
|
if (pageIndex < 0 || pageIndex >= document.PageCount) continue;
|
|
|
|
var page = document.Pages[pageIndex];
|
|
|
|
using var gfx = PdfSharp.Drawing.XGraphics.FromPdfPage(page);
|
|
|
|
double x = field.X;
|
|
double y = field.Y;
|
|
|
|
// --- Calculate layout positions first (needed for bg rect) ---
|
|
double imgH = sigH * imgRatio;
|
|
double lineY = y + imgH + 1.0; // 1pt gap between image and separator
|
|
double textY = lineY + 1.5; // 1.5pt gap below separator line
|
|
double padding = 3;
|
|
|
|
// Row 1: FullName
|
|
double row1Y = textY;
|
|
// Row 2: Position (optional)
|
|
double row2Y = row1Y + lineH;
|
|
// Row 3: Place, Date — immediately after row2 regardless of position
|
|
double row3Y = !string.IsNullOrWhiteSpace(sig.Position) ? row2Y + lineH : row2Y;
|
|
double contentBottom = row3Y + lineH;
|
|
|
|
// --- Background rectangle sized to actual content (not full sigH) ---
|
|
var bgRect = new PdfSharp.Drawing.XRect(
|
|
x - bgPad,
|
|
y - bgPad,
|
|
sigW + bgPad * 2,
|
|
(contentBottom - y) + bgPad * 2);
|
|
gfx.DrawRectangle(bgBrush, bgRect);
|
|
|
|
// --- Image area ---
|
|
var imgRect = new PdfSharp.Drawing.XRect(x, y, sigW, imgH);
|
|
using var imgStream = new System.IO.MemoryStream(imgBytes);
|
|
var xImg = PdfSharp.Drawing.XImage.FromStream(imgStream);
|
|
gfx.DrawImage(xImg, imgRect);
|
|
|
|
// --- Separator line ---
|
|
gfx.DrawLine(linePen, x + 2, lineY, x + sigW - 2, lineY);
|
|
|
|
// --- Text rows ---
|
|
// Row 1: FullName (bold)
|
|
var nameRect = new PdfSharp.Drawing.XRect(x + padding, row1Y, sigW - padding * 2, lineH);
|
|
gfx.DrawString(sig.FullName, fontBold, new PdfSharp.Drawing.XSolidBrush(black), nameRect, fmtLeft);
|
|
|
|
// Row 2: Position (optional)
|
|
if (!string.IsNullOrWhiteSpace(sig.Position))
|
|
{
|
|
var posRect = new PdfSharp.Drawing.XRect(x + padding, row2Y, sigW - padding * 2, lineH);
|
|
gfx.DrawString(sig.Position, fontNormal, new PdfSharp.Drawing.XSolidBrush(darkGray), posRect, fmtLeft);
|
|
}
|
|
|
|
// Row 3: Place, Date
|
|
var placeDate = $"{sig.Place}, {date}";
|
|
var dateRect = new PdfSharp.Drawing.XRect(x + padding, row3Y, sigW - padding * 2, lineH);
|
|
gfx.DrawString(placeDate, fontNormal, new PdfSharp.Drawing.XSolidBrush(darkGray), dateRect, fmtLeft);
|
|
}
|
|
|
|
document.Save(outputMs);
|
|
return outputMs.ToArray();
|
|
}
|
|
|
|
static byte[]? DataUrlToBytes(string? dataUrl)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(dataUrl)) return null;
|
|
var comma = dataUrl.IndexOf(',');
|
|
if (comma < 0) return null;
|
|
return Convert.FromBase64String(dataUrl[(comma + 1)..]);
|
|
}
|
|
}
|