From 8933cff5581ac41c4825767a7bdaf7ef2dcb68c5 Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 30 Sep 2026 14:03:56 +0200 Subject: [PATCH] Add envelope rejection feature with popup UI Introduced a new feature to allow users to reject envelopes with an optional reason. Added a `RejectAsync` method in `AnnotationService` to handle rejection requests. Updated `ReceiverPage.razor` to include a "Reject" button in the PDF toolbar and a rejection popup for user input. Implemented state management for the rejection popup, including visibility, submission status, and validation messages. Updated `AnnotationController` to accept `envelopeKey` as a route parameter for the rejection endpoint. Modified `ReceiverClaimExtensions` to support multiple email claim types. Added new CSS styles for the "Reject" button, including hover effects and dark theme support. Enhanced error handling and logging for the rejection process. --- .../Services/AnnotationService.cs | 20 +++ .../Components/Pages/ReceiverPage.razor | 137 ++++++++++++++++++ .../Controllers/AnnotationController.cs | 5 +- .../Extensions/ReceiverClaimExtensions.cs | 4 +- .../wwwroot/css/envelope-viewer.css | 35 +++++ 5 files changed, 197 insertions(+), 4 deletions(-) diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AnnotationService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AnnotationService.cs index fd9e7556..2f28584a 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AnnotationService.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AnnotationService.cs @@ -26,4 +26,24 @@ public class AnnotationService(IHttpClientFactory httpClientFactory) var result = await response.Content.ReadFromJsonAsync>(_jsonOptions, cancel); return result ?? []; } + + public async Task RejectAsync(string envelopeKey, string? reason, CancellationToken cancel = default) + { + using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server"); + + var response = await http.PostAsJsonAsync( + $"/api/Annotation/reject/{Uri.EscapeDataString(envelopeKey)}", + reason, + _jsonOptions, + cancel); + + if (!response.IsSuccessStatusCode) + { + var body = await response.Content.ReadAsStringAsync(cancel); + throw new HttpRequestException( + $"Failed to reject envelope {envelopeKey}: {(int)response.StatusCode} {response.StatusCode} - {body}", + null, + response.StatusCode); + } + } } diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverPage.razor index d477976a..c7999655 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverPage.razor @@ -16,6 +16,7 @@ @inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService @inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService @inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService +@inject AnnotationService AnnotationService @inject AppVersionService AppVersion @inject IMemoryCache MemoryCache @inject ILogger Logger @@ -125,6 +126,17 @@ Unterschreiben } + + @@ -342,6 +354,75 @@ +@* Rejection Popup *@ + + +
+
+ + + +
+
+
+ Möchten Sie dieses Dokument wirklich ablehnen? +
+
+ Diese Aktion markiert den Umschlag als abgelehnt und informiert den Absender. +
+
+
+ +
+ + +
+ + @if (!string.IsNullOrWhiteSpace(_rejectValidationMessage)) + { +
+ @_rejectValidationMessage +
+ } +
+ +
+ + +
+
+
+ @code { // ----- Constants ----- const string SignatureTabDraw = "draw"; @@ -387,6 +468,12 @@ string _signerPosition = string.Empty; string _signaturePlace = string.Empty; + // ----- Rejection popup state ----- + bool _rejectPopupVisible; + bool _isRejectSubmitting; + string _rejectReason = string.Empty; + string? _rejectValidationMessage; + // ----- Lifecycle ----- protected override async Task OnInitializedAsync() { @@ -701,6 +788,56 @@ forceLoad: true); } + void OpenRejectPopup() + { + _rejectValidationMessage = null; + _rejectPopupVisible = true; + } + + void CloseRejectPopup() + { + if (_isRejectSubmitting) + return; + + _rejectPopupVisible = false; + } + + async Task SubmitRejectAsync() + { + if (_isRejectSubmitting) + return; + + _isRejectSubmitting = true; + _rejectValidationMessage = null; + await InvokeAsync(StateHasChanged); + + try + { + var reason = string.IsNullOrWhiteSpace(_rejectReason) + ? null + : _rejectReason.Trim(); + + if (string.IsNullOrWhiteSpace(EnvelopeKey)) + { + _rejectValidationMessage = "Envelope-Schluessel fehlt."; + _isRejectSubmitting = false; + await InvokeAsync(StateHasChanged); + return; + } + + await AnnotationService.RejectAsync(EnvelopeKey, reason); + + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to reject envelope {EnvelopeKey}", EnvelopeKey); + _rejectValidationMessage = "Die Ablehnung konnte nicht gespeichert werden. Bitte versuchen Sie es erneut."; + _isRejectSubmitting = false; + await InvokeAsync(StateHasChanged); + } + } + async Task GetActiveSignatureDataUrlAsync() { if (_activeSignatureTab == SignatureTabDraw) diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/AnnotationController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/AnnotationController.cs index 4847945b..0bf20d17 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/AnnotationController.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/AnnotationController.cs @@ -100,11 +100,12 @@ public class AnnotationController : ControllerBase /// /// Rejects the document for the current receiver. /// + /// Envelope key route parameter. /// Optional rejection reason. [Authorize(Policy = AuthPolicy.Receiver)] - [HttpPost("reject")] + [HttpPost("reject/{envelopeKey}")] [Obsolete("Use MediatR")] - public async Task Reject([FromBody] string? reason = null) + public async Task Reject([FromRoute] string envelopeKey, [FromBody] string? reason = null) { var signature = User.ReceiverSignature(); var uuid = User.EnvelopeUuid(); diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Extensions/ReceiverClaimExtensions.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Extensions/ReceiverClaimExtensions.cs index 13720e38..8882325d 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Extensions/ReceiverClaimExtensions.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Extensions/ReceiverClaimExtensions.cs @@ -67,7 +67,7 @@ public static class ReceiverClaimExtensions /// Gets the authenticated receiver email address from the claims. /// public static string ReceiverMail(this ClaimsPrincipal user) - => user.GetRequiredClaimValue(JwtRegisteredClaimNames.Email); + => user.GetRequiredClaimValue(JwtRegisteredClaimNames.Email, ClaimTypes.Email); /// /// Gets the authenticated envelope identifier from the claims. @@ -120,4 +120,4 @@ public static class ReceiverClaimExtensions { return Role.Receiver.Authorized.Any(user.IsInRole); } -} \ No newline at end of file +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css index 24f1b009..14355c5a 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css @@ -609,6 +609,29 @@ body.resizing { cursor: not-allowed; } +.pdf-toolbar__btn--reject { + display: flex; + align-items: center; + gap: 0.375rem; + min-width: auto; + padding: 0.5rem 0.75rem; + background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(220, 38, 38, 0.08) 100%); + border: 1px solid rgba(239, 68, 68, 0.3); + color: #b91c1c; +} + +.pdf-toolbar__btn--reject:hover:not(:disabled) { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + border-color: transparent; + color: #ffffff; + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3); +} + +.pdf-toolbar__btn--reject:disabled { + opacity: 0.6; + cursor: not-allowed; +} + .pdf-toolbar__btn-text { font-size: 0.813rem; font-weight: 600; @@ -1211,6 +1234,18 @@ body.resizing { color: #a5b4fc; } +[data-sf-theme="blazing-dark"] .pdf-toolbar__btn--reject { + background: rgba(239, 68, 68, 0.15); + border-color: rgba(248, 113, 113, 0.4); + color: #fca5a5; +} + +[data-sf-theme="blazing-dark"] .pdf-toolbar__btn--reject:hover:not(:disabled) { + background: rgba(239, 68, 68, 0.35); + border-color: rgba(248, 113, 113, 0.6); + color: #ffffff; +} + /* Error container */ [data-sf-theme="blazing-dark"] .error-container .alert-danger { background: rgba(239, 68, 68, 0.12);