Refactor: Unify grid logic for active/completed tabs
Replaced separate `DxGrid` components for "active" and "completed" tabs with a single unified grid. Introduced dynamic properties (`CurrentGridData` and `CurrentGridKey`) to handle data and layout switching based on the active tab. Refactored layout loading/saving methods into shared versions (`OnGridLayoutAutoLoading` and `OnGridLayoutAutoSaving`) using dynamic keys. Simplified export logic to use the unified grid instance. Removed redundant code, templates, and private fields specific to the "completed" tab. Updated status filter logic to dynamically fetch data. Improved maintainability by consolidating logic and reducing duplication.
This commit is contained in:
@@ -214,16 +214,15 @@
|
||||
</div>
|
||||
|
||||
<div class="sender-grid-wrapper">
|
||||
@if (_activeTab == "active")
|
||||
{
|
||||
<DxGrid Data="@_activeEnvelopes"
|
||||
@ref="_gridActive"
|
||||
<DxGrid @key="_activeTab"
|
||||
Data="@CurrentGridData"
|
||||
@ref="_grid"
|
||||
KeyFieldName="Id"
|
||||
ShowFilterRow="false"
|
||||
FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always"
|
||||
ShowSearchBox="true"
|
||||
AllowColumnReorder="true"
|
||||
AllowSort=true
|
||||
AllowSort="true"
|
||||
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||
PageSize="20"
|
||||
PagerVisible="true"
|
||||
@@ -234,8 +233,8 @@
|
||||
RowClick="OnRowClick"
|
||||
RowDoubleClick="OnRowDoubleClick"
|
||||
CustomizeElement="OnCustomizeElement"
|
||||
LayoutAutoLoading="OnActiveGridLayoutAutoLoading"
|
||||
LayoutAutoSaving="OnActiveGridLayoutAutoSaving">
|
||||
LayoutAutoLoading="OnGridLayoutAutoLoading"
|
||||
LayoutAutoSaving="OnGridLayoutAutoSaving">
|
||||
<Columns>
|
||||
<DxGridDataColumn FieldName="Id" Caption="ID"
|
||||
DisplayFormat="0"
|
||||
@@ -252,7 +251,7 @@
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="Status" Caption="@L("Status", "Status")" ExportEnabled="false">
|
||||
<EditSettings>
|
||||
<DxComboBoxSettings Data="@GetStatusFilterItems(_activeEnvelopes)"
|
||||
<DxComboBoxSettings Data="@GetStatusFilterItems(CurrentGridData)"
|
||||
ValueFieldName="Value"
|
||||
TextFieldName="Text"
|
||||
SearchMode="ListSearchMode.AutoSearch"
|
||||
@@ -382,173 +381,6 @@
|
||||
</div>
|
||||
</DetailRowTemplate>
|
||||
</DxGrid>
|
||||
}
|
||||
else
|
||||
{
|
||||
<DxGrid Data="@_completedEnvelopes"
|
||||
@ref="_gridCompleted"
|
||||
KeyFieldName="Id"
|
||||
ShowFilterRow="false"
|
||||
FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always"
|
||||
ShowSearchBox="true"
|
||||
PageSize="20"
|
||||
PagerVisible="true"
|
||||
SelectionMode="GridSelectionMode.Single"
|
||||
AllowSelectRowByClick="true"
|
||||
SelectedDataItem="@_selectedEnvelope"
|
||||
SelectedDataItemChanged="@((object o) => _selectedEnvelope = o as EnvelopeDto)"
|
||||
RowClick="OnRowClick"
|
||||
RowDoubleClick="OnRowDoubleClick"
|
||||
CustomizeElement="OnCustomizeElement"
|
||||
LayoutAutoLoading="OnCompletedGridLayoutAutoLoading"
|
||||
LayoutAutoSaving="OnCompletedGridLayoutAutoSaving">
|
||||
<Columns>
|
||||
<DxGridDataColumn FieldName="Id" Caption="ID"
|
||||
DisplayFormat="0"
|
||||
SortIndex="0"
|
||||
SortOrder="GridColumnSortOrder.Descending">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@((cellContext.DataItem as EnvelopeDto)?.Id)
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="Title" Caption="@L("Title", "Titel")">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="Status" Caption="@L("Status", "Status")" ExportEnabled="false">
|
||||
<EditSettings>
|
||||
<DxComboBoxSettings Data="@GetStatusFilterItems(_completedEnvelopes)"
|
||||
ValueFieldName="Value"
|
||||
TextFieldName="Text"
|
||||
SearchMode="ListSearchMode.AutoSearch"
|
||||
SearchFilterCondition="ListSearchFilterCondition.Contains" />
|
||||
</EditSettings>
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
if (envelope != null)
|
||||
{
|
||||
var statusInfo = GetStatusInfo(envelope.Status);
|
||||
<div class="status-badge status-badge--@statusInfo.CssClass">
|
||||
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
||||
@statusInfo.Label
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="@L("Receiver", "Empfänger")" ExportEnabled="false">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
if (envelope != null)
|
||||
{
|
||||
var receivers = envelope.EnvelopeReceivers?.ToList() ?? [];
|
||||
var signed = GetSignedReceiverCount(envelope);
|
||||
var total = receivers.Count;
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span style="font-size: 0.875rem; color: #6b7280;">
|
||||
@signed / @total @L("Signed", "unterschrieben")
|
||||
</span>
|
||||
@if (total > 0)
|
||||
{
|
||||
<div style="flex: 1; min-width: 60px; max-width: 120px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden;">
|
||||
<div style="height: 100%; background: linear-gradient(90deg, #81c784 0%, #66bb6a 100%); width: @((signed * 100.0 / total).ToString("F0"))%;"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="EnvelopeTypeTitle" Caption="@L("Type", "Typ")"
|
||||
Width="120px" AllowSort="true" />
|
||||
<DxGridDataColumn FieldName="AddedWhen" Caption="@L("Created", "Erstellt")"
|
||||
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
||||
<DxGridDataColumn FieldName="ChangedWhen" Caption="@L("ChangedAt", "Geändert am")"
|
||||
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
||||
</Columns>
|
||||
<DetailRowTemplate Context="detailContext">
|
||||
<div style="padding: 1rem; background: #f9fafb;">
|
||||
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">@L("Receiver", "Empfänger")</h6>
|
||||
@{
|
||||
var envelope = detailContext.DataItem as EnvelopeDto;
|
||||
if (envelope?.EnvelopeReceivers?.Any() == true)
|
||||
{
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
@foreach (var receiver in envelope.EnvelopeReceivers)
|
||||
{
|
||||
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
||||
@{
|
||||
var isSigned = IsReceiverSigned(envelope, receiver);
|
||||
}
|
||||
<span class="receiver-badge receiver-badge--@(isSigned ? "signed" : "unsigned")" style="min-width: 100px;">
|
||||
@if (isSigned)
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
||||
</svg>
|
||||
<span>@L("Signed", "Unterschrieben")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
||||
</svg>
|
||||
<span>@L("Pending", "Ausstehend")</span>
|
||||
}
|
||||
</span>
|
||||
<div style="flex: 1; font-size: 0.875rem;">
|
||||
<strong style="color: #1f2937;">@receiver.Name</strong>
|
||||
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Receiver?.EmailAddress</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">@L("Sender_NoReceivers", "Keine Empfänger")</p>
|
||||
}
|
||||
}
|
||||
|
||||
<h6 style="font-weight: 600; color: #374151; margin: 1rem 0 0.75rem;">@L("History", "Historie")</h6>
|
||||
@{
|
||||
var historyEntries = envelope is null
|
||||
? Enumerable.Empty<HistoryDto>()
|
||||
: GetOrderedHistories(envelope);
|
||||
if (historyEntries.Any())
|
||||
{
|
||||
<div class="sender-history-list">
|
||||
@foreach (var history in historyEntries)
|
||||
{
|
||||
var statusInfo = GetStatusInfo(history.Status);
|
||||
<div class="sender-history-item">
|
||||
<div class="sender-history-item__left">
|
||||
<div class="status-badge status-badge--@statusInfo.CssClass">
|
||||
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
||||
@statusInfo.Label
|
||||
</div>
|
||||
<span class="sender-history-item__actor">@GetHistoryActor(history)</span>
|
||||
</div>
|
||||
<div class="sender-history-item__right">
|
||||
@GetHistoryDate(history).ToString("dd.MM.yyyy HH:mm")
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">@L("Sender_NoHistory", "Keine Historie vorhanden")</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</DetailRowTemplate>
|
||||
</DxGrid>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -673,8 +505,7 @@
|
||||
private bool _isLoading = true;
|
||||
private bool _isLoggingOut = false;
|
||||
private string? _errorMessage;
|
||||
private DxGrid? _gridActive;
|
||||
private DxGrid? _gridCompleted;
|
||||
private DxGrid? _grid;
|
||||
|
||||
// Logo config loaded from /api/CustomImages after auth
|
||||
private EnvelopeGenerator.Server.Client.Services.CustomImageDto? _appLogo;
|
||||
@@ -697,10 +528,15 @@
|
||||
private string _deleteReason = string.Empty;
|
||||
private string? _deleteErrorMessage;
|
||||
|
||||
// Stable keys used to store/restore each grid's layout in UserPreferences.GridLayouts
|
||||
// Stable keys used to store/restore each tab's layout in UserPreferences.GridLayouts
|
||||
private const string GridKeyActive = "sender.active-envelopes";
|
||||
private const string GridKeyCompleted = "sender.completed-envelopes";
|
||||
|
||||
private IEnumerable<EnvelopeDto> CurrentGridData =>
|
||||
(_activeTab == "completed" ? _completedEnvelopes : _activeEnvelopes) ?? Enumerable.Empty<EnvelopeDto>();
|
||||
|
||||
private string CurrentGridKey => _activeTab == "completed" ? GridKeyCompleted : GridKeyActive;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var hasAccess = await AuthService.CheckSenderAccessAsync();
|
||||
@@ -765,7 +601,7 @@
|
||||
|
||||
async Task ExportSelectedFormatAsync()
|
||||
{
|
||||
var grid = _activeTab == "active" ? _gridActive : _gridCompleted;
|
||||
var grid = _grid;
|
||||
if (grid is null)
|
||||
return;
|
||||
|
||||
@@ -1106,21 +942,21 @@
|
||||
// UserPreferencesService.GetAsync() uses the 1h in-memory cache → 0 HTTP on restore.
|
||||
// UserPreferencesService.SaveGridLayoutAsync() merges the single key → 1 PUT.
|
||||
|
||||
async Task OnActiveGridLayoutAutoLoading(GridPersistentLayoutEventArgs e)
|
||||
async Task OnGridLayoutAutoLoading(GridPersistentLayoutEventArgs e)
|
||||
{
|
||||
// Use cached preferences only — avoid HTTP during grid initialization.
|
||||
// If cache is empty (LoadFromServerAsync failed), skip layout restore silently.
|
||||
var prefs = UserPreferencesService.GetCached();
|
||||
if (prefs is not null && prefs.GridLayouts.TryGetValue(GridKeyActive, out var json))
|
||||
if (prefs is not null && prefs.GridLayouts.TryGetValue(CurrentGridKey, out var json))
|
||||
e.Layout = JsonSerializer.Deserialize<GridPersistentLayout>(json);
|
||||
}
|
||||
|
||||
async Task OnActiveGridLayoutAutoSaving(GridPersistentLayoutEventArgs e)
|
||||
async Task OnGridLayoutAutoSaving(GridPersistentLayoutEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(e.Layout);
|
||||
await UserPreferencesService.SaveGridLayoutAsync(GridKeyActive, json);
|
||||
await UserPreferencesService.SaveGridLayoutAsync(CurrentGridKey, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1128,18 +964,5 @@
|
||||
}
|
||||
}
|
||||
|
||||
async Task OnCompletedGridLayoutAutoLoading(GridPersistentLayoutEventArgs e)
|
||||
{
|
||||
var prefs = UserPreferencesService.GetCached();
|
||||
if (prefs is not null && prefs.GridLayouts.TryGetValue(GridKeyCompleted, out var json))
|
||||
e.Layout = JsonSerializer.Deserialize<GridPersistentLayout>(json);
|
||||
}
|
||||
|
||||
async Task OnCompletedGridLayoutAutoSaving(GridPersistentLayoutEventArgs e)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(e.Layout);
|
||||
await UserPreferencesService.SaveGridLayoutAsync(GridKeyCompleted, json);
|
||||
}
|
||||
|
||||
private sealed record StatusFilterItem(EnvelopeStatus Value, string Text);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user