@using System.Text.Json @using Microsoft.AspNetCore.Components @using Microsoft.AspNetCore.Components.Rendering @using Microsoft.AspNetCore.Components.Forms @inject CatalogApiClient Api @inject LayoutApiClient LayoutApi @inject IJSRuntime JsRuntime @if (!string.IsNullOrWhiteSpace(errorMessage)) { } else if (!string.IsNullOrWhiteSpace(infoMessage)) { } @if (isLoading) {

Lade Daten...

} else if (items.Count == 0) {

Keine Einträge vorhanden.

} else {
@foreach (var band in bandLayout.Bands) {
} @foreach (var column in columnDefinitions) { }
@RenderColumns() @{ SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); } @if (!editModel.IsNew) { }
} @code { private List 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 const string LayoutType = "GRID_BANDS"; private const string LayoutKey = "CatalogsGrid"; private const string LayoutUserStorageKey = "layoutUser"; private string? layoutUser; private BandLayout bandLayout = new(); private Dictionary columnBandAssignments = new(); private List bandOptions = new(); private Dictionary columnLookup = new(); private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web); private List 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 procedureOptions = new() { new() { Value = 0, Text = "PRTBMY_CATALOG_UPDATE" }, new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" } }; private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser); protected override async Task OnInitializedAsync() { columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase); await EnsureLayoutUserAsync(); await LoadBandLayoutAsync(); await LoadCatalogs(); } 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("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(stored.LayoutData, jsonOptions); bandLayout = NormalizeBandLayout(parsed); } else { bandLayout = new BandLayout(); } columnBandAssignments = BuildAssignmentsFromLayout(bandLayout); ApplyColumnLayoutFromStorage(); ApplyBandOrderingFromColumnOrder(); UpdateBandOptions(); } private async Task SaveBandLayoutAsync() { await SaveGridLayoutAsync(); } private async Task SaveGridLayoutAsync() { 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 = "Grid-Layout gespeichert."; errorMessage = null; } catch (Exception ex) { errorMessage = $"Grid-Layout konnte nicht gespeichert werden: {ex.Message}"; } } 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() { if (bandLayout.ColumnOrder.Count > 0) { var ordered = bandLayout.ColumnOrder .Where(columnLookup.ContainsKey) .Select(field => columnLookup[field]) .ToList(); ordered.AddRange(columnDefinitions.Where(column => !ordered.Contains(column))); columnDefinitions = ordered; } 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 CaptureColumnLayoutFromGrid() { if (gridRef == null) { return; } var gridColumns = gridRef.GetColumns() .OfType() .Where(column => !string.IsNullOrWhiteSpace(column.FieldName)) .ToList(); bandLayout.ColumnOrder = gridColumns .OrderBy(column => column.VisibleIndex) .Select(column => column.FieldName) .ToList(); bandLayout.ColumnWidths = gridColumns .Where(column => !string.IsNullOrWhiteSpace(column.Width)) .ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase); ApplyBandOrderingFromColumnOrder(); } 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 { 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 { 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(); 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(); } return layout; } private Dictionary BuildAssignmentsFromLayout(BandLayout layout) { var assignments = new Dictionary(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(seq++); builder.AddAttribute(seq++, "Width", "120px"); builder.CloseComponent(); var bandLookup = bandLayout.Bands.ToDictionary(band => band.Id, StringComparer.OrdinalIgnoreCase); var renderedBands = new HashSet(StringComparer.OrdinalIgnoreCase); var orderedFields = bandLayout.ColumnOrder .Where(columnLookup.ContainsKey) .ToList(); if (orderedFields.Count == 0) { 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(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(); } return; } foreach (var fieldName in orderedFields) { if (columnBandAssignments.TryGetValue(fieldName, out var bandId) && bandLookup.TryGetValue(bandId, out var band)) { if (!renderedBands.Add(bandId) || 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(); } else if (columnLookup.TryGetValue(fieldName, out var column)) { BuildDataColumn(builder, ref seq, column); } } foreach (var column in columnDefinitions) { if (!orderedFields.Contains(column.FieldName, StringComparer.OrdinalIgnoreCase) && (!columnBandAssignments.TryGetValue(column.FieldName, out var bandId) || !bandLookup.ContainsKey(bandId))) { BuildDataColumn(builder, ref seq, column); } } foreach (var band in bandLayout.Bands) { if (renderedBands.Contains(band.Id) || 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(); } }; private 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); } builder.CloseComponent(); } private RenderFragment? BuildFilterTemplate(ColumnDefinition column) { return null; } private RenderFragment? BuildTextFilterTemplate() { return null; } private RenderFragment? BuildDateFilterTemplate() { return null; } private sealed class BandLayout { public List Bands { get; set; } = new(); public List ColumnOrder { get; set; } = new(); public Dictionary ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase); } private sealed class BandDefinition { public string Id { get; set; } = string.Empty; public string Caption { get; set; } = string.Empty; public List 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; } }