diff --git a/EnvelopeGenerator.ReceiverUI/Pages/ReportViewer.razor b/EnvelopeGenerator.ReceiverUI/Pages/ReportViewer.razor index dd2f9d6d..6497aad3 100644 --- a/EnvelopeGenerator.ReceiverUI/Pages/ReportViewer.razor +++ b/EnvelopeGenerator.ReceiverUI/Pages/ReportViewer.razor @@ -1,11 +1,33 @@ @page "/reportviewer/" +@using System.Drawing +@using DevExpress.Drawing +@using DevExpress.Utils +@using DevExpress.XtraPrinting +@using DevExpress.XtraPrinting.Drawing @using DevExpress.XtraReports.UI; @using EnvelopeGenerator.ReceiverUI.Services; +@inject IJSRuntime JSRuntime @inject InMemoryReportStorageWebExtension ReportStorage +
+
+
Unterschrift
+

Bitte fuegen Sie vor dem PDF-Export Ihre Unterschrift in das Feld unten ein.

+ + @if(!string.IsNullOrWhiteSpace(SignatureValidationMessage)) { +
@SignatureValidationMessage
+ } +
+ + + +
+
+
+ @if(Report is not null) { } @@ -13,6 +35,8 @@ @code { DxReportViewer reportViewer; XtraReport? Report; + bool SignatureApplied; + string? SignatureValidationMessage; protected override async Task OnInitializedAsync() { Report = ReportStorage.TryGetReport("LargeDatasetReport", out var savedReport) @@ -21,4 +45,100 @@ await Task.CompletedTask; } + + protected override async Task OnAfterRenderAsync(bool firstRender) { + if(firstRender) + await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", "receiver-signature-pad"); + } + + async Task ClearSignatureAsync() { + await JSRuntime.InvokeVoidAsync("receiverSignature.clear", "receiver-signature-pad"); + SignatureApplied = false; + SignatureValidationMessage = null; + Report = ReportStorage.TryGetReport("LargeDatasetReport", out var savedReport) + ? savedReport + : PredefinedReports.ReportsFactory.GetReport("LargeDatasetReport"); + + if(reportViewer is not null) + await reportViewer.OpenReportAsync(Report); + } + + async Task ApplySignatureAsync() { + var signatureDataUrl = await JSRuntime.InvokeAsync("receiverSignature.getDataUrl", "receiver-signature-pad"); + + if(string.IsNullOrWhiteSpace(signatureDataUrl)) { + SignatureApplied = false; + SignatureValidationMessage = "Die Unterschrift ist fuer den PDF-Export erforderlich."; + return; + } + + SignatureValidationMessage = null; + ApplySignatureToReport(signatureDataUrl); + SignatureApplied = true; + + if(reportViewer is not null) + await reportViewer.OpenReportAsync(Report); + } + + async Task ExportSignedPdfAsync() { + if(!SignatureApplied || Report is null) { + SignatureValidationMessage = "Bitte fuegen Sie die Unterschrift zuerst zum Bericht hinzu."; + return; + } + + await reportViewer.ExportToAsync(ExportFormat.Pdf); + } + + void ApplySignatureToReport(string signatureDataUrl) { + Report ??= ReportStorage.TryGetReport("LargeDatasetReport", out var savedReport) + ? savedReport + : PredefinedReports.ReportsFactory.GetReport("LargeDatasetReport"); + + AddSignature(Report, signatureDataUrl); + } + + static void AddSignature(XtraReport report, string signatureDataUrl) { + 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); + } + + bottomMargin.HeightF = Math.Max(bottomMargin.HeightF, 120F); + RemoveExistingSignature(bottomMargin); + + var signatureLabel = new XRLabel { + Name = "receiverSignatureLabel", + Text = $"Empfaengerunterschrift - {DateTime.Now:g}", + BoundsF = new RectangleF(390F, 6F, 230F, 18F), + Font = new DXFont("Open Sans", 8F, DXFontStyle.Bold), + ForeColor = System.Drawing.Color.FromArgb(73, 80, 87), + TextAlignment = TextAlignment.MiddleLeft + }; + + var signature = new XRPictureBox { + Name = "receiverSignatureImage", + ImageSource = imageSource, + BoundsF = new RectangleF(390F, 28F, 230F, 70F), + Sizing = ImageSizeMode.ZoomImage, + Borders = BorderSide.Bottom, + BorderColor = System.Drawing.Color.FromArgb(73, 80, 87) + }; + + bottomMargin.Controls.AddRange(new XRControl[] { signatureLabel, signature }); + } + + 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); + } } \ No newline at end of file diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/index.html b/EnvelopeGenerator.ReceiverUI/wwwroot/index.html index 73ce68f0..9fe2cb56 100644 --- a/EnvelopeGenerator.ReceiverUI/wwwroot/index.html +++ b/EnvelopeGenerator.ReceiverUI/wwwroot/index.html @@ -62,6 +62,7 @@ X + diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/js/receiver-signature.js b/EnvelopeGenerator.ReceiverUI/wwwroot/js/receiver-signature.js new file mode 100644 index 00000000..e30ba8cf --- /dev/null +++ b/EnvelopeGenerator.ReceiverUI/wwwroot/js/receiver-signature.js @@ -0,0 +1,86 @@ +window.receiverSignature = (() => { + const pads = new Map(); + + function getPosition(canvas, event) { + const rect = canvas.getBoundingClientRect(); + const source = event.touches && event.touches.length ? event.touches[0] : event; + return { + x: (source.clientX - rect.left) * (canvas.width / rect.width), + y: (source.clientY - rect.top) * (canvas.height / rect.height) + }; + } + + function initialize(canvasId) { + const canvas = document.getElementById(canvasId); + if (!canvas || pads.has(canvasId)) + return; + + const context = canvas.getContext('2d'); + context.lineWidth = 2.5; + context.lineCap = 'round'; + context.lineJoin = 'round'; + context.strokeStyle = '#111'; + + const state = { drawing: false, hasSignature: false }; + pads.set(canvasId, state); + + const start = event => { + event.preventDefault(); + const pos = getPosition(canvas, event); + state.drawing = true; + context.beginPath(); + context.moveTo(pos.x, pos.y); + }; + + const move = event => { + if (!state.drawing) + return; + + event.preventDefault(); + const pos = getPosition(canvas, event); + context.lineTo(pos.x, pos.y); + context.stroke(); + state.hasSignature = true; + }; + + const end = event => { + if (!state.drawing) + return; + + event.preventDefault(); + state.drawing = false; + }; + + canvas.addEventListener('mousedown', start); + canvas.addEventListener('mousemove', move); + window.addEventListener('mouseup', end); + canvas.addEventListener('touchstart', start, { passive: false }); + canvas.addEventListener('touchmove', move, { passive: false }); + canvas.addEventListener('touchend', end, { passive: false }); + } + + function clear(canvasId) { + const canvas = document.getElementById(canvasId); + const state = pads.get(canvasId); + if (!canvas || !state) + return; + + canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height); + state.hasSignature = false; + } + + function getDataUrl(canvasId) { + const canvas = document.getElementById(canvasId); + const state = pads.get(canvasId); + if (!canvas || !state || !state.hasSignature) + return null; + + return canvas.toDataURL('image/png'); + } + + return { + initialize, + clear, + getDataUrl + }; +})();