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) {
+
+ }
}
@@ -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';