From 8387b71676c1c067ac1981f3db7cba6470580322 Mon Sep 17 00:00:00 2001 From: OlgunR Date: Mon, 9 Feb 2026 16:10:16 +0100 Subject: [PATCH] Add operator selection to grid filter rows for dates/numbers 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. --- .../Components/CatalogsGrid.razor | 164 ++++++++---- .../Components/MassDataGrid.razor | 197 +++++++++++++- .../Components/CatalogsGrid.razor | 194 +++++++++++--- .../Components/MassDataGrid.razor | 243 ++++++++++++++---- 4 files changed, 665 insertions(+), 133 deletions(-) diff --git a/DbFirst.BlazorWasm/Components/CatalogsGrid.razor b/DbFirst.BlazorWasm/Components/CatalogsGrid.razor index 57f741e..31fb68c 100644 --- a/DbFirst.BlazorWasm/Components/CatalogsGrid.razor +++ b/DbFirst.BlazorWasm/Components/CatalogsGrid.razor @@ -3,6 +3,7 @@ @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 @@ -73,6 +74,21 @@ .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; + } @if (!string.IsNullOrWhiteSpace(errorMessage)) @@ -186,6 +202,10 @@ else 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"; @@ -212,8 +232,16 @@ else new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" } }; - private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser); + private readonly List 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() @@ -554,49 +582,6 @@ else columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase); } - private void ApplyBandOrderingFromColumnOrder() - { - if (bandLayout.ColumnOrder.Count == 0) - { - return; - } - - var bandById = bandLayout.Bands.ToDictionary(band => band.Id, StringComparer.OrdinalIgnoreCase); - var orderedBandIds = new List(); - var orderedColumnsByBand = bandLayout.Bands.ToDictionary( - band => band.Id, - _ => new List(), - StringComparer.OrdinalIgnoreCase); - - foreach (var field in bandLayout.ColumnOrder) - { - if (columnBandAssignments.TryGetValue(field, out var bandId) && bandById.ContainsKey(bandId)) - { - if (!orderedBandIds.Contains(bandId, StringComparer.OrdinalIgnoreCase)) - { - orderedBandIds.Add(bandId); - } - - orderedColumnsByBand[bandId].Add(field); - } - } - - foreach (var band in bandLayout.Bands) - { - var orderedColumns = orderedColumnsByBand[band.Id]; - orderedColumns.AddRange(band.Columns.Where(column => !orderedColumns.Contains(column, StringComparer.OrdinalIgnoreCase))); - band.Columns = orderedColumns; - } - - if (orderedBandIds.Count > 0) - { - bandLayout.Bands = orderedBandIds - .Select(id => bandById[id]) - .Concat(bandLayout.Bands.Where(band => !orderedBandIds.Contains(band.Id, StringComparer.OrdinalIgnoreCase))) - .ToList(); - } - } - private void AddBand() { bandLayout.Bands.Add(new BandDefinition @@ -760,22 +745,95 @@ else 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? BuildFilterTemplate(ColumnDefinition column) + private RenderFragment BuildAddedWhenFilterTemplate() { - return null; + return BuildDateFilterTemplate( + () => addedWhenFilterValue, + value => addedWhenFilterValue = value, + () => addedWhenFilterOperator, + value => addedWhenFilterOperator = value); } - private RenderFragment? BuildTextFilterTemplate() + private RenderFragment BuildChangedWhenFilterTemplate() { - return null; + return BuildDateFilterTemplate( + () => changedWhenFilterValue, + value => changedWhenFilterValue = value, + () => changedWhenFilterOperator, + value => changedWhenFilterOperator = value); } - private RenderFragment? BuildDateFilterTemplate() + private RenderFragment BuildDateFilterTemplate( + Func getValue, + Action setValue, + Func getOperator, + Action setOperator) { - return null; + return context => builder => + { + var seq = 0; + builder.OpenElement(seq++, "div"); + builder.AddAttribute(seq++, "class", "filter-row-cell"); + + builder.OpenComponent>(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(this, value => + { + setOperator(value); + UpdateFilterCriteria(context, value, getValue()); + })); + builder.AddAttribute(seq++, "CssClass", "filter-operator"); + builder.CloseComponent(); + + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Date", getValue()); + builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create(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 @@ -830,4 +888,10 @@ else 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; + } } diff --git a/DbFirst.BlazorWasm/Components/MassDataGrid.razor b/DbFirst.BlazorWasm/Components/MassDataGrid.razor index f29fee4..a8c4f2c 100644 --- a/DbFirst.BlazorWasm/Components/MassDataGrid.razor +++ b/DbFirst.BlazorWasm/Components/MassDataGrid.razor @@ -3,6 +3,7 @@ @using Microsoft.AspNetCore.Components.Rendering @using Microsoft.AspNetCore.Components.Forms @using DevExpress.Blazor +@using DevExpress.Data.Filtering @inject MassDataApiClient Api @inject LayoutApiClient LayoutApi @inject IJSRuntime JsRuntime @@ -96,6 +97,25 @@ .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; + } + .filter-value-amount { + min-width: 110px; + flex-basis: 110px; + } @if (!string.IsNullOrWhiteSpace(errorMessage)) @@ -234,6 +254,12 @@ else private int pageCount = 1; private int? pageSize = 100; private string popupHeaderText = "Edit"; + private decimal? amountFilterValue; + private string amountFilterOperator = "="; + private DateTime? addedWhenFilterValue; + private string addedWhenFilterOperator = "="; + private DateTime? changedWhenFilterValue; + private string changedWhenFilterOperator = "="; private EditContext? editContext; private ValidationMessageStore? validationMessageStore; private IGrid? gridRef; @@ -280,6 +306,17 @@ else new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" } }; + private readonly List filterOperators = new() + { + new() { Value = "=", Text = "=" }, + new() { Value = "<", Text = "<" }, + new() { Value = ">", Text = ">" }, + new() { Value = "<=", Text = "<=" }, + new() { Value = ">=", Text = ">=" } + }; + + private bool gridLayoutApplied; + protected override async Task OnInitializedAsync() { columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase); @@ -601,9 +638,163 @@ else builder.AddAttribute(seq++, "ReadOnly", true); } + if (string.Equals(column.FieldName, nameof(MassDataReadDto.Amount), StringComparison.OrdinalIgnoreCase)) + { + builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildAmountFilterTemplate()); + } + else if (string.Equals(column.FieldName, nameof(MassDataReadDto.AddedWhen), StringComparison.OrdinalIgnoreCase)) + { + builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildAddedWhenFilterTemplate()); + } + else if (string.Equals(column.FieldName, nameof(MassDataReadDto.ChangedWhen), StringComparison.OrdinalIgnoreCase)) + { + builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildChangedWhenFilterTemplate()); + } + builder.CloseComponent(); } + private RenderFragment BuildAmountFilterTemplate() + { + return BuildNumberFilterTemplate( + () => amountFilterValue, + value => amountFilterValue = value, + () => amountFilterOperator, + value => amountFilterOperator = value); + } + + private RenderFragment BuildAddedWhenFilterTemplate() + { + return BuildDateFilterTemplate( + () => addedWhenFilterValue, + value => addedWhenFilterValue = value, + () => addedWhenFilterOperator, + value => addedWhenFilterOperator = value); + } + + private RenderFragment BuildChangedWhenFilterTemplate() + { + return BuildDateFilterTemplate( + () => changedWhenFilterValue, + value => changedWhenFilterValue = value, + () => changedWhenFilterOperator, + value => changedWhenFilterOperator = value); + } + + private RenderFragment BuildNumberFilterTemplate( + Func getValue, + Action setValue, + Func getOperator, + Action setOperator) + { + return context => builder => + { + var seq = 0; + builder.OpenElement(seq++, "div"); + builder.AddAttribute(seq++, "class", "filter-row-cell"); + + builder.OpenComponent>(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(this, value => + { + setOperator(value); + UpdateFilterCriteria(context, value, getValue()); + })); + builder.AddAttribute(seq++, "CssClass", "filter-operator"); + builder.CloseComponent(); + + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Value", getValue()); + builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create(this, value => + { + setValue(value); + UpdateFilterCriteria(context, getOperator(), value); + })); + builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto); + builder.AddAttribute(seq++, "CssClass", "filter-value filter-value-amount"); + builder.CloseComponent(); + + builder.CloseElement(); + }; + } + + private RenderFragment BuildDateFilterTemplate( + Func getValue, + Action setValue, + Func getOperator, + Action setOperator) + { + return context => builder => + { + var seq = 0; + builder.OpenElement(seq++, "div"); + builder.AddAttribute(seq++, "class", "filter-row-cell"); + + builder.OpenComponent>(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(this, value => + { + setOperator(value); + UpdateFilterCriteria(context, value, getValue()); + })); + builder.AddAttribute(seq++, "CssClass", "filter-operator"); + builder.CloseComponent(); + + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Date", getValue()); + builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create(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, object? value) + { + if (value == null) + { + context.FilterCriteria = null; + return; + } + + var prop = new OperandProperty(context.DataColumn.FieldName); + if (value is DateTime dateValue) + { + var date = dateValue.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 + }; + return; + } + + context.FilterCriteria = op switch + { + "<" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.Less), + ">" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.Greater), + "<=" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.LessOrEqual), + ">=" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.GreaterOrEqual), + _ => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.Equal) + }; + } + private void SetEditContext(EditContext context) { if (editContext == context) @@ -808,7 +999,11 @@ else public string Text { get; set; } = string.Empty; } - private bool gridLayoutApplied; + private sealed class FilterOperatorOption + { + public string Value { get; set; } = string.Empty; + public string Text { get; set; } = string.Empty; + } protected override async Task OnAfterRenderAsync(bool firstRender) { diff --git a/DbFirst.BlazorWebApp/Components/CatalogsGrid.razor b/DbFirst.BlazorWebApp/Components/CatalogsGrid.razor index 48c62e5..55c5290 100644 --- a/DbFirst.BlazorWebApp/Components/CatalogsGrid.razor +++ b/DbFirst.BlazorWebApp/Components/CatalogsGrid.razor @@ -3,6 +3,7 @@ @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 @@ -32,6 +33,21 @@ .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; + } @if (!string.IsNullOrWhiteSpace(errorMessage)) @@ -145,6 +161,10 @@ else 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"; @@ -171,7 +191,17 @@ else new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" } }; + private readonly List 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() { @@ -181,6 +211,16 @@ else 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) @@ -474,6 +514,33 @@ else UpdateBandOptions(); } + 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 @@ -637,9 +704,97 @@ else 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 BuildAddedWhenFilterTemplate() + { + return BuildDateFilterTemplate( + () => addedWhenFilterValue, + value => addedWhenFilterValue = value, + () => addedWhenFilterOperator, + value => addedWhenFilterOperator = value); + } + + private RenderFragment BuildChangedWhenFilterTemplate() + { + return BuildDateFilterTemplate( + () => changedWhenFilterValue, + value => changedWhenFilterValue = value, + () => changedWhenFilterOperator, + value => changedWhenFilterOperator = value); + } + + private RenderFragment BuildDateFilterTemplate( + Func getValue, + Action setValue, + Func getOperator, + Action setOperator) + { + return context => builder => + { + var seq = 0; + builder.OpenElement(seq++, "div"); + builder.AddAttribute(seq++, "class", "filter-row-cell"); + + builder.OpenComponent>(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(this, value => + { + setOperator(value); + UpdateFilterCriteria(context, value, getValue()); + })); + builder.AddAttribute(seq++, "CssClass", "filter-operator"); + builder.CloseComponent(); + + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Date", getValue()); + builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create(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 Bands { get; set; } = new(); @@ -693,42 +848,9 @@ else public string Text { get; set; } = string.Empty; } - private async Task ResetBandLayoutAsync() + private sealed class FilterOperatorOption { - 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 bool gridLayoutApplied; - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null) - { - gridRef.LoadLayout(bandLayout.GridLayout); - gridLayoutApplied = true; - await InvokeAsync(StateHasChanged); - } + public string Value { get; set; } = string.Empty; + public string Text { get; set; } = string.Empty; } } diff --git a/DbFirst.BlazorWebApp/Components/MassDataGrid.razor b/DbFirst.BlazorWebApp/Components/MassDataGrid.razor index 72ee7da..c6a739c 100644 --- a/DbFirst.BlazorWebApp/Components/MassDataGrid.razor +++ b/DbFirst.BlazorWebApp/Components/MassDataGrid.razor @@ -3,6 +3,7 @@ @using Microsoft.AspNetCore.Components.Rendering @using Microsoft.AspNetCore.Components.Forms @using DevExpress.Blazor +@using DevExpress.Data.Filtering @inject MassDataApiClient Api @inject LayoutApiClient LayoutApi @inject IJSRuntime JsRuntime @@ -96,6 +97,25 @@ .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; + } + .filter-value-amount { + min-width: 110px; + flex-basis: 110px; + } @if (!string.IsNullOrWhiteSpace(errorMessage)) @@ -234,6 +254,12 @@ else private int pageCount = 1; private int? pageSize = 100; private string popupHeaderText = "Edit"; + private decimal? amountFilterValue; + private string amountFilterOperator = "="; + private DateTime? addedWhenFilterValue; + private string addedWhenFilterOperator = "="; + private DateTime? changedWhenFilterValue; + private string changedWhenFilterOperator = "="; private EditContext? editContext; private ValidationMessageStore? validationMessageStore; private IGrid? gridRef; @@ -280,6 +306,17 @@ else new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" } }; + private readonly List filterOperators = new() + { + new() { Value = "=", Text = "=" }, + new() { Value = "<", Text = "<" }, + new() { Value = ">", Text = ">" }, + new() { Value = "<=", Text = "<=" }, + new() { Value = ">=", Text = ">=" } + }; + + private bool gridLayoutApplied; + protected override async Task OnInitializedAsync() { columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase); @@ -356,7 +393,6 @@ else columnBandAssignments = BuildAssignmentsFromLayout(bandLayout); ApplyColumnLayoutFromStorage(); - //ApplyBandOrderingFromColumnOrder(); UpdateBandOptions(); } @@ -426,49 +462,6 @@ else infoMessage = "Band-Layout zurückgesetzt."; } - private void ApplyBandOrderingFromColumnOrder() - { - if (bandLayout.ColumnOrder.Count == 0) - { - return; - } - - var bandById = bandLayout.Bands.ToDictionary(band => band.Id, StringComparer.OrdinalIgnoreCase); - var orderedBandIds = new List(); - var orderedColumnsByBand = bandLayout.Bands.ToDictionary( - band => band.Id, - _ => new List(), - StringComparer.OrdinalIgnoreCase); - - foreach (var field in bandLayout.ColumnOrder) - { - if (columnBandAssignments.TryGetValue(field, out var bandId) && bandById.ContainsKey(bandId)) - { - if (!orderedBandIds.Contains(bandId, StringComparer.OrdinalIgnoreCase)) - { - orderedBandIds.Add(bandId); - } - - orderedColumnsByBand[bandId].Add(field); - } - } - - foreach (var band in bandLayout.Bands) - { - var orderedColumns = orderedColumnsByBand[band.Id]; - orderedColumns.AddRange(band.Columns.Where(column => !orderedColumns.Contains(column, StringComparer.OrdinalIgnoreCase))); - band.Columns = orderedColumns; - } - - if (orderedBandIds.Count > 0) - { - bandLayout.Bands = orderedBandIds - .Select(id => bandById[id]) - .Concat(bandLayout.Bands.Where(band => !orderedBandIds.Contains(band.Id, StringComparer.OrdinalIgnoreCase))) - .ToList(); - } - } - private void ApplyColumnLayoutFromStorage() { foreach (var column in columnDefinitions) @@ -645,9 +638,163 @@ else builder.AddAttribute(seq++, "ReadOnly", true); } + if (string.Equals(column.FieldName, nameof(MassDataReadDto.Amount), StringComparison.OrdinalIgnoreCase)) + { + builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildAmountFilterTemplate()); + } + else if (string.Equals(column.FieldName, nameof(MassDataReadDto.AddedWhen), StringComparison.OrdinalIgnoreCase)) + { + builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildAddedWhenFilterTemplate()); + } + else if (string.Equals(column.FieldName, nameof(MassDataReadDto.ChangedWhen), StringComparison.OrdinalIgnoreCase)) + { + builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildChangedWhenFilterTemplate()); + } + builder.CloseComponent(); } + private RenderFragment BuildAmountFilterTemplate() + { + return BuildNumberFilterTemplate( + () => amountFilterValue, + value => amountFilterValue = value, + () => amountFilterOperator, + value => amountFilterOperator = value); + } + + private RenderFragment BuildAddedWhenFilterTemplate() + { + return BuildDateFilterTemplate( + () => addedWhenFilterValue, + value => addedWhenFilterValue = value, + () => addedWhenFilterOperator, + value => addedWhenFilterOperator = value); + } + + private RenderFragment BuildChangedWhenFilterTemplate() + { + return BuildDateFilterTemplate( + () => changedWhenFilterValue, + value => changedWhenFilterValue = value, + () => changedWhenFilterOperator, + value => changedWhenFilterOperator = value); + } + + private RenderFragment BuildNumberFilterTemplate( + Func getValue, + Action setValue, + Func getOperator, + Action setOperator) + { + return context => builder => + { + var seq = 0; + builder.OpenElement(seq++, "div"); + builder.AddAttribute(seq++, "class", "filter-row-cell"); + + builder.OpenComponent>(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(this, value => + { + setOperator(value); + UpdateFilterCriteria(context, value, getValue()); + })); + builder.AddAttribute(seq++, "CssClass", "filter-operator"); + builder.CloseComponent(); + + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Value", getValue()); + builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create(this, value => + { + setValue(value); + UpdateFilterCriteria(context, getOperator(), value); + })); + builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto); + builder.AddAttribute(seq++, "CssClass", "filter-value filter-value-amount"); + builder.CloseComponent(); + + builder.CloseElement(); + }; + } + + private RenderFragment BuildDateFilterTemplate( + Func getValue, + Action setValue, + Func getOperator, + Action setOperator) + { + return context => builder => + { + var seq = 0; + builder.OpenElement(seq++, "div"); + builder.AddAttribute(seq++, "class", "filter-row-cell"); + + builder.OpenComponent>(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(this, value => + { + setOperator(value); + UpdateFilterCriteria(context, value, getValue()); + })); + builder.AddAttribute(seq++, "CssClass", "filter-operator"); + builder.CloseComponent(); + + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Date", getValue()); + builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create(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, object? value) + { + if (value == null) + { + context.FilterCriteria = null; + return; + } + + var prop = new OperandProperty(context.DataColumn.FieldName); + if (value is DateTime dateValue) + { + var date = dateValue.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 + }; + return; + } + + context.FilterCriteria = op switch + { + "<" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.Less), + ">" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.Greater), + "<=" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.LessOrEqual), + ">=" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.GreaterOrEqual), + _ => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.Equal) + }; + } + private void SetEditContext(EditContext context) { if (editContext == context) @@ -783,7 +930,7 @@ else e.Cancel = true; return Task.CompletedTask; } - + private sealed class BandLayout { public List Bands { get; set; } = new(); @@ -852,7 +999,11 @@ else public string Text { get; set; } = string.Empty; } - private bool gridLayoutApplied; + private sealed class FilterOperatorOption + { + public string Value { get; set; } = string.Empty; + public string Text { get; set; } = string.Empty; + } protected override async Task OnAfterRenderAsync(bool firstRender) {