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) {