diff --git a/DbFirst.BlazorWebApp/Components/BandGridBase.cs b/DbFirst.BlazorWebApp/Components/BandGridBase.cs index 7f41118..5e201e6 100644 --- a/DbFirst.BlazorWebApp/Components/BandGridBase.cs +++ b/DbFirst.BlazorWebApp/Components/BandGridBase.cs @@ -1,6 +1,7 @@ using DbFirst.BlazorWebApp.Models.Grid; using DbFirst.BlazorWebApp.Services; using DevExpress.Blazor; +using DevExpress.Data.Filtering; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Rendering; @@ -24,6 +25,15 @@ public abstract class BandGridBase : ComponentBase protected bool gridLayoutApplied; protected IGrid? gridRef; + // --- Datumsfilter-Zustand --- + private readonly Dictionary _filterFrom = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _filterTo = new(StringComparer.OrdinalIgnoreCase); + + // Stabile Referenzen: werden einmal pro FieldName erstellt und wiederverwendet + private readonly Dictionary> _fromCallbacks = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> _toCallbacks = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> _dateFilterTemplates = new(StringComparer.OrdinalIgnoreCase); + // --- SizeMode --- protected SizeMode _sizeMode = SizeMode.Medium; protected static readonly List _sizeModes = Enum.GetValues().ToList(); @@ -224,9 +234,97 @@ public abstract class BandGridBase : ComponentBase builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat); if (column.ReadOnly) builder.AddAttribute(seq++, "ReadOnly", true); + if (column.FilterType == ColumnFilterType.Date) + builder.AddAttribute(seq++, "FilterMenuTemplate", GetOrCreateDateFilterTemplate(column.FieldName)); builder.CloseComponent(); } + private RenderFragment GetOrCreateDateFilterTemplate(string fieldName) + { + if (!_dateFilterTemplates.TryGetValue(fieldName, out var template)) + { + // EventCallbacks einmalig erstellen – stabile Referenzen über alle Renders + _fromCallbacks[fieldName] = EventCallback.Factory.Create(this, (DateTime? v) => OnFilterFromChanged(fieldName, v)); + _toCallbacks[fieldName] = EventCallback.Factory.Create(this, (DateTime? v) => OnFilterToChanged(fieldName, v)); + template = BuildDateFilterTemplate(fieldName); + _dateFilterTemplates[fieldName] = template; + } + return template; + } + + private RenderFragment BuildDateFilterTemplate(string fieldName) => + _ => b => + { + int s = 0; + b.OpenElement(s++, "div"); + b.AddAttribute(s++, "class", "date-filter-menu p-2"); + + // Ab Datum + b.OpenElement(s++, "div"); + b.AddAttribute(s++, "class", "mb-2"); + b.OpenElement(s++, "label"); + b.AddAttribute(s++, "class", "form-label small fw-semibold"); + b.AddContent(s++, "Ab Datum"); + b.CloseElement(); + b.OpenComponent>(s++); + b.AddAttribute(s++, "Date", _filterFrom.GetValueOrDefault(fieldName)); + b.AddAttribute(s++, "DateChanged", _fromCallbacks[fieldName]); + b.AddAttribute(s++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto); + b.AddAttribute(s++, "NullText", "Kein Startdatum"); + b.AddAttribute(s++, "Width", "100%"); + b.CloseComponent(); + b.CloseElement(); + + // Bis Datum + b.OpenElement(s++, "div"); + b.AddAttribute(s++, "class", "mb-0"); + b.OpenElement(s++, "label"); + b.AddAttribute(s++, "class", "form-label small fw-semibold"); + b.AddContent(s++, "Bis Datum"); + b.CloseElement(); + b.OpenComponent>(s++); + b.AddAttribute(s++, "Date", _filterTo.GetValueOrDefault(fieldName)); + b.AddAttribute(s++, "DateChanged", _toCallbacks[fieldName]); + b.AddAttribute(s++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto); + b.AddAttribute(s++, "NullText", "Kein Enddatum"); + b.AddAttribute(s++, "Width", "100%"); + b.CloseComponent(); + b.CloseElement(); + + b.CloseElement(); + }; + + private void OnFilterFromChanged(string fieldName, DateTime? value) + { + _filterFrom[fieldName] = value; + ApplyDateFilter(fieldName); + } + + private void OnFilterToChanged(string fieldName, DateTime? value) + { + _filterTo[fieldName] = value; + ApplyDateFilter(fieldName); + } + + private void ApplyDateFilter(string fieldName) + { + var ops = new List(); + if (_filterFrom.TryGetValue(fieldName, out var from) && from.HasValue) + ops.Add(new BinaryOperator(fieldName, from.Value.Date, BinaryOperatorType.GreaterOrEqual)); + if (_filterTo.TryGetValue(fieldName, out var to) && to.HasValue) + ops.Add(new BinaryOperator(fieldName, to.Value.Date.AddDays(1), BinaryOperatorType.Less)); + + CriteriaOperator? criteria = ops.Count switch + { + 0 => null, + 1 => ops[0], + _ => new GroupOperator(GroupOperatorType.And, ops) + }; + + gridRef?.SetFieldFilterCriteria(fieldName, criteria); + _ = InvokeAsync(StateHasChanged); + } + protected void SetEditContext(EditContext context) { if (editContext == context) return; diff --git a/DbFirst.BlazorWebApp/wwwroot/app.css b/DbFirst.BlazorWebApp/wwwroot/app.css index 72224cc..6b6b0cc 100644 --- a/DbFirst.BlazorWebApp/wwwroot/app.css +++ b/DbFirst.BlazorWebApp/wwwroot/app.css @@ -294,4 +294,10 @@ html.dx-dark .dxbl-grid > .dxbl-grid-top-panel { .top-row .btn-gap { margin-left: 8px; +} + +/* Date-Filter: nativer Apply/Clear-Footer ausblenden, da Filter sofort bei Datumsauswahl angewendet wird */ +/* .dxbl-dropdown-footer ist der korrekte Selektor für das Filter-Dropdown (nicht .dxbl-popup-buttons-area, das ist für modale Popups) */ +.dxbl-filter-menu-dropdown-root:has(.date-filter-menu) .dxbl-dropdown-footer { + display: none !important; } \ No newline at end of file