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); }