diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Components/Pages/EnvelopeReceiverPage.razor b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Components/Pages/EnvelopeReceiverPage.razor new file mode 100644 index 00000000..b1525967 --- /dev/null +++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Components/Pages/EnvelopeReceiverPage.razor @@ -0,0 +1,1069 @@ +@page "/envelope/{EnvelopeKey}" +@using EnvelopeGenerator.ReceiverUI.Models +@using EnvelopeGenerator.ReceiverUI.Models.Constants +@using EnvelopeGenerator.ReceiverUI.Services +@using Microsoft.Extensions.Options +@using EnvelopeGenerator.ReceiverUI.Options +@using Microsoft.JSInterop +@using DevExpress.Blazor +@inject DocumentService DocumentService +@inject NavigationManager Navigation +@inject IOptions AppOptions +@inject IOptions PdfViewerOptions +@inject IJSRuntime JSRuntime +@inject SignatureService SignatureService +@inject SignatureCacheService SignatureCacheService +@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService +@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService +@inject AppVersionService AppVersion +@inject ILogger logger +@implements IAsyncDisposable + + + + + + + + +
+
+
+ @* Row 1: Title + Sender + Badges + Logout *@ +
+ @* Left: Title + Sender *@ +
+ @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> + } + @if (_envelopeReceiver.Envelope?.AddedWhen != null) { +  · @_envelopeReceiver.Envelope.AddedWhen.ToString("dd.MM.yyyy") + } + + } + } else { +
Dokumentenansicht
+ } +
+ + @* Right: Badges + Logout *@ +
+ @if (_envelopeReceiver is not null) { +
+ @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Name)) { + + + + + @_envelopeReceiver.Name + + } + @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName)) { + + Von @_envelopeReceiver.Envelope.User.FullName + + } + @{ + int sigCount = _signatures.Count; + } + @if (sigCount > 0) { + + + + + @sigCount + + } + @if (_envelopeReceiver.Envelope?.UseAccessCode == true) { + + + + + Code + + } + @if (_envelopeReceiver.Envelope?.TFAEnabled == true) { + + + + + + 2FA + + } +
+ + } + + @* Logout button *@ + @if (!string.IsNullOrWhiteSpace(EnvelopeKey)) { + + } +
+
+ + @* Row 2: Messages (visible text) *@ + @if (_envelopeReceiver is not null && (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message) || !string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage))) { +
+ @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message)) { +
+ 📧 + @_envelopeReceiver.Envelope.Message +
+ } + @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage)) { +
+ 🔒 + @_envelopeReceiver.PrivateMessage +
+ } +
+ } +
+
+ +
+ @if (_isLoading) { +
+
+
+ L�dt... +
+

Dokument wird geladen...

+
+
+ } else if (_errorMessage is not null) { +
+
+
+ + + + +
+
Fehler beim Laden des Dokuments
+

@_errorMessage

+
+
+
+
+ } else if (!string.IsNullOrWhiteSpace(_pdfDataUrl)) { +
+ @if (_pdfLoaded) { +
+
+ +
+ +
+ +
+ +
+ + / @_totalPages +
+ +
+ +
+ +
+ +
+ +
@(_currentZoom)%
+
+ +
+ +
+ + @if (_totalSignatures > 0) { +
+ +
+ +
+ +
+ + +
+ + + + + @if (_currentSignatureIndex > 0) { + #@_currentSignatureIndex + | + } + @_signedSignatures +  /  + @_totalSignatures + + @if (_unsignedSignatures > 0) { + @_unsignedSignatures offen + } else { + ✓ Komplett + } +
+ + +
+ +
+ + @* Reset button - only show when signatures are signed *@ + @if (_signedSignatures > 0) { +
+ +
+ } + } +
+ } +
+ @if (_pdfLoaded && _showThumbnails) { + +
+
+ @for (int i = 1; i <= _totalPages; i++) { + var pageNum = i; +
+
+ +
+
@pageNum
+
+ } +
+
+ +
+
+ } +
+
+ +
+
+
+
+
+
+ } else { +
+
+
+ + + + Dokument konnte nicht geladen werden. +
+
+
+ } +
+
+ + + + + + @if(_activeSignatureTab == SignatureTabDraw) { +

Bitte unterschreiben Sie im folgenden Feld.

+ + } else if(_activeSignatureTab == SignatureTabText) { +

Geben Sie Ihre Unterschrift als Text ein und wählen Sie eine Schriftart.

+
+
+ +
+
+ +
+
+ + } else { +

Laden Sie ein Bild Ihrer Unterschrift hoch.

+ + + } + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + @if(!string.IsNullOrWhiteSpace(_popupValidationMessage)) { +
+ @_popupValidationMessage +
+ } +
+ +
+ + +
+
+
+ +@code { +// Signature tab constants +const string SignatureTabDraw = "draw"; +const string SignatureTabText = "text"; +const string SignatureTabImage = "image"; +const string DrawCanvasId = "envelope-signature-pad"; +const string TypedCanvasId = "envelope-typed-signature-pad"; +const string ImageInputId = "envelope-signature-image-input"; +const string ImageCanvasId = "envelope-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; } + +bool _isLoading = true; +string? _errorMessage; +string? _pdfDataUrl; +bool _pdfLoaded = false; +int _currentPage = 1; +int _totalPages = 0; +int _currentZoom = 150; +bool _showThumbnails = true; +bool _isLoggingOut = false; +DotNetObjectReference? _dotNetRef; +IReadOnlyList _signatures = []; +EnvelopeReceiverDto? _envelopeReceiver; + +// Signature navigation state +int _totalSignatures = 0; +int _signedSignatures = 0; +int _unsignedSignatures = 0; +int _currentSignatureIndex = 0; // Current signature index (1-based) + +// Signature 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; + +// Resizable splitter state +int _thumbnailWidth = 260; +bool _isResizing = false; +int _resizeStartX = 0; +int _resizeStartWidth = 0; +const int MinThumbnailWidth = 150; +const int MaxThumbnailWidth = 400; + + async Task LogoutAsync() { + if (string.IsNullOrWhiteSpace(EnvelopeKey) || _isLoggingOut) return; + _isLoggingOut = true; + await InvokeAsync(StateHasChanged); + await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey); + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true); + } + + protected override async Task OnInitializedAsync() { + if (string.IsNullOrWhiteSpace(EnvelopeKey)) { + _errorMessage = "Envelope-Schlüssel fehlt."; + _isLoading = false; + return; + } + + // Check authentication + var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey); + if (!hasAccess) { + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}"); + return; + } + + try { + var pdfBytes = await DocumentService.GetDocumentAsync(EnvelopeKey); + + if (pdfBytes is { Length: > 0 }) { + var base64 = Convert.ToBase64String(pdfBytes); + _pdfDataUrl = $"data:application/pdf;base64,{base64}"; + } else { + _errorMessage = "Dokument konnte nicht geladen werden: Keine Daten empfangen."; + } + + var signatures = await SignatureService.GetAsync(EnvelopeKey); + _signatures = signatures.Convert(UnitOfLength.Point); + + _envelopeReceiver = await EnvelopeReceiverService.GetAsync(EnvelopeKey); + if (_envelopeReceiver is null) + { + logger.LogWarning("Envelope receiver data is null for envelope {EnvelopeKey}", EnvelopeKey); + } + + await JSRuntime.InvokeVoidAsync("console.log", "Loaded signatures:", _signatures); + + // Try to load cached signature first + try + { + var cachedSignature = await SignatureCacheService.GetSignatureAsync(EnvelopeKey); + if (cachedSignature is not null) + { + _capturedSignature = cachedSignature; + _signerFullName = cachedSignature.FullName; + _signerPosition = cachedSignature.Position; + _signaturePlace = cachedSignature.Place; + _signaturePopupVisible = false; + + logger.LogInformation("Cached signature loaded for envelope {EnvelopeKey}", EnvelopeKey); + } + else + { + _activeSignatureTab = SignatureTabDraw; + _signaturePopupVisible = true; + _popupValidationMessage = null; + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to load cached signature, showing popup"); + _activeSignatureTab = SignatureTabDraw; + _signaturePopupVisible = true; + _popupValidationMessage = null; + } + + } catch (HttpRequestException ex) { + _errorMessage = $"Dokument konnte nicht geladen werden: {ex.Message}"; + logger.LogError(ex, "Failed to load document for envelope {EnvelopeKey}", EnvelopeKey); + } catch (Exception ex) { + _errorMessage = $"Fehler: {ex.Message}"; + logger.LogError(ex, "Unexpected error during initialization for envelope {EnvelopeKey}", EnvelopeKey); + } + + _isLoading = false; + await InvokeAsync(StateHasChanged); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) { + if (firstRender) { + // Load saved thumbnail width from localStorage + try { + var savedWidth = await JSRuntime.InvokeAsync("localStorage.getItem", "envelopeViewer_thumbnailWidth"); + if (!string.IsNullOrEmpty(savedWidth) && int.TryParse(savedWidth, out var width)) { + _thumbnailWidth = Math.Clamp(width, MinThumbnailWidth, MaxThumbnailWidth); + await InvokeAsync(StateHasChanged); + } + } catch { + // Ignore localStorage errors + } + } + + if (!_pdfLoaded && !string.IsNullOrWhiteSpace(_pdfDataUrl)) { + await Task.Delay(500); + + try { + _dotNetRef = DotNetObjectReference.Create(this); + + // Send quality options to JavaScript + var options = PdfViewerOptions.Value; + await JSRuntime.InvokeVoidAsync("pdfViewer.setQualityOptions", new + { + options.ThumbnailBaseScale, + options.ThumbnailEnableHiDPI, + options.ThumbnailMaxDPR, + options.MainCanvasEnableHiDPI, + options.MainCanvasMaxDPR, + options.EnableSmoothZoom, + options.ZoomTransitionDuration, + options.RenderingOpacity, + options.ZoomStepPercentage + }); + + var success = await JSRuntime.InvokeAsync("pdfViewer.initialize", "pdf-canvas", _pdfDataUrl, _dotNetRef); + + if (success) { + _pdfLoaded = true; + _totalPages = await JSRuntime.InvokeAsync("pdfViewer.getTotalPages"); + _currentPage = await JSRuntime.InvokeAsync("pdfViewer.getCurrentPage"); + + // Attach resize listeners + await JSRuntime.InvokeVoidAsync("pdfViewer.attachResizeListeners", _dotNetRef); + + + await InvokeAsync(StateHasChanged); + + // Wait for DOM to be ready, then render thumbnails + await Task.Delay(100); + await RenderThumbnailsAsync(); + + // Render signature buttons + await RenderSignatureButtonsAsync(); + } + } catch (Exception ex) { + _errorMessage = $"PDF.js Fehler: {ex.Message}"; + await InvokeAsync(StateHasChanged); + } + } + } + + [JSInvokable] + public async Task OnZoomChanged(double scale) + { + _currentZoom = (int)(scale * 100); + await InvokeAsync(StateHasChanged); + + // Small delay for canvas render to complete (reduced from 100ms to 10ms) + await Task.Delay(10); + await RenderSignatureButtonsAsync(); + } + + async Task NextPage() { + if (await JSRuntime.InvokeAsync("pdfViewer.nextPage")) { + _currentPage = await JSRuntime.InvokeAsync("pdfViewer.getCurrentPage"); + await RenderSignatureButtonsAsync(); + } + } + + async Task PreviousPage() { + if (await JSRuntime.InvokeAsync("pdfViewer.previousPage")) { + _currentPage = await JSRuntime.InvokeAsync("pdfViewer.getCurrentPage"); + await RenderSignatureButtonsAsync(); + } + } + + async Task ZoomIn() { + if (_currentZoom >= 300) return; + await JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn"); + var scale = await JSRuntime.InvokeAsync("pdfViewer.getScale"); + _currentZoom = (int)(scale * 100); + + // Update signature overlay positions after zoom + await RenderSignatureButtonsAsync(); + } + + async Task ZoomOut() { + if (_currentZoom <= 50) return; + await JSRuntime.InvokeVoidAsync("pdfViewer.zoomOut"); + var scale = await JSRuntime.InvokeAsync("pdfViewer.getScale"); + _currentZoom = (int)(scale * 100); + + // Update signature overlay positions after zoom + await RenderSignatureButtonsAsync(); + } + + async Task SetZoom(int percentage) { + var scale = percentage / 100.0; + await JSRuntime.InvokeVoidAsync("pdfViewer.setScale", scale); + _currentZoom = percentage; + } + + async Task OnZoomSliderChanged(ChangeEventArgs e) { + if (int.TryParse(e.Value?.ToString(), out var zoom)) { + await SetZoom(zoom); + + // Update signature overlay positions after zoom + await RenderSignatureButtonsAsync(); + } + } + + async Task OnPageInputChanged(ChangeEventArgs e) { + if (int.TryParse(e.Value?.ToString(), out var pageNum) && pageNum >= 1 && pageNum <= _totalPages) { + if (await JSRuntime.InvokeAsync("pdfViewer.goToPage", pageNum)) { + _currentPage = pageNum; + } + } + } + + async Task FitToWidth() { + await JSRuntime.InvokeVoidAsync("pdfViewer.fitToWidth"); + var scale = await JSRuntime.InvokeAsync("pdfViewer.getScale"); + _currentZoom = (int)(scale * 100); + } + + async Task ToggleThumbnails() { + _showThumbnails = !_showThumbnails; + + // Re-render thumbnails when showing them + if (_showThumbnails && _pdfLoaded) { + await InvokeAsync(StateHasChanged); // Force UI update first + await Task.Delay(150); // Wait for DOM to render canvas elements + await RenderThumbnailsAsync(); + } + } + + async Task GoToPageFromThumbnail(int pageNum) { + if (await JSRuntime.InvokeAsync("pdfViewer.goToPage", pageNum)) { + _currentPage = pageNum; + await RenderSignatureButtonsAsync(); + } + } + + async Task RenderSignatureButtonsAsync() { + if (_signatures.Count == 0 || !_pdfLoaded) return; + + try { + await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons", _signatures, _currentPage, _dotNetRef); + await UpdateSignatureCounterAsync(); + } catch (Exception ex) { + System.Diagnostics.Debug.WriteLine($"Signature button rendering error: {ex.Message}"); + } + } + + [JSInvokable] + public async Task OnSignatureButtonClick(int signatureId) { + if (_capturedSignature == null) { + // No signature captured yet - should not happen as popup is shown on page load + return; + } + + // Apply signature to PDF canvas + await JSRuntime.InvokeVoidAsync("pdfViewer.applySignature", + signatureId, + _capturedSignature.DataUrl, + _capturedSignature.FullName, + _capturedSignature.Position, + _capturedSignature.Place); + + // Update counter + await UpdateSignatureCounterAsync(); + } + + [JSInvokable] + public async Task OnSignatureNavChanged() { + await UpdateSignatureCounterAsync(); + } + + [JSInvokable] + public async Task OnPageChangedBySignatureNav(int newPage) { + _currentPage = newPage; + await RenderSignatureButtonsAsync(); + } + + async Task UpdateSignatureCounterAsync() { + try { + var state = await JSRuntime.InvokeAsync("pdfViewer.getSignatureNavState"); + _totalSignatures = state.Total; + _signedSignatures = state.Signed; + _unsignedSignatures = state.Unsigned; + _currentSignatureIndex = state.CurrentIndex; // Current signature + await InvokeAsync(StateHasChanged); + } catch { + // Ignore errors during counter update + } + } + + async Task GoToPreviousSignature() { + await JSRuntime.InvokeVoidAsync("pdfViewer.goToPreviousSignature", _dotNetRef); + } + + async Task GoToNextSignature() { + await JSRuntime.InvokeVoidAsync("pdfViewer.goToNextSignature", _dotNetRef); + } + + void RestartSigning() { + // Force page reload to reset all signatures and state + Navigation.NavigateTo(Navigation.Uri, forceLoad: true); + } + + record SignatureNavState(int Total, int Signed, int Unsigned, int CurrentIndex, bool CanGoPrev, bool CanGoNext); + + string GetSignatureButtonTitle() + { + if (_signedSignatures > 0) + return "Unterschrift ist gesperrt – bitte Seite neu laden, um zu ändern"; + + return _capturedSignature is not null + ? "Unterschrift ändern" + : "Unterschrift erstellen"; + } + + void HandleSignatureChangeClick() + { + // If any signature is applied, button is disabled - this won't be called + // But just in case, do nothing + if (_signedSignatures > 0) + return; + + // No signatures applied - open popup normally + OpenSignaturePopup(); + } + + // Signature popup methods + void OpenSignaturePopup() { + // Open popup with current signature (edit mode) + _activeSignatureTab = SignatureTabDraw; + _signaturePopupVisible = true; + _popupValidationMessage = null; + + // Load current signature info into form fields + if (_capturedSignature is not null) + { + _signerFullName = _capturedSignature.FullName; + _signerPosition = _capturedSignature.Position; + _signaturePlace = _capturedSignature.Place; + } + } + + async Task OnPopupShownAsync() { + await InitializeActiveSignatureTabAsync(); + + // If there's an existing signature and we're on draw tab, load it to canvas + if (_capturedSignature is not null && _activeSignatureTab == SignatureTabDraw) + { + await Task.Delay(100); // Wait for canvas to be ready + await JSRuntime.InvokeVoidAsync("receiverSignature.loadExistingSignature", DrawCanvasId, _capturedSignature.DataUrl); + } + } + + 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); + } + } + + 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; + + // Save to cache (fire-and-forget, ignore errors) + if (!string.IsNullOrWhiteSpace(EnvelopeKey)) + { + _ = Task.Run(async () => + { + try + { + await SignatureCacheService.SaveSignatureAsync(EnvelopeKey, _capturedSignature); + } + catch + { + // Ignore cache errors + } + }); + } + + await InvokeAsync(StateHasChanged); + Console.WriteLine($"Signature saved: {_signerFullName}, {_signaturePlace}"); + } + + 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 RenderThumbnailsAsync() { + try { + var delay = PdfViewerOptions.Value.ThumbnailRenderDelay; + + // Sequential rendering to avoid overwhelming the browser + for (int i = 1; i <= _totalPages; i++) { + await JSRuntime.InvokeVoidAsync("pdfViewer.renderThumbnail", i, $"thumb-canvas-{i}"); + + // Configurable delay between renders + if (i < _totalPages) { + await Task.Delay(delay); + } + } + } catch (Exception ex) { + // Thumbnail rendering is not critical + System.Diagnostics.Debug.WriteLine($"Thumbnail rendering error: {ex.Message}"); + } + } + + // Resizable splitter methods + void OnSplitterMouseDown(MouseEventArgs e) { + _isResizing = true; + _resizeStartX = (int)e.ClientX; + _resizeStartWidth = _thumbnailWidth; + + // Add resizing class to body to prevent text selection + _ = JSRuntime.InvokeVoidAsync("eval", "document.body.classList.add('resizing')"); + _ = JSRuntime.InvokeVoidAsync("pdfViewer.startResize"); + } + + [JSInvokable] + public async Task OnSplitterMouseMove(int clientX) { + if (!_isResizing) return; + + var delta = clientX - _resizeStartX; + var newWidth = _resizeStartWidth + delta; + + // Clamp to min/max + _thumbnailWidth = Math.Clamp(newWidth, MinThumbnailWidth, MaxThumbnailWidth); + + await InvokeAsync(StateHasChanged); + } + + [JSInvokable] + public async Task OnSplitterMouseUp() { + if (!_isResizing) return; + + _isResizing = false; + + // Remove resizing class from body + await JSRuntime.InvokeVoidAsync("eval", "document.body.classList.remove('resizing')"); + + // Save preference to localStorage + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "envelopeViewer_thumbnailWidth", _thumbnailWidth.ToString()); + + await InvokeAsync(StateHasChanged); + } + + public async ValueTask DisposeAsync() { + if (_pdfLoaded) { + try { + await JSRuntime.InvokeVoidAsync("pdfViewer.dispose"); + } catch { + // Ignore errors during disposal + } + } + _dotNetRef?.Dispose(); + } +} diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor new file mode 100644 index 00000000..86eb1943 --- /dev/null +++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor @@ -0,0 +1,112 @@ +@page "/envelope/DxPdfViewer" +@using System.IO +@using DevExpress.Blazor +@using System.Reflection + + + + + +
+ + Drag and Drop File Hereor + +
+ + + +@if (DocumentContent != null && DocumentContent.Length > 0) +{ +
+ PDF loaded: @DocumentContent.Length bytes +
+ +} +else +{ +
+ Please upload a PDF file to view it. +
+} + +@code { + readonly List ALLOWED_FILE_TYPES = new List { ".pdf" }; + DxFileInput fileInput; + byte[] DocumentContent { get; set; } + protected override void OnInitialized() + { + Assembly assembly = Assembly.GetExecutingAssembly(); + Stream stream = assembly.GetManifestResourceStream("EnvelopeGenerator.ReceiverUI.Resources.Invoice.pdf"); + if (stream != null) + { + using (stream) + using (var binaryReader = new BinaryReader(stream)) + DocumentContent = binaryReader.ReadBytes((int)stream.Length); + } + } + protected async Task OnFilesUploading(FilesUploadingEventArgs args) + { + using (MemoryStream stream = new MemoryStream()) + { + IFileInputSelectedFile file = args.Files[0]; + await file.OpenReadStream(file.Size).CopyToAsync(stream); + DocumentContent = stream.ToArray(); + await InvokeAsync(StateHasChanged); + } + } +} + diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor new file mode 100644 index 00000000..7c2de031 --- /dev/null +++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor @@ -0,0 +1,49 @@ +@page "/envelope/{EnvelopeKey}/DxReportViewer" +@using XtraReport = DevExpress.XtraReports.UI.XtraReport +@using DevExpress.Blazor.Reporting +@using Microsoft.Extensions.Options +@using EnvelopeGenerator.ReceiverUI.Options +@using EnvelopeGenerator.ReceiverUI.Services +@inject InMemoryReportStorageWebExtension ReportStorage +@inject DocumentService DocumentService +@inject IOptions AppOptions + + + + + +@if (_report is not null) { + +} + +@code { + [Parameter] public string EnvelopeKey { get; init; } = null!; + + XtraReport? _report = null; + + protected override async Task OnInitializedAsync() + { + _report = await CreateReport(); + } + + async Task CreateReport() + { + if (AppOptions.Value.UsePredefinedReports) + { + return PredefinedReports.ReportsFactory.GetReport("LargeDatasetReport"); + } + else + { + + var pdfBytes = await DocumentService.GetDocumentAsync(EnvelopeKey); + if (pdfBytes is null || pdfBytes.Length == 0) + throw new InvalidOperationException($"No PDF bytes found for EnvelopeKey: {EnvelopeKey}"); + + var report = new XtraReport(); + var detail = new DevExpress.XtraReports.UI.DetailBand(); + report.Bands.Add(detail); + detail.Controls.Add(new DevExpress.XtraReports.UI.XRPdfContent { Source = pdfBytes, GenerateOwnPages = true }); + return report; + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Components/Pages/EnvelopeReceiverPage_embed.razor b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Components/Pages/EnvelopeReceiverPage_embed.razor new file mode 100644 index 00000000..83834158 --- /dev/null +++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Components/Pages/EnvelopeReceiverPage_embed.razor @@ -0,0 +1,123 @@ +@page "/envelope/Embed" +@using System.IO +@using DevExpress.Blazor +@using System.Reflection + + + + + +
+ + Drag and Drop File Hereor + +
+ + + +@if (DocumentContent != null && DocumentContent.Length > 0) +{ +
+ PDF loaded: @DocumentContent.Length bytes +
+ +} +else +{ +
+ Please upload a PDF file to view it. +
+} + +@code { + readonly List ALLOWED_FILE_TYPES = new List { ".pdf" }; + DxFileInput fileInput; + byte[] DocumentContent { get; set; } + + protected override void OnInitialized() + { + Assembly assembly = Assembly.GetExecutingAssembly(); + Stream stream = assembly.GetManifestResourceStream("EnvelopeGenerator.ReceiverUI.Resources.Invoice.pdf"); + if (stream != null) + { + using (stream) + using (var binaryReader = new BinaryReader(stream)) + DocumentContent = binaryReader.ReadBytes((int)stream.Length); + } + } + + protected async Task OnFilesUploading(FilesUploadingEventArgs args) + { + using (MemoryStream stream = new MemoryStream()) + { + IFileInputSelectedFile file = args.Files[0]; + await file.OpenReadStream(file.Size).CopyToAsync(stream); + DocumentContent = stream.ToArray(); + await InvokeAsync(StateHasChanged); + } + } + + private string GetPdfDataUrl() + { + if (DocumentContent == null || DocumentContent.Length == 0) + return string.Empty; + + string base64 = Convert.ToBase64String(DocumentContent); + return $"data:application/pdf;base64,{base64}#toolbar=0&navpanes=0&scrollbar=1"; + } +} +