@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 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
@* ?? Envelope info header ???????????????????????????????????????????????? *@ @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> }  ·  @_envelopeReceiver.Envelope?.AddedWhen.ToString("dd.MM.yyyy")
}
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Name)) { @_envelopeReceiver.Name } @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.CompanyName)) { @_envelopeReceiver.CompanyName } @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.JobTitle)) { @_envelopeReceiver.JobTitle } @{ var docElements = _envelopeReceiver.Envelope?.Documents?.FirstOrDefault()?.Elements; int sigCount = docElements?.Count() ?? _annotations.Count; } @if (sigCount > 0) { @sigCount @(sigCount == 1 ? "Unterschriftsfeld" : "Unterschriftsfelder") }
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message)) {
@_envelopeReceiver.Envelope!.Message
} @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage)) {
@_envelopeReceiver.PrivateMessage
}
} @* ?? Signature action bar ???????????????????????????????????????????????? *@
@if (_annotations.Count > 0 && !SignatureApplied) {
@_checkedAnnotations.Count / @_annotations.Count Seite@(AnnotationPages.Count() == 1 ? "" : "n") @string.Join(",", AnnotationPages) @if (_capturedSignature is null) { Zuerst Unterschrift erstellen } else if (_checkedAnnotations.Count == _annotations.Count) { ✓ Wird angewendet… }
} @if (!string.IsNullOrWhiteSpace(SignatureValidationMessage)) { @SignatureValidationMessage } @if (SignatureApplied) { Unterschrift angewendet }
@if (!string.IsNullOrWhiteSpace(EnvelopeKey)) { }
@if(ActiveSignatureTab == SignatureTabDraw) {

Bitte unterschreiben Sie im folgenden Feld.

} else if(ActiveSignatureTab == SignatureTabText) {

Geben Sie Ihre Unterschrift als Text ein und waehlen Sie eine Schriftart.

} else {

Laden Sie ein Bild Ihrer Unterschrift hoch.

}

Bitte geben Sie die folgenden Angaben ein. Das Datum wird automatisch hinzugefuegt.

@if(!string.IsNullOrWhiteSpace(PopupValidationMessage)) {
@PopupValidationMessage
}
@if(Report is not null) { }
@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 _annotations = []; IEnumerable 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 _checkedAnnotations = []; DotNetObjectReference? _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ü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 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); } 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 annotations) { var bottomMargin = report.Bands.OfType().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().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() .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().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() .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(); } }