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; namespace DbFirst.BlazorWebApp.Components; public abstract class BandGridBase : ComponentBase { [Inject] protected BandLayoutService BandLayoutService { get; set; } = default!; // --- Abstract: jedes Grid definiert diese selbst --- protected abstract string LayoutKey { get; } protected abstract List ColumnDefinitions { get; } // --- Band-Layout Felder --- protected BandLayout bandLayout = new(); protected Dictionary columnBandAssignments = new(); protected List bandOptions = new(); protected Dictionary columnLookup = new(); protected string? layoutUser; 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); private readonly Dictionary _filterContexts = new(StringComparer.OrdinalIgnoreCase); // --- SizeMode --- protected SizeMode _sizeMode = SizeMode.Medium; protected static readonly List _sizeModes = Enum.GetValues().ToList(); protected string? errorMessage; protected string? infoMessage; protected bool isLoading; protected bool hasLoaded; protected EditContext? editContext; protected ValidationMessageStore? validationMessageStore; protected string popupHeaderText = "Edit"; protected int _focusedVisibleIndex; private const string LayoutType = "GRID_BANDS"; // --- Lifecycle --- protected async Task InitializeBandLayoutAsync() { columnLookup = ColumnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase); layoutUser = await BandLayoutService.EnsureLayoutUserAsync(); bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup); columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout); ApplyColumnLayoutFromStorage(); _sizeMode = bandLayout.SizeMode; UpdateBandOptions(); } protected async Task ApplyGridLayoutAfterRenderAsync() { if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null) { gridRef.LoadLayout(bandLayout.GridLayout); gridLayoutApplied = true; await InvokeAsync(StateHasChanged); } } // --- Layout speichern / zurücksetzen --- protected async Task SaveLayoutAsync() { if (string.IsNullOrWhiteSpace(layoutUser)) return; CaptureColumnLayoutFromGrid(); await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout); } protected async Task ResetLayoutAsync() { if (string.IsNullOrWhiteSpace(layoutUser)) return; await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser); bandLayout = new BandLayout(); columnBandAssignments.Clear(); UpdateBandOptions(); foreach (var column in ColumnDefinitions) column.Width = null; columnLookup = ColumnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase); _sizeMode = SizeMode.Medium; gridRef?.LoadLayout(new GridPersistentLayout()); gridLayoutApplied = false; } private void CaptureColumnLayoutFromGrid() { if (gridRef == null) return; var layout = gridRef.SaveLayout(); bandLayout.GridLayout = layout; bandLayout.SizeMode = _sizeMode; var orderedColumns = layout.Columns .Where(c => !string.IsNullOrWhiteSpace(c.FieldName)) .OrderBy(c => c.VisibleIndex) .ToList(); bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList(); bandLayout.ColumnWidths = orderedColumns .Where(c => !string.IsNullOrWhiteSpace(c.Width)) .ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase); } 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(c => c.FieldName, StringComparer.OrdinalIgnoreCase); } // --- Band-Methoden --- protected bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser); protected virtual bool ShowCommandColumn => true; protected void AddBand() { bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" }); UpdateBandOptions(); } protected async Task RemoveBand(BandDefinition band) { bandLayout.Bands.Remove(band); foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList()) columnBandAssignments.Remove(key); UpdateBandOptions(); await SyncBandsFromAssignments(); } protected void UpdateBandCaption(BandDefinition band, string value) { band.Caption = value; UpdateBandOptions(); } protected async Task UpdateColumnBand(string fieldName, string? bandId) { if (string.IsNullOrWhiteSpace(bandId)) columnBandAssignments.Remove(fieldName); else columnBandAssignments[fieldName] = bandId; await SyncBandsFromAssignments(); } protected string GetColumnBand(string fieldName) => columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty; protected async Task SyncBandsFromAssignments() { foreach (var band in bandLayout.Bands) { band.Columns = ColumnDefinitions .Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id) .Select(c => c.FieldName) .ToList(); } await InvokeAsync(StateHasChanged); } protected void UpdateBandOptions() { bandOptions = [new() { Id = string.Empty, Caption = "Ohne Band" }]; bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption })); } // --- SizeMode --- protected string FormatSizeText(SizeMode size) => size switch { SizeMode.Small => "Klein", SizeMode.Medium => "Mittel", SizeMode.Large => "Groß", _ => size.ToString() }; protected void OnSizeChange(DropDownButtonItemClickEventArgs args) { _sizeMode = Enum.Parse(args.ItemInfo.Id); } // --- RenderColumns / BuildDataColumn --- protected RenderFragment RenderColumns() => builder => { var seq = 0; if (ShowCommandColumn) { builder.OpenComponent(seq++); builder.AddAttribute(seq++, "Width", "120px"); builder.CloseComponent(); } var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (var column in ColumnDefinitions.Where(c => !grouped.Contains(c.FieldName))) BuildDataColumn(builder, ref seq, column); foreach (var band in bandLayout.Bands) { if (band.Columns.Count == 0) continue; builder.OpenComponent(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(); } }; protected void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column) { builder.OpenComponent(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 (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, async (DateTime? v) => await OnFilterFromChanged(fieldName, v)); _toCallbacks[fieldName] = EventCallback.Factory.Create(this, async (DateTime? v) => await OnFilterToChanged(fieldName, v)); template = BuildDateFilterTemplate(fieldName); _dateFilterTemplates[fieldName] = template; } return template; } private RenderFragment BuildDateFilterTemplate(string fieldName) => ctx => b => { _filterContexts[fieldName] = ctx; SyncDateFilterFromContext(fieldName, ctx.FilterCriteria); 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 SyncDateFilterFromContext(string fieldName, CriteriaOperator? criteria) { if (criteria == null) { _filterFrom[fieldName] = null; _filterTo[fieldName] = null; return; } DateTime? from = null; DateTime? to = null; if (criteria is GroupOperator group) { foreach (var op in group.Operands.OfType()) ParseDateOperand(op, ref from, ref to); } else if (criteria is BinaryOperator binary) { ParseDateOperand(binary, ref from, ref to); } _filterFrom[fieldName] = from; _filterTo[fieldName] = to; } private static void ParseDateOperand(BinaryOperator op, ref DateTime? from, ref DateTime? to) { if (op.RightOperand is not OperandValue val || val.Value is not DateTime dt) return; if (op.OperatorType == BinaryOperatorType.GreaterOrEqual) from = dt; else if (op.OperatorType == BinaryOperatorType.Less) to = dt.AddDays(-1); } private async Task OnFilterFromChanged(string fieldName, DateTime? value) { _filterFrom[fieldName] = value; await ApplyDateFilter(fieldName); } private async Task OnFilterToChanged(string fieldName, DateTime? value) { _filterTo[fieldName] = value; await ApplyDateFilter(fieldName); } private async Task 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); if (_filterContexts.TryGetValue(fieldName, out var ctx)) ctx.FilterCriteria = criteria; await InvokeAsync(StateHasChanged); } protected void SetEditContext(EditContext context) { if (editContext == context) return; if (editContext != null) editContext.OnFieldChanged -= OnEditFieldChanged; editContext = context; validationMessageStore = new ValidationMessageStore(editContext); editContext.OnFieldChanged += OnEditFieldChanged; } protected virtual void OnEditFieldChanged(object? sender, FieldChangedEventArgs e) { validationMessageStore?.Clear(); editContext?.NotifyValidationStateChanged(); } protected void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit"; protected async Task EditFocusedRow() => await gridRef!.StartEditRowAsync(_focusedVisibleIndex); protected Task DeleteFocusedRow() { gridRef!.ShowRowDeleteConfirmation(_focusedVisibleIndex); return Task.CompletedTask; } protected async Task SaveLayoutWithFeedbackAsync() { try { await SaveLayoutAsync(); infoMessage = "Layout gespeichert."; errorMessage = null; } catch (Exception ex) { errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}"; } } protected async Task ResetLayoutWithFeedbackAsync() { await ResetLayoutAsync(); infoMessage = "Layout zurückgesetzt."; errorMessage = null; } }