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.
This commit is contained in:
@@ -26,4 +26,24 @@ public class AnnotationService(IHttpClientFactory httpClientFactory)
|
||||
var result = await response.Content.ReadFromJsonAsync<List<AnnotationDto>>(_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ReceiverPage> Logger
|
||||
@@ -125,6 +126,17 @@
|
||||
<span class="pdf-toolbar__btn-text">Unterschreiben</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--reject"
|
||||
@onclick="OpenRejectPopup"
|
||||
disabled="@_isRejectSubmitting"
|
||||
title="Ablehnen"
|
||||
style="flex-shrink: 0;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
|
||||
</svg>
|
||||
<span class="pdf-toolbar__btn-text">Ablehnen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -342,6 +354,75 @@
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
@* Rejection Popup *@
|
||||
<DxPopup @bind-Visible="_rejectPopupVisible"
|
||||
HeaderText="Dokument ablehnen"
|
||||
Width="500px"
|
||||
MaxWidth="95vw"
|
||||
ShowFooter="true"
|
||||
CloseOnOutsideClick="false"
|
||||
ShowCloseButton="false"
|
||||
CloseOnEscape="false">
|
||||
<BodyContentTemplate>
|
||||
<div style="display: flex; align-items: flex-start; gap: 0.9rem; padding: 0.2rem 0 0.8rem;">
|
||||
<div style="flex-shrink: 0; width: 36px; height: 36px; border-radius: 999px; background: #fee2e2; color: #b91c1c; display: inline-flex; align-items: center; justify-content: center;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div style="min-width: 0;">
|
||||
<div style="font-size: 0.95rem; font-weight: 600; color: #1f2937; margin-bottom: 0.3rem;">
|
||||
Möchten Sie dieses Dokument wirklich ablehnen?
|
||||
</div>
|
||||
<div style="font-size: 0.84rem; color: #6b7280; line-height: 1.45;">
|
||||
Diese Aktion markiert den Umschlag als abgelehnt und informiert den Absender.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="rp-reject-reason" class="form-label" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
|
||||
Grund <span style="color: #6c757d; font-weight: 400;">(optional)</span>
|
||||
</label>
|
||||
<textarea id="rp-reject-reason"
|
||||
class="form-control"
|
||||
rows="4"
|
||||
maxlength="1000"
|
||||
value="@_rejectReason"
|
||||
@oninput="args => _rejectReason = args.Value?.ToString() ?? string.Empty"
|
||||
placeholder="Grund der Ablehnung"
|
||||
style="border: 2px solid #e9ecef; border-radius: 8px; padding: 0.625rem; resize: vertical;"></textarea>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(_rejectValidationMessage))
|
||||
{
|
||||
<div style="background: #fee2e2; border-left: 4px solid #dc2626; padding: 0.75rem 1rem; margin-top: 1rem; border-radius: 4px;">
|
||||
<span style="color: #b91c1c; font-size: 0.875rem; font-weight: 500;">@_rejectValidationMessage</span>
|
||||
</div>
|
||||
}
|
||||
</BodyContentTemplate>
|
||||
<FooterContentTemplate>
|
||||
<div class="d-flex gap-2 justify-content-end w-100" style="padding: 0.5rem 0;">
|
||||
<button class="btn btn-outline-secondary"
|
||||
@onclick="CloseRejectPopup"
|
||||
disabled="@_isRejectSubmitting"
|
||||
style="border-radius: 6px; padding: 0.625rem 1.25rem; font-weight: 500;">
|
||||
Zurück
|
||||
</button>
|
||||
<button class="btn btn-danger"
|
||||
@onclick="SubmitRejectAsync"
|
||||
disabled="@_isRejectSubmitting"
|
||||
style="border-radius: 6px; padding: 0.625rem 1.5rem; font-weight: 600;">
|
||||
@if (_isRejectSubmitting)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
|
||||
}
|
||||
Ablehnen
|
||||
</button>
|
||||
</div>
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
@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<string?> GetActiveSignatureDataUrlAsync()
|
||||
{
|
||||
if (_activeSignatureTab == SignatureTabDraw)
|
||||
|
||||
@@ -100,11 +100,12 @@ public class AnnotationController : ControllerBase
|
||||
/// <summary>
|
||||
/// Rejects the document for the current receiver.
|
||||
/// </summary>
|
||||
/// <param name="envelopeKey">Envelope key route parameter.</param>
|
||||
/// <param name="reason">Optional rejection reason.</param>
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[HttpPost("reject")]
|
||||
[HttpPost("reject/{envelopeKey}")]
|
||||
[Obsolete("Use MediatR")]
|
||||
public async Task<IActionResult> Reject([FromBody] string? reason = null)
|
||||
public async Task<IActionResult> Reject([FromRoute] string envelopeKey, [FromBody] string? reason = null)
|
||||
{
|
||||
var signature = User.ReceiverSignature();
|
||||
var uuid = User.EnvelopeUuid();
|
||||
|
||||
@@ -67,7 +67,7 @@ public static class ReceiverClaimExtensions
|
||||
/// Gets the authenticated receiver email address from the claims.
|
||||
/// </summary>
|
||||
public static string ReceiverMail(this ClaimsPrincipal user)
|
||||
=> user.GetRequiredClaimValue(JwtRegisteredClaimNames.Email);
|
||||
=> user.GetRequiredClaimValue(JwtRegisteredClaimNames.Email, ClaimTypes.Email);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated envelope identifier from the claims.
|
||||
@@ -120,4 +120,4 @@ public static class ReceiverClaimExtensions
|
||||
{
|
||||
return Role.Receiver.Authorized.Any(user.IsInRole);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user