Introduce custom filter row cell templates for date and numeric columns in CatalogsGrid and MassDataGrid. Users can now select a filter operator (e.g., =, <, >, <=, >=) alongside the filter value for columns like AddedWhen, ChangedWhen, and Amount. Updated filter criteria logic to support these operators using DevExpress expressions. Added new CSS for filter row alignment and appearance. Removed obsolete methods and improved layout logic. This enhances filtering flexibility and user experience in both grids.
898 lines
30 KiB
Plaintext
898 lines
30 KiB
Plaintext
@using System.Text.Json
|
|
@using Microsoft.AspNetCore.Components
|
|
@using Microsoft.AspNetCore.Components.Rendering
|
|
@using Microsoft.AspNetCore.Components.Forms
|
|
@using DevExpress.Blazor
|
|
@using DevExpress.Data.Filtering
|
|
@inject CatalogApiClient Api
|
|
@inject LayoutApiClient LayoutApi
|
|
@inject IJSRuntime JsRuntime
|
|
|
|
<style>
|
|
.action-panel { margin-bottom: 16px; }
|
|
.grid-section { margin-top: 12px; }
|
|
.catalog-grid .dxbl-grid-sort-asc,
|
|
.catalog-grid .dxbl-grid-sort-desc {
|
|
display: none;
|
|
}
|
|
.catalog-grid th.dxbl-grid-header-sortable {
|
|
position: relative;
|
|
padding-right: 1.5rem;
|
|
}
|
|
.catalog-grid th.dxbl-grid-header-sortable::before,
|
|
.catalog-grid th.dxbl-grid-header-sortable::after {
|
|
content: "";
|
|
position: absolute;
|
|
right: 0.45rem;
|
|
width: 0.7rem;
|
|
height: 0.7rem;
|
|
background-repeat: no-repeat;
|
|
background-size: 0.7rem 0.7rem;
|
|
opacity: 0.35;
|
|
pointer-events: none;
|
|
}
|
|
.catalog-grid th.dxbl-grid-header-sortable::before {
|
|
top: 38%;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.957 10.999a1 1 0 0 1-.821-1.571l2.633-3.785a1.5 1.5 0 0 1 2.462 0l2.633 3.785a1 1 0 0 1-.821 1.57H4.957Z' fill='%23888888'/%3E%3C/svg%3E");
|
|
}
|
|
.catalog-grid th.dxbl-grid-header-sortable::after {
|
|
top: 58%;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.957 5a1 1 0 0 0-.821 1.571l2.633 3.784a1.5 1.5 0 0 0 2.462 0l2.633-3.784A1 1 0 0 0 11.043 5H4.957Z' fill='%23888888'/%3E%3C/svg%3E");
|
|
}
|
|
.catalog-grid th.dxbl-grid-header-sortable[aria-sort="ascending"]::after {
|
|
opacity: 0;
|
|
}
|
|
.catalog-grid th.dxbl-grid-header-sortable[aria-sort="descending"]::before {
|
|
opacity: 0;
|
|
}
|
|
.catalog-grid .filter-search-input input {
|
|
padding-right: 1.75rem;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M9.309 10.016a4.5 4.5 0 1 1 .707-.707l3.838 3.837a.5.5 0 0 1-.708.708L9.31 10.016ZM10 6.5a3.5 3.5 0 1 0-7 0 3.5 3.5 0 0 0 7 0Z' fill='%23666666'/%3E%3C/svg%3E");
|
|
background-repeat: no-repeat;
|
|
background-position: right 0.5rem center;
|
|
background-size: 0.9rem;
|
|
}
|
|
.catalog-edit-popup {
|
|
min-width: 720px;
|
|
}
|
|
.band-editor {
|
|
display: grid;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
}
|
|
.band-controls {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
.band-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
.band-columns {
|
|
max-width: 720px;
|
|
}
|
|
.filter-row-cell {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.filter-operator {
|
|
width: 52px;
|
|
min-width: 52px;
|
|
flex: 0 0 52px;
|
|
}
|
|
.filter-value {
|
|
min-width: 140px;
|
|
flex: 1 1 140px;
|
|
}
|
|
</style>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
|
{
|
|
<div class="alert alert-danger" role="alert">@errorMessage</div>
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(infoMessage))
|
|
{
|
|
<div class="alert alert-success" role="alert">@infoMessage</div>
|
|
}
|
|
|
|
@if (isLoading)
|
|
{
|
|
<p><em>Lade Daten...</em></p>
|
|
}
|
|
else if (items.Count == 0)
|
|
{
|
|
<p>Keine Einträge vorhanden.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="band-editor">
|
|
<div class="band-controls">
|
|
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
|
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
|
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
|
</div>
|
|
@foreach (var band in bandLayout.Bands)
|
|
{
|
|
<div class="band-row">
|
|
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
|
|
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
|
|
</div>
|
|
}
|
|
<DxFormLayout CssClass="band-columns" ColCount="2">
|
|
@foreach (var column in columnDefinitions)
|
|
{
|
|
<DxFormLayoutItem Caption="@column.Caption">
|
|
<DxComboBox Data="@bandOptions"
|
|
TData="BandOption"
|
|
TValue="string"
|
|
TextFieldName="Caption"
|
|
ValueFieldName="Id"
|
|
Value="@GetColumnBand(column.FieldName)"
|
|
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
|
|
Width="100%" />
|
|
</DxFormLayoutItem>
|
|
}
|
|
</DxFormLayout>
|
|
</div>
|
|
|
|
<div class="grid-section">
|
|
<DxGrid Data="@items"
|
|
TItem="CatalogReadDto"
|
|
KeyFieldName="@nameof(CatalogReadDto.Guid)"
|
|
ShowGroupPanel="true"
|
|
ShowGroupedColumns="true"
|
|
AllowGroup="true"
|
|
ShowFilterRow="true"
|
|
AllowColumnResize="true"
|
|
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
|
AllowColumnReorder="true"
|
|
PageSize="10"
|
|
CssClass="mb-4 catalog-grid"
|
|
EditMode="GridEditMode.PopupEditForm"
|
|
PopupEditFormCssClass="catalog-edit-popup"
|
|
PopupEditFormHeaderText="@popupHeaderText"
|
|
CustomizeEditModel="OnCustomizeEditModel"
|
|
EditModelSaving="OnEditModelSaving"
|
|
DataItemDeleting="OnDataItemDeleting"
|
|
@ref="gridRef">
|
|
<Columns>
|
|
@RenderColumns()
|
|
</Columns>
|
|
<EditFormTemplate Context="editFormContext">
|
|
@{ SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
|
|
<DxFormLayout ColCount="2">
|
|
<DxFormLayoutItem Caption="Titel">
|
|
<DxTextBox @bind-Text="editModel.CatTitle" Width="100%" />
|
|
</DxFormLayoutItem>
|
|
<DxFormLayoutItem Caption="Kennung">
|
|
<DxTextBox @bind-Text="editModel.CatString" Width="100%" />
|
|
</DxFormLayoutItem>
|
|
@if (!editModel.IsNew)
|
|
{
|
|
<DxFormLayoutItem Caption="Update-Prozedur">
|
|
<DxComboBox Data="@procedureOptions"
|
|
TData="ProcedureOption"
|
|
TValue="int"
|
|
TextFieldName="Text"
|
|
ValueFieldName="Value"
|
|
@bind-Value="editModel.UpdateProcedure"
|
|
Width="100%" />
|
|
</DxFormLayoutItem>
|
|
}
|
|
<DxFormLayoutItem ColSpanMd="12">
|
|
<ValidationSummary />
|
|
</DxFormLayoutItem>
|
|
</DxFormLayout>
|
|
</EditFormTemplate>
|
|
</DxGrid>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
private List<CatalogReadDto> items = new();
|
|
private bool isLoading;
|
|
private string? errorMessage;
|
|
private string? infoMessage;
|
|
private EditContext? editContext;
|
|
private ValidationMessageStore? validationMessageStore;
|
|
private IGrid? gridRef;
|
|
private string popupHeaderText = "Edit";
|
|
private DateTime? addedWhenFilterValue;
|
|
private string addedWhenFilterOperator = "=";
|
|
private DateTime? changedWhenFilterValue;
|
|
private string changedWhenFilterOperator = "=";
|
|
private const string LayoutType = "GRID_BANDS";
|
|
private const string LayoutKey = "CatalogsGrid";
|
|
private const string LayoutUserStorageKey = "layoutUser";
|
|
private string? layoutUser;
|
|
private BandLayout bandLayout = new();
|
|
private Dictionary<string, string> columnBandAssignments = new();
|
|
private List<BandOption> bandOptions = new();
|
|
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
|
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
|
|
private List<ColumnDefinition> columnDefinitions = new()
|
|
{
|
|
new() { FieldName = nameof(CatalogReadDto.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text },
|
|
new() { FieldName = nameof(CatalogReadDto.CatTitle), Caption = "Titel", FilterType = ColumnFilterType.Text },
|
|
new() { FieldName = nameof(CatalogReadDto.CatString), Caption = "String", FilterType = ColumnFilterType.Text },
|
|
new() { FieldName = nameof(CatalogReadDto.AddedWho), Caption = "Angelegt von", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
|
new() { FieldName = nameof(CatalogReadDto.AddedWhen), Caption = "Angelegt am", ReadOnly = true, FilterType = ColumnFilterType.Date },
|
|
new() { FieldName = nameof(CatalogReadDto.ChangedWho), Caption = "Geändert von", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
|
new() { FieldName = nameof(CatalogReadDto.ChangedWhen), Caption = "Geändert am", ReadOnly = true, FilterType = ColumnFilterType.Date }
|
|
};
|
|
|
|
private readonly List<ProcedureOption> procedureOptions = new()
|
|
{
|
|
new() { Value = 0, Text = "PRTBMY_CATALOG_UPDATE" },
|
|
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
|
|
};
|
|
|
|
private readonly List<FilterOperatorOption> filterOperators = new()
|
|
{
|
|
new() { Value = "=", Text = "=" },
|
|
new() { Value = "<", Text = "<" },
|
|
new() { Value = ">", Text = ">" },
|
|
new() { Value = "<=", Text = "<=" },
|
|
new() { Value = ">=", Text = ">=" }
|
|
};
|
|
|
|
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
|
private bool gridLayoutApplied;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
|
await EnsureLayoutUserAsync();
|
|
await LoadBandLayoutAsync();
|
|
await LoadCatalogs();
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
|
{
|
|
gridRef.LoadLayout(bandLayout.GridLayout);
|
|
gridLayoutApplied = true;
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
|
|
private void SetEditContext(EditContext context)
|
|
{
|
|
if (editContext == context)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (editContext != null)
|
|
{
|
|
editContext.OnFieldChanged -= OnEditFieldChanged;
|
|
}
|
|
|
|
editContext = context;
|
|
validationMessageStore = new ValidationMessageStore(editContext);
|
|
editContext.OnFieldChanged += OnEditFieldChanged;
|
|
}
|
|
|
|
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
|
{
|
|
if (validationMessageStore == null || editContext == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.UpdateProcedure))
|
|
{
|
|
validationMessageStore.Clear();
|
|
editContext.NotifyValidationStateChanged();
|
|
return;
|
|
}
|
|
|
|
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.CatTitle))
|
|
{
|
|
var field = new FieldIdentifier(editContext.Model, nameof(CatalogEditModel.CatTitle));
|
|
validationMessageStore.Clear(field);
|
|
editContext.NotifyValidationStateChanged();
|
|
}
|
|
}
|
|
|
|
private void SetPopupHeaderText(bool isNew)
|
|
{
|
|
popupHeaderText = isNew ? "Neu" : "Edit";
|
|
}
|
|
|
|
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
|
{
|
|
popupHeaderText = e.IsNew ? "Neu" : "Edit";
|
|
if (e.IsNew)
|
|
{
|
|
e.EditModel = new CatalogEditModel { IsNew = true };
|
|
return;
|
|
}
|
|
|
|
var item = (CatalogReadDto)e.DataItem;
|
|
e.EditModel = new CatalogEditModel
|
|
{
|
|
Guid = item.Guid,
|
|
CatTitle = item.CatTitle,
|
|
CatString = item.CatString,
|
|
UpdateProcedure = 0,
|
|
OriginalCatTitle = item.CatTitle,
|
|
IsNew = false
|
|
};
|
|
}
|
|
|
|
private async Task LoadCatalogs()
|
|
{
|
|
isLoading = true;
|
|
errorMessage = null;
|
|
try
|
|
{
|
|
items = await Api.GetAllAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errorMessage = $"Kataloge konnten nicht geladen werden: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
isLoading = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
|
|
{
|
|
errorMessage = null;
|
|
infoMessage = null;
|
|
|
|
validationMessageStore?.Clear();
|
|
editContext?.NotifyValidationStateChanged();
|
|
|
|
var editModel = (CatalogEditModel)e.EditModel;
|
|
if (!ValidateEditModel(editModel, e.IsNew))
|
|
{
|
|
e.Cancel = true;
|
|
return;
|
|
}
|
|
|
|
var dto = new CatalogWriteDto
|
|
{
|
|
CatTitle = editModel.CatTitle,
|
|
CatString = editModel.CatString,
|
|
UpdateProcedure = editModel.UpdateProcedure
|
|
};
|
|
|
|
try
|
|
{
|
|
if (e.IsNew)
|
|
{
|
|
var created = await Api.CreateAsync(dto);
|
|
if (!created.Success || created.Value == null)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(created.Error))
|
|
{
|
|
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), created.Error);
|
|
}
|
|
else
|
|
{
|
|
errorMessage = "Anlegen fehlgeschlagen.";
|
|
}
|
|
|
|
e.Cancel = true;
|
|
return;
|
|
}
|
|
|
|
infoMessage = "Katalog angelegt.";
|
|
}
|
|
else
|
|
{
|
|
var updated = await Api.UpdateAsync(editModel.Guid, dto);
|
|
if (!updated.Success)
|
|
{
|
|
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
|
|
e.Cancel = true;
|
|
return;
|
|
}
|
|
|
|
infoMessage = "Katalog aktualisiert.";
|
|
}
|
|
|
|
await LoadCatalogs();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errorMessage = $"Fehler beim Speichern: {ex.Message}";
|
|
e.Cancel = true;
|
|
}
|
|
}
|
|
|
|
private void AddValidationError(CatalogEditModel editModel, string fieldName, string message)
|
|
{
|
|
if (editContext == null || validationMessageStore == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var field = new FieldIdentifier(editModel, fieldName);
|
|
validationMessageStore.Add(field, message);
|
|
editContext.NotifyValidationStateChanged();
|
|
}
|
|
|
|
private bool ValidateEditModel(CatalogEditModel editModel, bool isNew)
|
|
{
|
|
if (isNew)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (editModel.UpdateProcedure == 0 &&
|
|
!string.Equals(editModel.CatTitle, editModel.OriginalCatTitle, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), "Titel kann nicht geändert werden.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private async Task OnDataItemDeleting(GridDataItemDeletingEventArgs e)
|
|
{
|
|
errorMessage = null;
|
|
infoMessage = null;
|
|
|
|
var item = (CatalogReadDto)e.DataItem;
|
|
|
|
try
|
|
{
|
|
var deleted = await Api.DeleteAsync(item.Guid);
|
|
if (!deleted.Success)
|
|
{
|
|
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
|
|
e.Cancel = true;
|
|
return;
|
|
}
|
|
|
|
infoMessage = "Katalog gelöscht.";
|
|
await LoadCatalogs();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errorMessage = $"Fehler beim Löschen: {ex.Message}";
|
|
e.Cancel = true;
|
|
}
|
|
}
|
|
|
|
private async Task EnsureLayoutUserAsync()
|
|
{
|
|
layoutUser = await JsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
|
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
{
|
|
layoutUser = Guid.NewGuid().ToString("N");
|
|
await JsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
|
|
}
|
|
}
|
|
|
|
private async Task LoadBandLayoutAsync()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
{
|
|
bandLayout = new BandLayout();
|
|
UpdateBandOptions();
|
|
return;
|
|
}
|
|
|
|
var stored = await LayoutApi.GetAsync(LayoutType, LayoutKey, layoutUser);
|
|
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
|
|
{
|
|
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, jsonOptions);
|
|
bandLayout = NormalizeBandLayout(parsed);
|
|
}
|
|
else
|
|
{
|
|
bandLayout = new BandLayout();
|
|
}
|
|
|
|
columnBandAssignments = BuildAssignmentsFromLayout(bandLayout);
|
|
ApplyColumnLayoutFromStorage();
|
|
UpdateBandOptions();
|
|
}
|
|
|
|
private async Task SaveLayoutAsync()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
CaptureColumnLayoutFromGrid();
|
|
|
|
var layoutData = JsonSerializer.Serialize(bandLayout, jsonOptions);
|
|
await LayoutApi.UpsertAsync(new LayoutDto
|
|
{
|
|
LayoutType = LayoutType,
|
|
LayoutKey = LayoutKey,
|
|
UserName = layoutUser,
|
|
LayoutData = layoutData
|
|
});
|
|
infoMessage = "Layout gespeichert.";
|
|
errorMessage = null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
|
}
|
|
}
|
|
|
|
private void CaptureColumnLayoutFromGrid()
|
|
{
|
|
if (gridRef == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var layout = gridRef.SaveLayout();
|
|
bandLayout.GridLayout = layout;
|
|
|
|
var orderedColumns = layout.Columns
|
|
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
|
|
.OrderBy(column => column.VisibleIndex)
|
|
.ToList();
|
|
|
|
bandLayout.ColumnOrder = orderedColumns
|
|
.Select(column => column.FieldName)
|
|
.ToList();
|
|
|
|
bandLayout.ColumnWidths = orderedColumns
|
|
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
|
|
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private async Task ResetBandLayoutAsync()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
{
|
|
return;
|
|
}
|
|
|
|
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
|
|
bandLayout = new BandLayout();
|
|
columnBandAssignments.Clear();
|
|
UpdateBandOptions();
|
|
infoMessage = "Band-Layout zurückgesetzt.";
|
|
}
|
|
|
|
private void ApplyColumnLayoutFromStorage()
|
|
{
|
|
foreach (var column in columnDefinitions)
|
|
{
|
|
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
|
{
|
|
column.Width = width;
|
|
}
|
|
}
|
|
|
|
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private void AddBand()
|
|
{
|
|
bandLayout.Bands.Add(new BandDefinition
|
|
{
|
|
Id = Guid.NewGuid().ToString("N"),
|
|
Caption = "Band"
|
|
});
|
|
UpdateBandOptions();
|
|
}
|
|
|
|
private void RemoveBand(BandDefinition band)
|
|
{
|
|
bandLayout.Bands.Remove(band);
|
|
var removedColumns = columnBandAssignments.Where(pair => pair.Value == band.Id)
|
|
.Select(pair => pair.Key)
|
|
.ToList();
|
|
foreach (var column in removedColumns)
|
|
{
|
|
columnBandAssignments.Remove(column);
|
|
}
|
|
UpdateBandOptions();
|
|
SyncBandsFromAssignments();
|
|
}
|
|
|
|
private void UpdateBandCaption(BandDefinition band, string value)
|
|
{
|
|
band.Caption = value;
|
|
UpdateBandOptions();
|
|
}
|
|
|
|
private void UpdateColumnBand(string fieldName, string? bandId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(bandId))
|
|
{
|
|
columnBandAssignments.Remove(fieldName);
|
|
}
|
|
else
|
|
{
|
|
columnBandAssignments[fieldName] = bandId;
|
|
}
|
|
|
|
SyncBandsFromAssignments();
|
|
}
|
|
|
|
private string GetColumnBand(string fieldName)
|
|
{
|
|
return columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
|
}
|
|
|
|
private void SyncBandsFromAssignments()
|
|
{
|
|
foreach (var band in bandLayout.Bands)
|
|
{
|
|
band.Columns = columnDefinitions
|
|
.Where(column => columnBandAssignments.TryGetValue(column.FieldName, out var bandId) && bandId == band.Id)
|
|
.Select(column => column.FieldName)
|
|
.ToList();
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void UpdateBandOptions()
|
|
{
|
|
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
|
bandOptions.AddRange(bandLayout.Bands.Select(band => new BandOption { Id = band.Id, Caption = band.Caption }));
|
|
}
|
|
|
|
private BandLayout NormalizeBandLayout(BandLayout? layout)
|
|
{
|
|
layout ??= new BandLayout();
|
|
layout.Bands ??= new List<BandDefinition>();
|
|
layout.ColumnOrder ??= new List<string>();
|
|
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var band in layout.Bands)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(band.Id))
|
|
{
|
|
band.Id = Guid.NewGuid().ToString("N");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(band.Caption))
|
|
{
|
|
band.Caption = "Band";
|
|
}
|
|
|
|
band.Columns = band.Columns?.Where(columnLookup.ContainsKey).ToList() ?? new List<string>();
|
|
}
|
|
|
|
return layout;
|
|
}
|
|
|
|
private Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
|
|
{
|
|
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var band in layout.Bands)
|
|
{
|
|
foreach (var column in band.Columns)
|
|
{
|
|
assignments[column] = band.Id;
|
|
}
|
|
}
|
|
|
|
return assignments;
|
|
}
|
|
|
|
private RenderFragment RenderColumns() => builder =>
|
|
{
|
|
var seq = 0;
|
|
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
|
builder.AddAttribute(seq++, "Width", "120px");
|
|
builder.CloseComponent();
|
|
|
|
var grouped = bandLayout.Bands.SelectMany(band => band.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var column in columnDefinitions.Where(column => !grouped.Contains(column.FieldName)))
|
|
{
|
|
BuildDataColumn(builder, ref seq, column);
|
|
}
|
|
|
|
foreach (var band in bandLayout.Bands)
|
|
{
|
|
if (band.Columns.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
builder.OpenComponent<DxGridBandColumn>(seq++);
|
|
builder.AddAttribute(seq++, "Caption", band.Caption);
|
|
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
|
{
|
|
var bandSeq = 0;
|
|
foreach (var columnName in band.Columns)
|
|
{
|
|
if (columnLookup.TryGetValue(columnName, out var column))
|
|
{
|
|
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
|
}
|
|
}
|
|
}));
|
|
builder.CloseComponent();
|
|
}
|
|
};
|
|
|
|
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
|
|
{
|
|
builder.OpenComponent<DxGridDataColumn>(seq++);
|
|
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
|
builder.AddAttribute(seq++, "Caption", column.Caption);
|
|
if (!string.IsNullOrWhiteSpace(column.Width))
|
|
{
|
|
builder.AddAttribute(seq++, "Width", column.Width);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
|
{
|
|
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
|
}
|
|
|
|
if (column.ReadOnly)
|
|
{
|
|
builder.AddAttribute(seq++, "ReadOnly", true);
|
|
}
|
|
|
|
if (string.Equals(column.FieldName, nameof(CatalogReadDto.AddedWhen), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildAddedWhenFilterTemplate());
|
|
}
|
|
else if (string.Equals(column.FieldName, nameof(CatalogReadDto.ChangedWhen), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildChangedWhenFilterTemplate());
|
|
}
|
|
|
|
builder.CloseComponent();
|
|
}
|
|
|
|
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildAddedWhenFilterTemplate()
|
|
{
|
|
return BuildDateFilterTemplate(
|
|
() => addedWhenFilterValue,
|
|
value => addedWhenFilterValue = value,
|
|
() => addedWhenFilterOperator,
|
|
value => addedWhenFilterOperator = value);
|
|
}
|
|
|
|
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildChangedWhenFilterTemplate()
|
|
{
|
|
return BuildDateFilterTemplate(
|
|
() => changedWhenFilterValue,
|
|
value => changedWhenFilterValue = value,
|
|
() => changedWhenFilterOperator,
|
|
value => changedWhenFilterOperator = value);
|
|
}
|
|
|
|
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildDateFilterTemplate(
|
|
Func<DateTime?> getValue,
|
|
Action<DateTime?> setValue,
|
|
Func<string> getOperator,
|
|
Action<string> setOperator)
|
|
{
|
|
return context => builder =>
|
|
{
|
|
var seq = 0;
|
|
builder.OpenElement(seq++, "div");
|
|
builder.AddAttribute(seq++, "class", "filter-row-cell");
|
|
|
|
builder.OpenComponent<DxComboBox<FilterOperatorOption, string>>(seq++);
|
|
builder.AddAttribute(seq++, "Data", filterOperators);
|
|
builder.AddAttribute(seq++, "TextFieldName", "Text");
|
|
builder.AddAttribute(seq++, "ValueFieldName", "Value");
|
|
builder.AddAttribute(seq++, "Value", getOperator());
|
|
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<string>(this, value =>
|
|
{
|
|
setOperator(value);
|
|
UpdateFilterCriteria(context, value, getValue());
|
|
}));
|
|
builder.AddAttribute(seq++, "CssClass", "filter-operator");
|
|
builder.CloseComponent();
|
|
|
|
builder.OpenComponent<DxDateEdit<DateTime?>>(seq++);
|
|
builder.AddAttribute(seq++, "Date", getValue());
|
|
builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create<DateTime?>(this, value =>
|
|
{
|
|
setValue(value);
|
|
UpdateFilterCriteria(context, getOperator(), value);
|
|
}));
|
|
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
|
|
builder.AddAttribute(seq++, "CssClass", "filter-value");
|
|
builder.CloseComponent();
|
|
|
|
builder.CloseElement();
|
|
};
|
|
}
|
|
|
|
private void UpdateFilterCriteria(GridDataColumnFilterRowCellTemplateContext context, string op, DateTime? value)
|
|
{
|
|
if (!value.HasValue)
|
|
{
|
|
context.FilterCriteria = null;
|
|
return;
|
|
}
|
|
|
|
var prop = new OperandProperty(context.DataColumn.FieldName);
|
|
var date = value.Value.Date;
|
|
context.FilterCriteria = op switch
|
|
{
|
|
"=" => new GroupOperator(GroupOperatorType.And, prop >= date, prop < date.AddDays(1)),
|
|
"<" => prop < date,
|
|
"<=" => prop < date.AddDays(1),
|
|
">" => prop >= date.AddDays(1),
|
|
">=" => prop >= date,
|
|
_ => prop == date
|
|
};
|
|
}
|
|
|
|
private sealed class BandLayout
|
|
{
|
|
public List<BandDefinition> Bands { get; set; } = new();
|
|
public List<string> ColumnOrder { get; set; } = new();
|
|
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
|
public GridPersistentLayout? GridLayout { get; set; }
|
|
}
|
|
|
|
private sealed class BandDefinition
|
|
{
|
|
public string Id { get; set; } = string.Empty;
|
|
public string Caption { get; set; } = string.Empty;
|
|
public List<string> Columns { get; set; } = new();
|
|
}
|
|
|
|
private sealed class BandOption
|
|
{
|
|
public string Id { get; set; } = string.Empty;
|
|
public string Caption { get; set; } = string.Empty;
|
|
}
|
|
|
|
private sealed class ColumnDefinition
|
|
{
|
|
public string FieldName { get; init; } = string.Empty;
|
|
public string Caption { get; init; } = string.Empty;
|
|
public string? Width { get; set; }
|
|
public string? DisplayFormat { get; init; }
|
|
public bool ReadOnly { get; init; }
|
|
public ColumnFilterType FilterType { get; init; }
|
|
}
|
|
|
|
private enum ColumnFilterType
|
|
{
|
|
Text,
|
|
Date
|
|
}
|
|
|
|
private sealed class CatalogEditModel
|
|
{
|
|
public int Guid { get; set; }
|
|
public string CatTitle { get; set; } = string.Empty;
|
|
public string CatString { get; set; } = string.Empty;
|
|
public int UpdateProcedure { get; set; }
|
|
public string OriginalCatTitle { get; set; } = string.Empty;
|
|
public bool IsNew { get; set; }
|
|
}
|
|
|
|
private sealed class ProcedureOption
|
|
{
|
|
public int Value { get; set; }
|
|
public string Text { get; set; } = string.Empty;
|
|
}
|
|
|
|
private sealed class FilterOperatorOption
|
|
{
|
|
public string Value { get; set; } = string.Empty;
|
|
public string Text { get; set; } = string.Empty;
|
|
}
|
|
}
|