From 8933cff5581ac41c4825767a7bdaf7ef2dcb68c5 Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 30 Sep 2026 14:03:56 +0200 Subject: [PATCH 1/4] 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); From 897860b2bf1a3e466ec132793fef8ee92a4a3fa2 Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 30 Sep 2026 14:21:09 +0200 Subject: [PATCH 2/4] Add rejection handling and dedicated page for envelopes Introduced a new rejection flow in `ReceiverPage.razor` to detect rejected envelopes and redirect users to a dedicated rejection page (`ReceiverRejectedPage.razor`). - Added `IsRejected` helper method to check envelope rejection status based on current status and history. - Created `ReceiverRejectedPage.razor` to display rejection messages, sender details, and navigation options. - Implemented localized strings for dynamic rejection messages. - Designed a visually distinct layout for the rejection page. - Improved user experience with clear messaging and proper handling of invalid or unauthorized access. --- .../Components/Pages/ReceiverPage.razor | 16 +++ .../Pages/ReceiverRejectedPage.razor | 105 ++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverRejectedPage.razor diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverPage.razor index c7999655..5db25ac2 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverPage.razor @@ -6,6 +6,7 @@ @using EnvelopeGenerator.Server.Client.Models.Constants @using EnvelopeGenerator.Server.Client.Services @using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver +@using EnvelopeGenerator.Domain.Constants @using Microsoft.JSInterop @using DevExpress.Blazor @using System.Drawing @@ -510,6 +511,11 @@ _envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey); if (_envelopeReceiver is null) Logger.LogWarning("Envelope receiver data is null for {EnvelopeKey}", EnvelopeKey); + else if (IsRejected(_envelopeReceiver)) + { + Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}/rejected", forceLoad: true); + return; + } // Build initial report (no signature image yet) _report = BuildReport(_pdfBytes, _signatures, capturedSignature: null); @@ -555,6 +561,16 @@ } // ----- Report builder ----- + static bool IsRejected(EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto envelopeReceiver) + { + var status = envelopeReceiver.Envelope?.Status; + if (status is EnvelopeStatus.DocumentRejected or EnvelopeStatus.EnvelopeRejected) + return true; + + return envelopeReceiver.Envelope?.Histories?.Any(h => + h.Status is EnvelopeStatus.DocumentRejected or EnvelopeStatus.EnvelopeRejected) == true; + } + /// /// Builds an XtraReport wrapping the PDF bytes. /// If a signature is captured and there are signature fields, the signature image is diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverRejectedPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverRejectedPage.razor new file mode 100644 index 00000000..6b8df10d --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverRejectedPage.razor @@ -0,0 +1,105 @@ +@page "/envelope/{EnvelopeKey}/rejected" +@rendermode InteractiveServer +@using EnvelopeGenerator.Application.Resources +@using EnvelopeGenerator.Domain.Constants +@using EnvelopeGenerator.Domain.Interfaces +@using Microsoft.Extensions.Localization +@inject NavigationManager Navigation +@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService +@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService +@inject IStringLocalizer UiLocalizer + +
+
+
+
@L("DocRejected", "Dokument abgelehnt")
+
+ +
+
+
+ + + +
+
+ +

+ @(_isReadAndConfirm + ? L("RejectionInfo1Confirmation", "Dieses Dokument wurde als nicht bestaetigt markiert.") + : L("RejectionInfo1", "Dieses Dokument wurde abgelehnt.")) +

+ +

+ @((MarkupString)_messageLine2) +

+ + +
+
+
+ +@code { + [Parameter] public string? EnvelopeKey { get; set; } + + bool _isReadAndConfirm; + string _messageLine2 = string.Empty; + + protected override async Task OnInitializedAsync() + { + if (string.IsNullOrWhiteSpace(EnvelopeKey)) + { + Navigation.NavigateTo("/"); + return; + } + + var user = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey); + if (user is null) + { + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}"); + return; + } + + var envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey); + if (envelopeReceiver is null) + { + Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true); + return; + } + + if (!IsRejected(envelopeReceiver)) + { + Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true); + return; + } + + _isReadAndConfirm = envelopeReceiver.Envelope?.IsReadAndConfirm() ?? false; + + var senderName = envelopeReceiver.Envelope?.User is null + ? string.Empty + : $"{envelopeReceiver.Envelope.User.Prename} {envelopeReceiver.Envelope.User.Name}".Trim(); + var senderEmail = envelopeReceiver.Envelope?.User?.Email ?? string.Empty; + var envelopeTitle = envelopeReceiver.Envelope?.Title ?? string.Empty; + + var template = L("RejectionInfo2", "Das Dokument \"{2}\" von {0} ({1}) wurde abgelehnt."); + _messageLine2 = string.Format(template, senderName, senderEmail, envelopeTitle); + } + + static bool IsRejected(EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto envelopeReceiver) + { + var status = envelopeReceiver.Envelope?.Status; + if (status is EnvelopeStatus.DocumentRejected or EnvelopeStatus.EnvelopeRejected) + return true; + + return envelopeReceiver.Envelope?.Histories?.Any(h => + h.Status is EnvelopeStatus.DocumentRejected or EnvelopeStatus.EnvelopeRejected) == true; + } + + string L(string key, string fallback) + { + var value = UiLocalizer[key]; + return value.ResourceNotFound ? fallback : value.Value; + } +} From 4047ba7c6b6cc50e088cec549ef4b8b45d9ed691 Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 30 Sep 2026 16:33:19 +0200 Subject: [PATCH 3/4] Improve envelope status handling and UI enhancements Refactored status handling to use `GetEffectiveEnvelopeStatus` for better accuracy and historical context. Updated methods like `IsEnvelopeSent`, `IsEnvelopeEditable`, and others to use effective status instead of raw status. Enhanced receiver status display by adding logic for rejected receivers (`IsReceiverRejected`) and updated the UI to show rejection badges with icons and labels. Improved badge rendering to differentiate between signed, unsigned, and rejected states. Improved envelope categorization by ensuring rejected envelopes are not shown in the active tab. Introduced `GetEffectiveEnvelopeStatus` to centralize status logic. Sanitized grid layout filters to prevent persistence of filter-related properties, ensuring grids appear complete when restoring layouts. Added utility methods for clearing filter properties dynamically. Added comments and refactored code for better readability and maintainability. --- .../Components/Pages/EnvelopeSenderPage.razor | 123 ++++++++++++++++-- 1 file changed, 112 insertions(+), 11 deletions(-) diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor index 20c248ee..e9222313 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor @@ -262,7 +262,7 @@ var envelope = cellContext.DataItem as EnvelopeDto; if (envelope != null) { - var statusInfo = GetStatusInfo(envelope.Status); + var statusInfo = GetStatusInfo(GetEffectiveEnvelopeStatus(envelope));
@statusInfo.Label @@ -314,12 +314,20 @@ {
@{ + var isRejected = IsReceiverRejected(envelope, receiver); var isSigned = IsReceiverSigned(envelope, receiver); var accessCode = receiver.AccessCode; var hasAccessCode = !string.IsNullOrWhiteSpace(accessCode); } - - @if (isSigned) + + @if (isRejected) + { + + + + @GetStatusLabel(EnvelopeStatus.DocumentRejected) + } + else if (isSigned) { @@ -590,10 +598,11 @@ { _allEnvelopes = await EnvelopeService.GetAsync() ?? []; - // Split into active and completed based on status + // Split into active and completed based on effective status + // (history-aware: rejected envelopes should not stay in active tab). var envelopes = _allEnvelopes.ToList(); - _activeEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsActive()).ToList(); - _completedEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsCompleted()).ToList(); + _activeEnvelopes = envelopes.Where(e => GetEffectiveEnvelopeStatus(e).IsActive()).ToList(); + _completedEnvelopes = envelopes.Where(e => GetEffectiveEnvelopeStatus(e).IsCompleted()).ToList(); } catch (Exception ex) { @@ -745,13 +754,13 @@ bool IsEnvelopeSent(EnvelopeDto envelope) { - var status = (EnvelopeStatus)envelope.Status; + var status = GetEffectiveEnvelopeStatus(envelope); return status >= EnvelopeStatus.EnvelopeQueued; } bool IsEnvelopeEditable(EnvelopeDto envelope) { - var status = (EnvelopeStatus)envelope.Status; + var status = GetEffectiveEnvelopeStatus(envelope); return status is EnvelopeStatus.EnvelopeCreated or EnvelopeStatus.EnvelopeSaved; } @@ -800,7 +809,7 @@ IReadOnlyList GetStatusFilterItems(IEnumerable? source) { return (source ?? Enumerable.Empty()) - .Select(x => x.Status) + .Select(GetEffectiveEnvelopeStatus) .Distinct() .OrderBy(x => x) .Select(x => new StatusFilterItem(x, GetStatusLabel(x))) @@ -818,7 +827,7 @@ bool IsReceiverSigned(EnvelopeDto envelope, EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto receiver) { - var status = (EnvelopeStatus)envelope.Status; + var status = GetEffectiveEnvelopeStatus(envelope); if (status is EnvelopeStatus.EnvelopeCompletelySigned or EnvelopeStatus.MessageCompletionSent) return true; @@ -841,6 +850,43 @@ return false; } + bool IsReceiverRejected(EnvelopeDto envelope, EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto receiver) + { + var status = GetEffectiveEnvelopeStatus(envelope); + var histories = envelope.Histories?.Where(h => + h.Status is EnvelopeStatus.DocumentRejected or EnvelopeStatus.EnvelopeRejected).ToList() ?? []; + + if (histories.Count == 0) + return false; + + if (histories.Any(h => h.Receiver?.Id == receiver.ReceiverId)) + return true; + + var receiverEmail = receiver.Receiver?.EmailAddress; + if (!string.IsNullOrWhiteSpace(receiverEmail) && + histories.Any(h => string.Equals(h.UserReference, receiverEmail, StringComparison.OrdinalIgnoreCase))) + return true; + + if (!string.IsNullOrWhiteSpace(receiver.Name) && + histories.Any(h => string.Equals(h.UserReference, receiver.Name, StringComparison.OrdinalIgnoreCase))) + return true; + + return status == EnvelopeStatus.EnvelopeRejected && (envelope.EnvelopeReceivers?.Count() ?? 0) == 1; + } + + EnvelopeStatus GetEffectiveEnvelopeStatus(EnvelopeDto envelope) + { + var rawStatus = envelope.Status; + + if (rawStatus is EnvelopeStatus.EnvelopeRejected or EnvelopeStatus.EnvelopeWithdrawn) + return rawStatus; + + var hasRejectedHistory = (envelope.Histories ?? Enumerable.Empty()) + .Any(h => h.Status is EnvelopeStatus.DocumentRejected or EnvelopeStatus.EnvelopeRejected); + + return hasRejectedHistory ? EnvelopeStatus.EnvelopeRejected : rawStatus; + } + DateTime GetHistoryDate(HistoryDto history) { return history.ActionDate ?? history.AddedWhen; @@ -983,13 +1029,18 @@ // If cache is empty (LoadFromServerAsync failed), skip layout restore silently. var prefs = UserPreferencesService.GetCached(); if (prefs is not null && prefs.GridLayouts.TryGetValue(CurrentGridKey, out var json)) - e.Layout = JsonSerializer.Deserialize(json); + { + var layout = JsonSerializer.Deserialize(json); + SanitizeLayoutFilters(layout); + e.Layout = layout; + } } async Task OnGridLayoutAutoSaving(GridPersistentLayoutEventArgs e) { try { + SanitizeLayoutFilters(e.Layout); var json = JsonSerializer.Serialize(e.Layout); await UserPreferencesService.SaveGridLayoutAsync(CurrentGridKey, json); } @@ -999,5 +1050,55 @@ } } + // Keep column/order personalization, but never persist/restore data filters. + // Tab counters are based on tab datasets; persisted grid filters can hide rows and + // make the grid appear empty or incomplete. + static void SanitizeLayoutFilters(GridPersistentLayout? layout) + { + if (layout is null) + return; + + ClearWritableProperty(layout, "FilterCriteria"); + ClearWritableProperty(layout, "SearchText"); + + if (layout.Columns is not System.Collections.IEnumerable columns) + return; + + foreach (var column in columns) + { + if (column is null) + continue; + + ClearWritableProperty(column, "FilterCriteria"); + ClearWritableProperty(column, "FilterValues"); + ClearWritableProperty(column, "FilterValue"); + ClearWritableProperty(column, "SearchText"); + } + } + + static void ClearWritableProperty(object target, string propertyName) + { + var prop = target.GetType() + .GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public) + .FirstOrDefault(p => p.Name == propertyName && p.GetIndexParameters().Length == 0); + + if (prop is null || !prop.CanWrite) + return; + + var propertyType = prop.PropertyType; + object? value; + + if (!propertyType.IsValueType || Nullable.GetUnderlyingType(propertyType) is not null) + { + value = null; + } + else + { + value = Activator.CreateInstance(propertyType); + } + + prop.SetValue(target, value); + } + private sealed record StatusFilterItem(EnvelopeStatus Value, string Text); } From 96c7cd6e8a04addc9853df259ed2289ebee574a1 Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 30 Sep 2026 16:34:06 +0200 Subject: [PATCH 4/4] Add "Technical Debt TODOs" section to AGENTS.md Added a new section in AGENTS.md to document technical debt tasks. Outlined three specific areas for improvement: - Refactor `EnvelopeSenderPage.razor` to reduce complexity. - Move sender dashboard logic to dedicated services/extensions. - Keep `EnvelopeSenderPage.razor` focused on UI/state orchestration. These changes aim to improve code modularity, maintainability, and separation of concerns. --- AGENTS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 6c287f5d..49d4d4db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -236,6 +236,12 @@ Manual testing workflow: 3. Test sender login at `/sender/login` 4. Test receiver flow at `/envelope/login/{envelopeKey}` +## Technical Debt TODOs + +- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor` to reduce complexity. +- Move sender dashboard business/status logic into dedicated services and extension methods (for example effective status resolution, tab classification, receiver signed/rejected detection, and grid-layout filter sanitation). +- Keep `EnvelopeSenderPage.razor` focused on UI composition/state orchestration; avoid embedding heavy domain logic directly in the Razor component. + ## Database **SQL Server** (DD_ECM)