diff --git a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor index 75e6acbe..a164c612 100644 --- a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor +++ b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor @@ -115,22 +115,43 @@
-
- - -
+ @if (_totalSignatures > 0) { +
+ + +
+ + + + + @_signedSignatures +  /  + @_totalSignatures + + @if (_unsignedSignatures > 0) { + @_unsignedSignatures offen + } else { + ✓ Komplett + } +
+ + +
+ } }
@@ -355,6 +376,11 @@ bool _showThumbnails = true; DotNetObjectReference? _dotNetRef; IReadOnlyList _signatures = []; +// Signature navigation state +int _totalSignatures = 0; +int _signedSignatures = 0; +int _unsignedSignatures = 0; + // Signature state record SignatureCapture(string DataUrl, string FullName, string Position, string Place); SignatureCapture? _capturedSignature; @@ -560,6 +586,7 @@ const int MaxThumbnailWidth = 400; try { await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons", _signatures, _currentPage, _dotNetRef); + await UpdateSignatureCounterAsync(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"Signature button rendering error: {ex.Message}"); } @@ -579,8 +606,38 @@ const int MaxThumbnailWidth = 400; _capturedSignature.FullName, _capturedSignature.Position, _capturedSignature.Place); + + // Update counter + await UpdateSignatureCounterAsync(); } + [JSInvokable] + public async Task OnSignatureNavChanged() { + await UpdateSignatureCounterAsync(); + } + + async Task UpdateSignatureCounterAsync() { + try { + var state = await JSRuntime.InvokeAsync("pdfViewer.getSignatureNavState"); + _totalSignatures = state.Total; + _signedSignatures = state.Signed; + _unsignedSignatures = state.Unsigned; + 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); + } + + record SignatureNavState(int Total, int Signed, int Unsigned, int CurrentIndex, bool CanGoPrev, bool CanGoNext); + // Signature popup methods void OpenSignaturePopup() { _activeSignatureTab = SignatureTabDraw; diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css b/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css index ae201187..415c200c 100644 --- a/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css +++ b/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css @@ -216,30 +216,32 @@ body.resizing { background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(20px); border-radius: 12px; - padding: 0.75rem 1.75rem; + padding: 0.75rem 1.5rem; display: flex; align-items: center; justify-content: space-between; - gap: 2rem; + gap: 1.5rem; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(126, 34, 206, 0.1); border: 1px solid rgba(126, 34, 206, 0.15); flex-shrink: 0; - width: 90%; - max-width: 1200px; + width: 95%; + max-width: 1400px; } .pdf-toolbar__section { display: flex; align-items: center; gap: 0.5rem; + flex-shrink: 0; } .pdf-toolbar__zoom-section { gap: 0.75rem; flex: 1; - max-width: 500px; + max-width: 400px; + min-width: 280px; justify-content: center; } @@ -341,8 +343,8 @@ body.resizing { .pdf-toolbar__zoom-slider { -webkit-appearance: none; width: 100%; - min-width: 240px; - max-width: 450px; + min-width: 180px; + max-width: 350px; height: 6px; border-radius: 3px; background: linear-gradient(90deg, @@ -395,6 +397,71 @@ body.resizing { text-align: center; } +/* Signature Navigation Styles */ +.pdf-toolbar__signature-nav { + display: flex; + align-items: center; + gap: 0.375rem; + background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%); + border: 1px solid rgba(126, 34, 206, 0.2); + border-radius: 10px; + padding: 0.25rem 0.5rem; + flex-shrink: 0; +} + +.pdf-toolbar__btn--signature-nav { + min-width: 30px; + min-height: 30px; + padding: 0.25rem; + background: white; + border: 1px solid rgba(126, 34, 206, 0.25); +} + +.pdf-toolbar__btn--signature-nav:hover:not(:disabled) { + background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%); + border-color: transparent; +} + +.pdf-toolbar__btn--signature-nav:hover:not(:disabled) svg { + color: white; +} + +.pdf-toolbar__signature-counter { + display: flex; + align-items: center; + gap: 0.375rem; + padding: 0 0.375rem; +} + +.pdf-toolbar__signature-counter svg { + color: #7e22ce; + flex-shrink: 0; +} + +.pdf-toolbar__signature-counter-text { + font-size: 0.8125rem; + font-weight: 600; + color: #1e293b; + white-space: nowrap; +} + +.pdf-toolbar__signature-badge { + font-size: 0.625rem; + font-weight: 700; + padding: 0.1875rem 0.5rem; + border-radius: 5px; + background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%); + color: #7e22ce; + text-transform: uppercase; + letter-spacing: 0.05em; + white-space: nowrap; +} + +.pdf-toolbar__signature-badge--complete { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%); + color: #059669; +} + .pdf-frame { background: white; border-radius: 16px; diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js b/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js index e78ede29..cc5267d5 100644 --- a/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js +++ b/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js @@ -455,6 +455,150 @@ window.pdfViewer = { // Signature button functionality signatureButtons: [], + appliedSignatures: [], // Track which signatures have been applied + + /** + * Gets signature navigation state (for toolbar display) + * @returns {object} { total, signed, unsigned, currentIndex, canGoPrev, canGoNext } + */ + getSignatureNavState() { + const total = this.signatureButtons.length + this.appliedSignatures.length; + const signed = this.appliedSignatures.length; + const unsigned = this.signatureButtons.length; + + // Find index of first unsigned signature button (if any) + let currentIndex = -1; + if (unsigned > 0) { + currentIndex = signed; // 0-based index of next signature to sign + } + + return { + total: total, + signed: signed, + unsigned: unsigned, + currentIndex: currentIndex, + canGoPrev: signed > 0, // Can go to previous applied signature + canGoNext: unsigned > 0 // Can go to next unsigned signature + }; + }, + + /** + * Navigates to the next unsigned signature button. + * Scrolls to button position and changes page if necessary. + */ + async goToNextSignature(dotNetRef) { + if (this.signatureButtons.length === 0) { + return false; + } + + // Get first unsigned signature button + const button = this.signatureButtons[0]; + const signatureId = parseInt(button.getAttribute('data-signature-id')); + + // Find signature in original list to get page number + const signature = this._allSignatures?.find(s => s.id === signatureId); + if (!signature) { + return false; + } + + // Change page if needed + if (signature.page !== this.pageNum) { + await this.goToPage(signature.page); + + // Wait for page render and button re-creation + await new Promise(resolve => setTimeout(resolve, 300)); + + // Re-find button after page change + const newButton = this.signatureButtons.find(btn => + parseInt(btn.getAttribute('data-signature-id')) === signatureId); + + if (newButton) { + this.scrollToButton(newButton); + } + } else { + this.scrollToButton(button); + } + + // Notify Blazor to update counter + if (dotNetRef) { + dotNetRef.invokeMethodAsync('OnSignatureNavChanged'); + } + + return true; + }, + + /** + * Navigates to the previous signature (last applied one). + * Scrolls to signature position and changes page if necessary. + */ + async goToPreviousSignature(dotNetRef) { + if (this.appliedSignatures.length === 0) { + return false; + } + + // Get last applied signature + const lastSig = this.appliedSignatures[this.appliedSignatures.length - 1]; + + // Change page if needed + if (lastSig.page !== this.pageNum) { + await this.goToPage(lastSig.page); + await new Promise(resolve => setTimeout(resolve, 300)); + } + + // Find applied signature container + const container = document.querySelector(`.applied-signature[data-signature-id="${lastSig.id}"]`); + if (container) { + this.scrollToElement(container); + } + + // Notify Blazor + if (dotNetRef) { + dotNetRef.invokeMethodAsync('OnSignatureNavChanged'); + } + + return true; + }, + + /** + * Scrolls to center a button in the viewport + */ + scrollToButton(button) { + const wrapper = this.canvas.closest('.pdf-canvas-wrapper'); + if (!wrapper) return; + + const buttonRect = button.getBoundingClientRect(); + const wrapperRect = wrapper.getBoundingClientRect(); + + // Calculate scroll to center button + const scrollLeft = wrapper.scrollLeft + buttonRect.left - wrapperRect.left - (wrapperRect.width / 2) + (buttonRect.width / 2); + const scrollTop = wrapper.scrollTop + buttonRect.top - wrapperRect.top - (wrapperRect.height / 2) + (buttonRect.height / 2); + + wrapper.scrollTo({ + left: scrollLeft, + top: scrollTop, + behavior: 'smooth' + }); + }, + + /** + * Scrolls to center an element in the viewport + */ + scrollToElement(element) { + const wrapper = this.canvas.closest('.pdf-canvas-wrapper'); + if (!wrapper) return; + + const elemRect = element.getBoundingClientRect(); + const wrapperRect = wrapper.getBoundingClientRect(); + + const scrollLeft = wrapper.scrollLeft + elemRect.left - wrapperRect.left - (wrapperRect.width / 2) + (elemRect.width / 2); + const scrollTop = wrapper.scrollTop + elemRect.top - wrapperRect.top - (wrapperRect.height / 2) + (elemRect.height / 2); + + wrapper.scrollTo({ + left: scrollLeft, + top: scrollTop, + behavior: 'smooth' + }); + }, /** * Renders clickable signature buttons on the PDF canvas. @@ -471,6 +615,7 @@ window.pdfViewer = { } this.dotNetReference = dotNetRef; + this._allSignatures = signatures; // Store for navigation try { // Filter signatures for current page @@ -629,12 +774,23 @@ window.pdfViewer = { const left = button.style.left; const top = button.style.top; + // Find signature data for tracking + const signature = this._allSignatures?.find(s => s.id === signatureId); + // Remove button if (button.parentNode) { button.parentNode.removeChild(button); } this.signatureButtons.splice(buttonIndex, 1); + // Track applied signature + if (signature) { + this.appliedSignatures.push({ + id: signatureId, + page: signature.page + }); + } + // Create signature container const signatureContainer = document.createElement('div'); signatureContainer.className = 'applied-signature';