Enhanced `ReportViewer.razor` with new UI elements, including a detailed `receiver-info-header` and `receiver-action-bar` for better signature workflows. Refactored signature logic to ensure accurate placement using `report.AfterPrint` and `PrintingSystem.Pages`. Removed legacy methods and iText7-based workflows. Added Turkish documentation (`COPILOT_CONTEXT_TR.md`) detailing project structure, workflows, and pending tasks. Updated `MainLayout.razor` to simplify layout. Improved styling in `app.css` for better visual hierarchy and readability. Documented pending tasks such as adding signature backgrounds, improving checkbox styles, and automating signature workflows.
725 lines
35 KiB
Plaintext
725 lines
35 KiB
Plaintext
@page "/receiver"
|
||
@page "/receiver/{EnvelopeKey}"
|
||
@using System.Drawing
|
||
@using DevExpress.Blazor
|
||
@using DevExpress.Drawing
|
||
@using DevExpress.Utils
|
||
@using DevExpress.XtraPrinting
|
||
@using DevExpress.XtraPrinting.Drawing
|
||
@using Microsoft.JSInterop
|
||
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
|
||
@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand
|
||
@using XRLabel = DevExpress.XtraReports.UI.XRLabel
|
||
@using XRPictureBox = DevExpress.XtraReports.UI.XRPictureBox
|
||
@using XRControl = DevExpress.XtraReports.UI.XRControl
|
||
@using ImageSizeMode = DevExpress.XtraPrinting.ImageSizeMode
|
||
@using EnvelopeGenerator.ReceiverUI.Services
|
||
@using DevExpress.Blazor.Reporting
|
||
@using Microsoft.Extensions.Options
|
||
@using EnvelopeGenerator.ReceiverUI.Options
|
||
@using EnvelopeGenerator.ReceiverUI.Models
|
||
@implements IDisposable
|
||
@inject IJSRuntime JSRuntime
|
||
@inject AnnotationService AnnotationService
|
||
@inject IOptions<ApiOptions> AppOptions
|
||
@inject NavigationManager Navigation
|
||
@inject InMemoryReportStorageWebExtension ReportStorage
|
||
@inject EnvelopeGenerator.ReceiverUI.Services.DocumentService DocumentService
|
||
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
|
||
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService
|
||
|
||
<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" />
|
||
|
||
<div class="receiver-page-layout">
|
||
|
||
<div class="receiver-signature-panel">
|
||
|
||
@* ?? Envelope info header ???????????????????????????????????????????????? *@
|
||
@if (_envelopeReceiver is not null) {
|
||
<div class="receiver-info-header">
|
||
<div class="receiver-info-header__gradient">
|
||
<div class="receiver-info-header__left">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" class="receiver-info-header__icon" viewBox="0 0 16 16">
|
||
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
|
||
</svg>
|
||
<div>
|
||
<div class="receiver-info-header__title">@(_envelopeReceiver.Envelope?.Title ?? "Dokument")</div>
|
||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName) || !string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) {
|
||
<div class="receiver-info-header__sender">
|
||
Von
|
||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName)) {
|
||
<strong>@_envelopeReceiver.Envelope!.User!.FullName</strong>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) {
|
||
<span class="opacity-75"><@_envelopeReceiver.Envelope!.User!.Email></span>
|
||
}
|
||
· @_envelopeReceiver.Envelope?.AddedWhen.ToString("dd.MM.yyyy")
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
<div class="receiver-info-header__badges">
|
||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Name)) {
|
||
<span class="receiver-info-badge">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" 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 (!string.IsNullOrWhiteSpace(_envelopeReceiver.CompanyName)) {
|
||
<span class="receiver-info-badge">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path d="M14.763.075A.5.5 0 0 1 15 .5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5V14h-1v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V10a.5.5 0 0 1 .342-.474L6 7.64V4.5a.5.5 0 0 1 .276-.447l8-4a.5.5 0 0 1 .487.022Z"/>
|
||
</svg>
|
||
@_envelopeReceiver.CompanyName
|
||
</span>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.JobTitle)) {
|
||
<span class="receiver-info-badge receiver-info-badge--muted">@_envelopeReceiver.JobTitle</span>
|
||
}
|
||
@{
|
||
var docElements = _envelopeReceiver.Envelope?.Documents?.FirstOrDefault()?.Elements;
|
||
int sigCount = docElements?.Count() ?? _annotations.Count;
|
||
}
|
||
@if (sigCount > 0) {
|
||
<span class="receiver-info-badge receiver-info-badge--accent">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" 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>
|
||
@sigCount @(sigCount == 1 ? "Unterschriftsfeld" : "Unterschriftsfelder")
|
||
</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message)) {
|
||
<div class="receiver-info-message">@_envelopeReceiver.Envelope!.Message</div>
|
||
}
|
||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage)) {
|
||
<div class="receiver-info-private-message">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1 flex-shrink-0" 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>
|
||
@_envelopeReceiver.PrivateMessage
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
@* ?? Signature action bar ???????????????????????????????????????????????? *@
|
||
<div class="receiver-action-bar">
|
||
<div class="receiver-action-bar__inner">
|
||
<button class="btn btn-sm @(_capturedSignature is not null ? "btn-outline-success" : "btn-primary")" @onclick="OpenSignaturePopupAsync">
|
||
@if (_capturedSignature is not null) {
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" 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>
|
||
<span>Unterschrift gespeichert</span>
|
||
} else {
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" 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>
|
||
<span>Unterschrift erstellen</span>
|
||
}
|
||
</button>
|
||
|
||
@if (_annotations.Count > 0 && !SignatureApplied) {
|
||
<div class="receiver-action-bar__progress">
|
||
<div class="progress" style="height:5px; min-width:70px; width:90px;">
|
||
<div class="progress-bar @(_checkedAnnotations.Count == _annotations.Count ? "bg-success" : "bg-primary")"
|
||
style="width:@(_annotations.Count > 0 ? (_checkedAnnotations.Count * 100 / _annotations.Count) : 0)%"
|
||
role="progressbar"></div>
|
||
</div>
|
||
<span class="text-muted" style="font-size:0.75rem;">
|
||
@_checkedAnnotations.Count / @_annotations.Count
|
||
Seite@(AnnotationPages.Count() == 1 ? "" : "n") @string.Join(",", AnnotationPages)
|
||
</span>
|
||
@if (_capturedSignature is null) {
|
||
<span class="text-muted fst-italic" style="font-size:0.72rem;">Zuerst Unterschrift erstellen</span>
|
||
} else if (_checkedAnnotations.Count == _annotations.Count) {
|
||
<span class="text-success fw-semibold" style="font-size:0.72rem;">✓ Wird angewendet…</span>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
@if (!string.IsNullOrWhiteSpace(SignatureValidationMessage)) {
|
||
<span class="text-danger" style="font-size:0.78rem;">@SignatureValidationMessage</span>
|
||
}
|
||
@if (SignatureApplied) {
|
||
<span class="text-success" style="font-size:0.78rem;">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
|
||
</svg>
|
||
Unterschrift angewendet
|
||
</span>
|
||
}
|
||
|
||
<div class="ms-auto d-flex align-items-center gap-2">
|
||
<button class="btn btn-sm btn-success" disabled="@(!SignatureApplied)" @onclick="ExportSignedPdfAsync">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||
</svg>
|
||
PDF exportieren
|
||
</button>
|
||
@if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||
<button class="btn btn-sm btn-outline-danger" @onclick="LogoutAsync" disabled="@IsLoggingOut" title="Abmelden">
|
||
@if (IsLoggingOut) {
|
||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||
} else {
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
|
||
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
|
||
</svg>
|
||
}
|
||
</button>
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<DxPopup @bind-Visible="SignaturePopupVisible"
|
||
HeaderText="Unterschrift erfassen"
|
||
Width="620px"
|
||
ShowFooter="true"
|
||
CloseOnEscape="false"
|
||
ShowCloseButton="false"
|
||
CloseOnOutsideClick="false"
|
||
Shown="OnPopupShownAsync">
|
||
<BodyContentTemplate>
|
||
<ul class="nav nav-tabs mb-3">
|
||
<li class="nav-item">
|
||
<button type="button" class="nav-link @(ActiveSignatureTab == SignatureTabDraw ? "active" : null)" @onclick="() => SetSignatureTabAsync(SignatureTabDraw)">Zeichnen</button>
|
||
</li>
|
||
<li class="nav-item">
|
||
<button type="button" class="nav-link @(ActiveSignatureTab == SignatureTabText ? "active" : null)" @onclick="() => SetSignatureTabAsync(SignatureTabText)">Text</button>
|
||
</li>
|
||
<li class="nav-item">
|
||
<button type="button" class="nav-link @(ActiveSignatureTab == SignatureTabImage ? "active" : null)" @onclick="() => SetSignatureTabAsync(SignatureTabImage)">Bild</button>
|
||
</li>
|
||
</ul>
|
||
|
||
@if(ActiveSignatureTab == SignatureTabDraw) {
|
||
<p class="text-muted mb-2">Bitte unterschreiben Sie im folgenden Feld.</p>
|
||
<canvas id="receiver-signature-pad" width="520" height="160" class="border rounded bg-white w-100" style="max-width: 520px; touch-action: none;"></canvas>
|
||
} else if(ActiveSignatureTab == SignatureTabText) {
|
||
<p class="text-muted mb-2">Geben Sie Ihre Unterschrift als Text ein und waehlen Sie eine Schriftart.</p>
|
||
<div class="row g-2 mb-2">
|
||
<div class="col-12 col-md-7">
|
||
<input class="form-control" placeholder="Ihre Unterschrift" value="@TypedSignatureText" @oninput="OnTypedSignatureChanged" />
|
||
</div>
|
||
<div class="col-12 col-md-5">
|
||
<select class="form-select" value="@TypedSignatureFont" @onchange="OnTypedSignatureFontChanged">
|
||
@foreach(var font in TypedSignatureFonts) {
|
||
<option value="@font.Value" style="font-family: @font.Value">@font.Text</option>
|
||
}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<canvas id="receiver-typed-signature-pad" width="520" height="160" class="border rounded bg-white w-100" style="max-width: 520px;"></canvas>
|
||
} else {
|
||
<p class="text-muted mb-2">Laden Sie ein Bild Ihrer Unterschrift hoch.</p>
|
||
<input id="receiver-signature-image-input" class="form-control mb-2" type="file" accept="image/png,image/jpeg,image/webp" />
|
||
<canvas id="receiver-image-signature-pad" width="520" height="160" class="border rounded bg-white w-100" style="max-width: 520px;"></canvas>
|
||
}
|
||
|
||
|
||
<div class="border-top mt-3 pt-3">
|
||
<p class="text-muted mb-2">Bitte geben Sie die folgenden Angaben ein. Das Datum wird automatisch hinzugefuegt.</p>
|
||
<div class="row g-2">
|
||
<div class="col-12 col-md-6">
|
||
<label class="form-label" for="receiver-signer-name">Vor- und Nachname *</label>
|
||
<input id="receiver-signer-name" class="form-control" value="@SignerFullName" @oninput="args => SignerFullName = args.Value?.ToString() ?? string.Empty" />
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<label class="form-label" for="receiver-signer-position">Position</label>
|
||
<input id="receiver-signer-position" class="form-control" value="@SignerPosition" @oninput="args => SignerPosition = args.Value?.ToString() ?? string.Empty" />
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<label class="form-label" for="receiver-signature-place">Ort *</label>
|
||
<input id="receiver-signature-place" class="form-control" value="@SignaturePlace" @oninput="args => SignaturePlace = args.Value?.ToString() ?? string.Empty" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@if(!string.IsNullOrWhiteSpace(PopupValidationMessage)) {
|
||
<div class="text-danger mt-2">@PopupValidationMessage</div>
|
||
}
|
||
</BodyContentTemplate>
|
||
<FooterContentTemplate>
|
||
<div class="d-flex gap-2 flex-wrap justify-content-end w-100">
|
||
<button class="btn btn-outline-secondary" @onclick="RenewSignatureAsync">Unterschrift erneuern</button>
|
||
<button class="btn btn-primary" @onclick="SaveSignatureAsync">Speichern</button>
|
||
</div>
|
||
</FooterContentTemplate>
|
||
</DxPopup>
|
||
|
||
<div class="receiver-viewer-wrapper">
|
||
@if(Report is not null) {
|
||
<DxReportViewer @key="ViewerKey" @ref="reportViewer" Report="Report" RootCssClasses="w-100 h-100" Zoom="1.3" />
|
||
}
|
||
</div>
|
||
|
||
</div>
|
||
|
||
@code {
|
||
|
||
const string SignatureTabDraw = "draw";
|
||
const string SignatureTabText = "text";
|
||
const string SignatureTabImage = "image";
|
||
const string DrawCanvasId = "receiver-signature-pad";
|
||
const string TypedCanvasId = "receiver-typed-signature-pad";
|
||
const string ImageInputId = "receiver-signature-image-input";
|
||
const string ImageCanvasId = "receiver-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")
|
||
};
|
||
|
||
[Parameter] public string? EnvelopeKey { get; set; }
|
||
|
||
DxReportViewer? reportViewer;
|
||
XtraReport? Report;
|
||
bool SignatureApplied;
|
||
bool SignaturePopupVisible;
|
||
string? SignatureValidationMessage;
|
||
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;
|
||
int ViewerKey;
|
||
bool IsLoggingOut;
|
||
|
||
IReadOnlyList<AnnotationDto> _annotations = [];
|
||
IEnumerable<int> AnnotationPages => _annotations.Select(a => a.Page).Distinct().OrderBy(p => p);
|
||
EnvelopeReceiverDto? _envelopeReceiver;
|
||
record SignatureCapture(string DataUrl, string FullName, string Position, string Place);
|
||
SignatureCapture? _capturedSignature;
|
||
byte[]? _basePdfBytes;
|
||
// annotation IDs the user has checked via overlay checkboxes
|
||
readonly HashSet<long> _checkedAnnotations = [];
|
||
DotNetObjectReference<ReportViewer>? _dotNetRef;
|
||
int _lastOverlayViewerKey = -1;
|
||
|
||
async Task LogoutAsync() {
|
||
if (string.IsNullOrWhiteSpace(EnvelopeKey) || IsLoggingOut) return;
|
||
IsLoggingOut = true;
|
||
await InvokeAsync(StateHasChanged);
|
||
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey);
|
||
Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||
}
|
||
|
||
protected override async Task OnInitializedAsync() {
|
||
|
||
if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||
if (!hasAccess) {
|
||
Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
ActiveSignatureTab = SignatureTabDraw;
|
||
SignaturePopupVisible = true;
|
||
SignatureValidationMessage = null;
|
||
PopupValidationMessage = null;
|
||
}
|
||
}
|
||
|
||
_annotations = await AnnotationService.GetAnnotationsAsync(EnvelopeKey ?? "fake");
|
||
_envelopeReceiver = await EnvelopeReceiverService.GetAsync(EnvelopeKey ?? "fake");
|
||
|
||
if (!AppOptions.Value.ForceToUseFakeDocument && !string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||
var (pdfBytes, _) = await DocumentService.GetDocumentAsync(EnvelopeKey);
|
||
if (pdfBytes is { Length: > 0 })
|
||
_basePdfBytes = pdfBytes;
|
||
}
|
||
|
||
var initialReport = BuildFreshBaseReport();
|
||
Report = initialReport;
|
||
}
|
||
|
||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||
if (firstRender)
|
||
_dotNetRef = DotNetObjectReference.Create(this);
|
||
|
||
|
||
if (Report is not null && _annotations.Count > 0
|
||
&& _capturedSignature is not null && !SignatureApplied
|
||
&& _lastOverlayViewerKey != ViewerKey) {
|
||
_lastOverlayViewerKey = ViewerKey;
|
||
await JSRuntime.InvokeVoidAsync(
|
||
"receiverSignature.installAnnotationCheckboxes",
|
||
_annotations, _checkedAnnotations.ToArray(), _dotNetRef);
|
||
}
|
||
}
|
||
|
||
[JSInvokable]
|
||
public async Task OnAnnotationToggled(long annotationId, bool isChecked) {
|
||
if (isChecked)
|
||
_checkedAnnotations.Add(annotationId);
|
||
else
|
||
_checkedAnnotations.Remove(annotationId);
|
||
await InvokeAsync(StateHasChanged);
|
||
|
||
if (_capturedSignature is not null
|
||
&& !SignatureApplied
|
||
&& _annotations.Count > 0
|
||
&& _checkedAnnotations.Count == _annotations.Count) {
|
||
// K?sa bekleme: kullan?c? son tick'in g<>rsel feedback'ini g<>rs<72>n
|
||
await Task.Delay(400);
|
||
await SubmitSignaturesAsync();
|
||
}
|
||
}
|
||
|
||
void OpenSignaturePopupAsync() {
|
||
ActiveSignatureTab = SignatureTabDraw;
|
||
SignaturePopupVisible = true;
|
||
SignatureValidationMessage = null;
|
||
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);
|
||
}
|
||
}
|
||
|
||
void CloseSignaturePopup() {
|
||
PopupValidationMessage = null;
|
||
SignaturePopupVisible = false;
|
||
}
|
||
|
||
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 fuer den PDF-Export erforderlich.";
|
||
return;
|
||
}
|
||
|
||
PopupValidationMessage = null;
|
||
SignatureValidationMessage = null;
|
||
_capturedSignature = new(signatureDataUrl, SignerFullName.Trim(), SignerPosition.Trim(), SignaturePlace.Trim());
|
||
|
||
// If no annotations, apply immediately (no checkbox step needed)
|
||
if (_annotations.Count == 0) {
|
||
var freshReport = BuildFreshBaseReport();
|
||
AddSignature(freshReport, _capturedSignature.DataUrl, _capturedSignature.FullName, _capturedSignature.Position, _capturedSignature.Place);
|
||
Report = freshReport;
|
||
SignatureApplied = true;
|
||
SignaturePopupVisible = false;
|
||
ViewerKey++;
|
||
return;
|
||
}
|
||
|
||
// Close popup; checkboxes will appear on the PDF via OnAfterRenderAsync
|
||
SignaturePopupVisible = false;
|
||
_lastOverlayViewerKey = -1; // force overlay reinstall
|
||
await InvokeAsync(StateHasChanged);
|
||
}
|
||
|
||
async Task SubmitSignaturesAsync() {
|
||
if (_checkedAnnotations.Count == 0) {
|
||
SignatureValidationMessage = "Bitte markieren Sie mindestens ein Unterschriftsfeld im Dokument.";
|
||
return;
|
||
}
|
||
if (_checkedAnnotations.Count < _annotations.Count) {
|
||
SignatureValidationMessage = $"Bitte markieren Sie alle {_annotations.Count} Unterschriftsfelder. Noch {_annotations.Count - _checkedAnnotations.Count} offen.";
|
||
return;
|
||
}
|
||
|
||
SignatureValidationMessage = null;
|
||
var freshReport = BuildFreshBaseReport();
|
||
foreach (var ann in _annotations)
|
||
AddSignatureAtAnnotation(freshReport, ann, _capturedSignature!.DataUrl, _capturedSignature.FullName, _capturedSignature.Position, _capturedSignature.Place);
|
||
|
||
Report = freshReport;
|
||
SignatureApplied = true;
|
||
ViewerKey++;
|
||
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);
|
||
}
|
||
|
||
async Task ExportSignedPdfAsync() {
|
||
if(!SignatureApplied || Report is null) {
|
||
SignatureValidationMessage = "Bitte fuegen Sie die Unterschrift zuerst zum Bericht hinzu.";
|
||
return;
|
||
}
|
||
|
||
try {
|
||
SignatureValidationMessage = null;
|
||
await reportViewer!.ExportToAsync(ExportFormat.Pdf);
|
||
} catch(Exception) {
|
||
SignatureValidationMessage = "Das signierte PDF konnte nicht exportiert werden. Bitte laden Sie die Seite neu und versuchen Sie es erneut.";
|
||
}
|
||
}
|
||
|
||
XtraReport BuildFreshBaseReport() {
|
||
if (_basePdfBytes is { Length: > 0 }) {
|
||
var report = new XtraReport();
|
||
var detail = new DevExpress.XtraReports.UI.DetailBand();
|
||
report.Bands.Add(detail);
|
||
detail.Controls.Add(new DevExpress.XtraReports.UI.XRPdfContent { Source = _basePdfBytes, GenerateOwnPages = true });
|
||
return report;
|
||
}
|
||
return CreateReportInstance();
|
||
}
|
||
|
||
static void AddAnnotationPlaceholders(XtraReport report, IReadOnlyList<AnnotationDto> annotations) {
|
||
var bottomMargin = report.Bands.OfType<BottomMarginBand>().FirstOrDefault();
|
||
if (bottomMargin is null) {
|
||
bottomMargin = new BottomMarginBand();
|
||
report.Bands.Add(bottomMargin);
|
||
}
|
||
|
||
const float sigWidth = 230F;
|
||
const float sigHeight = 154F;
|
||
const float bottomPad = 6F;
|
||
const float defaultTopPad = 8F;
|
||
const float maxBandHeight = 210F;
|
||
|
||
float requiredHeight = defaultTopPad + sigHeight + bottomPad;
|
||
bottomMargin.HeightF = Math.Min(maxBandHeight, Math.Max(bottomMargin.HeightF, requiredHeight));
|
||
float topPad = Math.Max(0F, bottomMargin.HeightF - bottomPad - sigHeight);
|
||
|
||
foreach (var ann in annotations) {
|
||
float sigX = (float)(ann.X);
|
||
var annotId = ann.Id.ToString();
|
||
|
||
var placeholder = new XRLabel {
|
||
Name = $"receiverSignaturePlaceholder_{annotId}",
|
||
Text = "\u270e Bitte unterschreiben",
|
||
BoundsF = new RectangleF(sigX, topPad, sigWidth, sigHeight),
|
||
Borders = BorderSide.All,
|
||
BorderColor = System.Drawing.Color.FromArgb(230, 81, 0),
|
||
BackColor = System.Drawing.Color.FromArgb(30, 255, 236, 153),
|
||
Font = new DXFont("Open Sans", 10F, DXFontStyle.Regular),
|
||
ForeColor = System.Drawing.Color.FromArgb(94, 38, 0),
|
||
TextAlignment = TextAlignment.MiddleCenter
|
||
};
|
||
|
||
bottomMargin.Controls.Add(placeholder);
|
||
}
|
||
}
|
||
|
||
XtraReport CreateReportInstance() {
|
||
return ReportStorage.TryGetReport("LargeDatasetReport", out var savedReport)
|
||
? savedReport
|
||
: PredefinedReports.ReportsFactory.GetReport("LargeDatasetReport");
|
||
}
|
||
|
||
static void AddSignature(XtraReport report, string signatureDataUrl, string signerFullName, string signerPosition, string signaturePlace) {
|
||
var imageBytes = Convert.FromBase64String(signatureDataUrl[(signatureDataUrl.IndexOf(',') + 1)..]);
|
||
using var imageStream = new MemoryStream(imageBytes);
|
||
var imageSource = new ImageSource(DXImage.FromStream(imageStream));
|
||
var bottomMargin = report.Bands.OfType<BottomMarginBand>().FirstOrDefault();
|
||
|
||
if(bottomMargin is null) {
|
||
bottomMargin = new BottomMarginBand();
|
||
report.Bands.Add(bottomMargin);
|
||
}
|
||
|
||
RemoveExistingSignature(bottomMargin);
|
||
|
||
// Layout constants
|
||
const float sigX = 390F;
|
||
const float sigWidth = 230F;
|
||
const float sigImgHeight = 70F;
|
||
const float infoHeight = 65F; // up to 4 lines at 8pt
|
||
const float innerGap = 5F;
|
||
const float bottomPad = 6F;
|
||
const float defaultTopPad = 8F;
|
||
const float maxBandHeight = 210F;
|
||
|
||
float requiredHeight = defaultTopPad + sigImgHeight + innerGap + infoHeight + bottomPad;
|
||
|
||
// Grow band if needed, but cap at maxBandHeight to avoid overlapping page content
|
||
bottomMargin.HeightF = Math.Min(maxBandHeight, Math.Max(bottomMargin.HeightF, requiredHeight));
|
||
|
||
// If band is tighter than required, compress top padding so content still fits
|
||
float topPad = Math.Max(0F, bottomMargin.HeightF - bottomPad - infoHeight - innerGap - sigImgHeight);
|
||
|
||
float imageY = topPad;
|
||
float labelY = imageY + sigImgHeight + innerGap;
|
||
|
||
var signatureInformation = string.IsNullOrWhiteSpace(signerPosition)
|
||
? $"{signerFullName}\n{signaturePlace}, {DateTime.Now:d}"
|
||
: $"{signerFullName}\n{signerPosition}\n{signaturePlace}, {DateTime.Now:d}";
|
||
|
||
var signature = new XRPictureBox {
|
||
Name = "receiverSignatureImage",
|
||
ImageSource = imageSource,
|
||
BoundsF = new RectangleF(sigX, imageY, sigWidth, sigImgHeight),
|
||
Sizing = ImageSizeMode.ZoomImage,
|
||
Borders = BorderSide.Bottom,
|
||
BorderColor = System.Drawing.Color.FromArgb(73, 80, 87)
|
||
};
|
||
|
||
var signatureLabel = new XRLabel {
|
||
Name = "receiverSignatureLabel",
|
||
Text = signatureInformation,
|
||
Multiline = true,
|
||
BoundsF = new RectangleF(sigX, labelY, sigWidth, infoHeight),
|
||
Font = new DXFont("Open Sans", 8F, DXFontStyle.Regular),
|
||
ForeColor = System.Drawing.Color.FromArgb(73, 80, 87),
|
||
TextAlignment = TextAlignment.TopLeft
|
||
};
|
||
|
||
bottomMargin.Controls.AddRange(new XRControl[] { signature, signatureLabel });
|
||
}
|
||
|
||
static void RemoveExistingSignature(BottomMarginBand bottomMargin) {
|
||
var controls = bottomMargin.Controls
|
||
.Cast<XRControl>()
|
||
.Where(control => control.Name is "receiverSignatureLabel" or "receiverSignatureImage")
|
||
.ToArray();
|
||
|
||
foreach(var control in controls)
|
||
bottomMargin.Controls.Remove(control);
|
||
}
|
||
|
||
static void AddSignatureAtAnnotation(XtraReport report, AnnotationDto? annotation, string signatureDataUrl, string signerFullName, string signerPosition, string signaturePlace) {
|
||
var imageBytes = Convert.FromBase64String(signatureDataUrl[(signatureDataUrl.IndexOf(',') + 1)..]);
|
||
using var imageStream = new MemoryStream(imageBytes);
|
||
var imageSource = new ImageSource(DXImage.FromStream(imageStream));
|
||
var bottomMargin = report.Bands.OfType<BottomMarginBand>().FirstOrDefault();
|
||
|
||
if (bottomMargin is null) {
|
||
bottomMargin = new BottomMarginBand();
|
||
report.Bands.Add(bottomMargin);
|
||
}
|
||
|
||
var annotId = annotation?.Id.ToString() ?? "0";
|
||
RemoveExistingSignatureById(bottomMargin, annotId);
|
||
|
||
const float sigWidth = 230F;
|
||
const float sigImgHeight = 70F;
|
||
const float infoHeight = 48.75F;
|
||
const float innerGap = 5F;
|
||
const float bottomPad = 6F;
|
||
const float defaultTopPad = 8F;
|
||
const float maxBandHeight = 210F;
|
||
|
||
float sigX = (float)(annotation?.X ?? 390.0);
|
||
float requiredHeight = defaultTopPad + sigImgHeight + innerGap + infoHeight + bottomPad;
|
||
bottomMargin.HeightF = Math.Min(maxBandHeight, Math.Max(bottomMargin.HeightF, requiredHeight));
|
||
float topPad = Math.Max(0F, bottomMargin.HeightF - bottomPad - infoHeight - innerGap - sigImgHeight);
|
||
float imageY = topPad;
|
||
float labelY = imageY + sigImgHeight + innerGap;
|
||
|
||
var signatureInformation = string.IsNullOrWhiteSpace(signerPosition)
|
||
? $"{signerFullName}\n{signaturePlace}, {DateTime.Now:d}"
|
||
: $"{signerFullName}\n{signerPosition}\n{signaturePlace}, {DateTime.Now:d}";
|
||
|
||
var signature = new XRPictureBox {
|
||
Name = $"receiverSignatureImage_{annotId}",
|
||
ImageSource = imageSource,
|
||
BoundsF = new RectangleF(sigX, imageY, sigWidth, sigImgHeight),
|
||
Sizing = ImageSizeMode.ZoomImage,
|
||
Borders = BorderSide.Bottom,
|
||
BorderColor = System.Drawing.Color.FromArgb(73, 80, 87),
|
||
BackColor = Color.FromArgb(219, 219, 219),
|
||
};
|
||
|
||
var signatureLabel = new XRLabel {
|
||
Name = $"receiverSignatureLabel_{annotId}",
|
||
Text = signatureInformation,
|
||
Multiline = true,
|
||
BoundsF = new RectangleF(sigX, labelY - 5, sigWidth, infoHeight),
|
||
Font = new DXFont("Open Sans", 8F, DXFontStyle.Regular),
|
||
ForeColor = System.Drawing.Color.FromArgb(73, 80, 87),
|
||
TextAlignment = TextAlignment.TopCenter,
|
||
BackColor = Color.FromArgb(219, 219, 219)
|
||
};
|
||
|
||
bottomMargin.Controls.AddRange(new XRControl[] { signature, signatureLabel });
|
||
}
|
||
|
||
static void RemoveExistingSignatureById(BottomMarginBand bottomMargin, string annotId) {
|
||
var controls = bottomMargin.Controls
|
||
.Cast<XRControl>()
|
||
.Where(c => c.Name == $"receiverSignatureImage_{annotId}" || c.Name == $"receiverSignatureLabel_{annotId}")
|
||
.ToArray();
|
||
foreach (var c in controls)
|
||
bottomMargin.Controls.Remove(c);
|
||
}
|
||
|
||
public void Dispose() {
|
||
_dotNetRef?.Dispose();
|
||
}
|
||
}
|
||
|