Compare commits

...

8 Commits

Author SHA1 Message Date
OlgunR
4ac8e94334 Refactor: centralize grid/editor CSS in app.css
Consolidate shared grid and band-editor styles from CatalogsGrid.razor.css and MassDataGrid.razor.css into app.css for improved maintainability and consistency. Only component-specific min-width rules remain in the respective files. Also includes minor formatting cleanups in app.css.
2026-03-25 17:15:55 +01:00
OlgunR
dc74d21426 Refactor: centralize grid band layout logic in service
Introduce BandLayoutService and shared models to manage grid band layouts across components. Refactor CatalogsGrid and MassDataGrid to use the new service, removing duplicated layout logic. Update _Imports.razor and register the service in Program.cs for improved maintainability and code reuse.
2026-03-25 17:04:19 +01:00
OlgunR
566c3b3276 Move dashboard styles to Dashboard.razor.css file
Separated CSS from Dashboard.razor by moving all dashboard-related styles into a new Dashboard.razor.css file. This improves maintainability and keeps styling concerns separate from markup and logic.
2026-03-25 13:34:52 +01:00
OlgunR
ac84866abe Extract inline styles to .razor.css files for grids
Moved CSS from inline <style> blocks in CatalogsGrid.razor and MassDataGrid.razor to dedicated CatalogsGrid.razor.css and MassDataGrid.razor.css files. This improves maintainability and keeps styling concerns separate from component logic. No changes to the actual styles were made.
2026-03-25 12:02:30 +01:00
OlgunR
d9ce4a5dca Move common usings to _Imports.razor for global access
Removed redundant using directives from CatalogsGrid.razor and MassDataGrid.razor. Added them to _Imports.razor to ensure global availability across components. Also added @using DbFirst.BlazorWebApp to _Imports.razor for consistency.
2026-03-25 10:49:51 +01:00
OlgunR
13617dde87 Remove duplicate DevExpress.Blazor package reference
Cleaned up the project file by removing a redundant <ItemGroup>
that contained a duplicate DevExpress.Blazor package reference.
Ensured that each package is referenced only once and that
Microsoft.AspNetCore.SignalR.Client remains in its own group.
2026-03-25 10:45:57 +01:00
OlgunR
789066a214 Replace HTML entities with Latin-1 bytes in UI strings
Replaced HTML-encoded German special characters with their Windows-1252 (Latin-1) encoded byte equivalents in user-facing strings in CatalogsGrid.razor and MassDataGrid.razor. Renamed and expanded the reset layout method for improved layout reset behavior. Updated button labels and info messages for consistency. These changes address encoding and rendering issues by shifting from HTML entities to direct byte encoding.
2026-02-23 09:13:12 +01:00
OlgunR
964d508630 Persist grid SizeMode in BandLayout for layout restore
Added SizeMode property to BandLayout in both CatalogsGrid.razor and MassDataGrid.razor. The grid's current SizeMode is now saved and restored with the layout, ensuring user preferences for grid sizing persist across sessions. This improves the consistency of the user experience when reloading or switching layouts.
2026-02-23 08:55:53 +01:00
12 changed files with 623 additions and 882 deletions

View File

@@ -1,60 +1,5 @@
@using System.Text.Json
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.AspNetCore.Components.Forms
@using DevExpress.Blazor
@using DevExpress.Data.Filtering
@inject CatalogApiClient Api @inject CatalogApiClient Api
@inject LayoutApiClient LayoutApi @inject BandLayoutService BandLayoutService
@inject IJSRuntime JsRuntime
<style>
.action-panel { margin-bottom: 16px; }
.grid-section { margin-top: 12px; }
.catalog-edit-popup {
min-width: 720px;
}
.band-editor {
display: grid;
gap: 12px;
margin-bottom: 16px;
}
.band-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.band-row {
display: flex;
gap: 8px;
align-items: center;
}
.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: 160px;
flex: 1 1 160px;
}
.loading-container {
min-height: 160px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
@if (!string.IsNullOrWhiteSpace(errorMessage)) @if (!string.IsNullOrWhiteSpace(errorMessage))
{ {
@@ -75,15 +20,15 @@ else if (!string.IsNullOrWhiteSpace(infoMessage))
} }
else if (items.Count == 0) else if (items.Count == 0)
{ {
<p>Keine Eintr&#228;ge vorhanden.</p> <p>Keine Einträge vorhanden.</p>
} }
else else
{ {
<div class="band-editor"> <div class="band-editor">
<div class="band-controls"> <div class="band-controls">
<DxButton Text="Band hinzuf&#252;gen" Click="AddBand" /> <DxButton Text="Band hinzufügen" Click="AddBand" />
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" /> <DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
<DxButton Text="Band-Layout zur&#252;cksetzen" Click="ResetBandLayoutAsync" /> <DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
</div> </div>
@foreach (var band in bandLayout.Bands) @foreach (var band in bandLayout.Bands)
{ {
@@ -155,7 +100,9 @@ else
@RenderColumns() @RenderColumns()
</Columns> </Columns>
<EditFormTemplate Context="editFormContext"> <EditFormTemplate Context="editFormContext">
@{ SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); } @{
SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew);
}
<DxFormLayout ColCount="2"> <DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="Titel"> <DxFormLayoutItem Caption="Titel">
<DxTextBox @bind-Text="editModel.CatTitle" Width="100%" /> <DxTextBox @bind-Text="editModel.CatTitle" Width="100%" />
@@ -197,13 +144,13 @@ else
private string popupHeaderText = "Edit"; private string popupHeaderText = "Edit";
private const string LayoutType = "GRID_BANDS"; private const string LayoutType = "GRID_BANDS";
private const string LayoutKey = "CatalogsGrid"; private const string LayoutKey = "CatalogsGrid";
private const string LayoutUserStorageKey = "layoutUser";
private string? layoutUser; private string? layoutUser;
private BandLayout bandLayout = new(); private BandLayout bandLayout = new();
private Dictionary<string, string> columnBandAssignments = new(); private Dictionary<string, string> columnBandAssignments = new();
private List<BandOption> bandOptions = new(); private List<BandOption> bandOptions = new();
private Dictionary<string, ColumnDefinition> columnLookup = new(); private Dictionary<string, ColumnDefinition> columnLookup = new();
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web); private bool gridLayoutApplied;
private List<ColumnDefinition> columnDefinitions = new() private List<ColumnDefinition> columnDefinitions = new()
{ {
new() { FieldName = nameof(CatalogReadDto.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text }, new() { FieldName = nameof(CatalogReadDto.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text },
@@ -211,8 +158,8 @@ else
new() { FieldName = nameof(CatalogReadDto.CatString), Caption = "String", 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.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.AddedWhen), Caption = "Angelegt am", ReadOnly = true, FilterType = ColumnFilterType.Date },
new() { FieldName = nameof(CatalogReadDto.ChangedWho), Caption = "Ge&#228;ndert von", ReadOnly = true, FilterType = ColumnFilterType.Text }, new() { FieldName = nameof(CatalogReadDto.ChangedWho), Caption = "Geändert von", ReadOnly = true, FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.ChangedWhen), Caption = "Ge&#228;ndert am", ReadOnly = true, FilterType = ColumnFilterType.Date } new() { FieldName = nameof(CatalogReadDto.ChangedWhen), Caption = "Geändert am", ReadOnly = true, FilterType = ColumnFilterType.Date }
}; };
private readonly List<ProcedureOption> procedureOptions = new() private readonly List<ProcedureOption> procedureOptions = new()
@@ -222,30 +169,32 @@ else
}; };
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser); private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
private bool gridLayoutApplied;
private DevExpress.Blazor.SizeMode _sizeMode = DevExpress.Blazor.SizeMode.Medium; private SizeMode _sizeMode = SizeMode.Medium;
private static readonly List<DevExpress.Blazor.SizeMode> _sizeModes = private static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
Enum.GetValues<DevExpress.Blazor.SizeMode>().ToList();
private string FormatSizeText(DevExpress.Blazor.SizeMode size) => size switch private string FormatSizeText(SizeMode size) => size switch
{ {
DevExpress.Blazor.SizeMode.Small => "Klein", SizeMode.Small => "Klein",
DevExpress.Blazor.SizeMode.Medium => "Mittel", SizeMode.Medium => "Mittel",
DevExpress.Blazor.SizeMode.Large => "Groß", SizeMode.Large => "Groß",
_ => size.ToString() _ => size.ToString()
}; };
private void OnSizeChange(DropDownButtonItemClickEventArgs args) private void OnSizeChange(DropDownButtonItemClickEventArgs args)
{ {
_sizeMode = Enum.Parse<DevExpress.Blazor.SizeMode>(args.ItemInfo.Id); _sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
} }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase); columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
await EnsureLayoutUserAsync(); layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
await LoadBandLayoutAsync(); bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
ApplyColumnLayoutFromStorage();
_sizeMode = bandLayout.SizeMode;
UpdateBandOptions();
await LoadCatalogs(); await LoadCatalogs();
} }
@@ -259,71 +208,6 @@ else
} }
} }
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() private async Task LoadCatalogs()
{ {
isLoading = true; isLoading = true;
@@ -344,11 +228,229 @@ else
} }
} }
private async Task SaveLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
return;
try
{
CaptureColumnLayoutFromGrid();
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
infoMessage = "Layout gespeichert.";
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
}
}
private 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;
if (gridRef != null)
gridRef.LoadLayout(new GridPersistentLayout());
gridLayoutApplied = false;
infoMessage = "Layout zurückgesetzt.";
errorMessage = null;
}
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);
}
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);
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
columnBandAssignments.Remove(key);
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)
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
private void 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();
}
StateHasChanged();
}
private void UpdateBandOptions()
{
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
}
private RenderFragment RenderColumns() => builder =>
{
var seq = 0;
builder.OpenComponent<DxGridCommandColumn>(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<DxGridBandColumn>(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<DxGridDataColumn>(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 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))
{
validationMessageStore.Clear(new FieldIdentifier(editContext.Model, nameof(CatalogEditModel.CatTitle)));
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 OnEditModelSaving(GridEditModelSavingEventArgs e) private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
{ {
errorMessage = null; errorMessage = null;
infoMessage = null; infoMessage = null;
validationMessageStore?.Clear(); validationMessageStore?.Clear();
editContext?.NotifyValidationStateChanged(); editContext?.NotifyValidationStateChanged();
@@ -374,18 +476,12 @@ else
if (!created.Success || created.Value == null) if (!created.Success || created.Value == null)
{ {
if (!string.IsNullOrWhiteSpace(created.Error)) if (!string.IsNullOrWhiteSpace(created.Error))
{
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), created.Error); AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), created.Error);
}
else else
{
errorMessage = "Anlegen fehlgeschlagen."; errorMessage = "Anlegen fehlgeschlagen.";
}
e.Cancel = true; e.Cancel = true;
return; return;
} }
infoMessage = "Katalog angelegt."; infoMessage = "Katalog angelegt.";
focusedRowKey = created.Value.Guid; focusedRowKey = created.Value.Guid;
} }
@@ -398,7 +494,6 @@ else
e.Cancel = true; e.Cancel = true;
return; return;
} }
infoMessage = "Katalog aktualisiert."; infoMessage = "Katalog aktualisiert.";
focusedRowKey = editModel.Guid; focusedRowKey = editModel.Guid;
} }
@@ -414,30 +509,20 @@ else
private void AddValidationError(CatalogEditModel editModel, string fieldName, string message) private void AddValidationError(CatalogEditModel editModel, string fieldName, string message)
{ {
if (editContext == null || validationMessageStore == null) if (editContext == null || validationMessageStore == null) return;
{ validationMessageStore.Add(new FieldIdentifier(editModel, fieldName), message);
return;
}
var field = new FieldIdentifier(editModel, fieldName);
validationMessageStore.Add(field, message);
editContext.NotifyValidationStateChanged(); editContext.NotifyValidationStateChanged();
} }
private bool ValidateEditModel(CatalogEditModel editModel, bool isNew) private bool ValidateEditModel(CatalogEditModel editModel, bool isNew)
{ {
if (isNew) if (isNew) return true;
{
return true;
}
if (editModel.UpdateProcedure == 0 && if (editModel.UpdateProcedure == 0 &&
!string.Equals(editModel.CatTitle, editModel.OriginalCatTitle, StringComparison.OrdinalIgnoreCase)) !string.Equals(editModel.CatTitle, editModel.OriginalCatTitle, StringComparison.OrdinalIgnoreCase))
{ {
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), "Titel kann nicht ge&#228;ndert werden."); AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), "Titel kann nicht geändert werden.");
return false; return false;
} }
return true; return true;
} }
@@ -445,346 +530,26 @@ else
{ {
errorMessage = null; errorMessage = null;
infoMessage = null; infoMessage = null;
var item = (CatalogReadDto)e.DataItem; var item = (CatalogReadDto)e.DataItem;
try try
{ {
var deleted = await Api.DeleteAsync(item.Guid); var deleted = await Api.DeleteAsync(item.Guid);
if (!deleted.Success) if (!deleted.Success)
{ {
errorMessage = deleted.Error ?? "L&#246;schen fehlgeschlagen."; errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
e.Cancel = true; e.Cancel = true;
return; return;
} }
infoMessage = "Katalog gelöscht.";
infoMessage = "Katalog gel&#246;scht.";
await LoadCatalogs(); await LoadCatalogs();
} }
catch (Exception ex) catch (Exception ex)
{ {
errorMessage = $"Fehler beim L&#246;schen: {ex.Message}"; errorMessage = $"Fehler beim Löschen: {ex.Message}";
e.Cancel = true; e.Cancel = true;
} }
} }
private async Task EnsureLayoutUserAsync()
{
layoutUser = await JsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
if (string.IsNullOrWhiteSpace(layoutUser))
{
layoutUser = Guid.NewGuid().ToString("N");
await JsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
}
}
private async Task SaveLayoutAsync()
{
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 = "Layout gespeichert.";
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
}
}
private void CaptureColumnLayoutFromGrid()
{
if (gridRef == null)
{
return;
}
var layout = gridRef.SaveLayout();
bandLayout.GridLayout = layout;
var orderedColumns = layout.Columns
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
.OrderBy(column => column.VisibleIndex)
.ToList();
bandLayout.ColumnOrder = orderedColumns
.Select(column => column.FieldName)
.ToList();
bandLayout.ColumnWidths = orderedColumns
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
}
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<BandLayout>(stored.LayoutData, jsonOptions);
bandLayout = NormalizeBandLayout(parsed);
}
else
{
bandLayout = new BandLayout();
}
columnBandAssignments = BuildAssignmentsFromLayout(bandLayout);
ApplyColumnLayoutFromStorage();
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&#252;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
{
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<BandOption> { 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<BandDefinition>();
layout.ColumnOrder ??= new List<string>();
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
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<string>();
}
return layout;
}
private Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
{
var assignments = new Dictionary<string, string>(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<DxGridCommandColumn>(seq++);
builder.AddAttribute(seq++, "Width", "120px");
builder.CloseComponent();
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<DxGridBandColumn>(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<DxGridDataColumn>(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 sealed class BandLayout
{
public List<BandDefinition> Bands { get; set; } = new();
public List<string> ColumnOrder { get; set; } = new();
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public GridPersistentLayout? GridLayout { get; set; }
}
private sealed class BandDefinition
{
public string Id { get; set; } = string.Empty;
public string Caption { get; set; } = string.Empty;
public List<string> 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 private sealed class CatalogEditModel
{ {
public int Guid { get; set; } public int Guid { get; set; }
@@ -800,10 +565,4 @@ else
public int Value { get; set; } public int Value { get; set; }
public string Text { get; set; } = string.Empty; 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;
}
} }

View File

@@ -0,0 +1,3 @@
.catalog-edit-popup {
min-width: 720px;
}

View File

@@ -1,68 +1,5 @@
@using System.Text.Json
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.AspNetCore.Components.Forms
@using DevExpress.Blazor
@using DevExpress.Data.Filtering
@inject MassDataApiClient Api @inject MassDataApiClient Api
@inject LayoutApiClient LayoutApi @inject BandLayoutService BandLayoutService
@inject IJSRuntime JsRuntime
<style>
.action-panel { margin-bottom: 16px; }
.grid-section { margin-top: 12px; }
.pager-container {
display: flex;
justify-content: center;
margin-top: 12px;
margin-bottom: 16px;
}
.page-size-selector {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
}
.page-size-label {
white-space: nowrap;
}
.page-size-combo {
width: 13ch;
min-width: 13ch;
max-width: 13ch;
}
.page-size-combo input {
text-align: left;
}
.massdata-edit-popup {
min-width: 720px;
}
.band-editor {
display: grid;
gap: 12px;
margin-bottom: 16px;
}
.band-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.band-row {
display: flex;
gap: 8px;
align-items: center;
}
.band-columns {
max-width: 720px;
}
.loading-container {
min-height: 160px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
@if (!string.IsNullOrWhiteSpace(errorMessage)) @if (!string.IsNullOrWhiteSpace(errorMessage))
{ {
@@ -83,12 +20,12 @@ else if (!string.IsNullOrWhiteSpace(infoMessage))
} }
else if (items.Count == 0) else if (items.Count == 0)
{ {
<p>Keine Eintr&#228;ge vorhanden.</p> <p>Keine Einträge vorhanden.</p>
} }
else else
{ {
<div class="mb-3 page-size-selector"> <div class="mb-3 page-size-selector">
<span class="page-size-label">Datens&#228;tze je Seite:</span> <span class="page-size-label">Datensätze je Seite:</span>
<DxComboBox Data="@pageSizeOptions" <DxComboBox Data="@pageSizeOptions"
TData="PageSizeOption" TData="PageSizeOption"
TValue="int?" TValue="int?"
@@ -101,9 +38,9 @@ else
<div class="band-editor"> <div class="band-editor">
<div class="band-controls"> <div class="band-controls">
<DxButton Text="Band hinzuf&#252;gen" Click="AddBand" /> <DxButton Text="Band hinzufügen" Click="AddBand" />
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" /> <DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
<DxButton Text="Band-Layout zur&#252;cksetzen" Click="ResetBandLayoutAsync" /> <DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
</div> </div>
@foreach (var band in bandLayout.Bands) @foreach (var band in bandLayout.Bands)
{ {
@@ -175,7 +112,9 @@ else
@RenderColumns() @RenderColumns()
</Columns> </Columns>
<EditFormTemplate Context="editFormContext"> <EditFormTemplate Context="editFormContext">
@{ SetEditContext(editFormContext.EditContext); var editModel = (MassDataEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); } @{
SetEditContext(editFormContext.EditContext); var editModel = (MassDataEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew);
}
<DxFormLayout ColCount="2"> <DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="CustomerName"> <DxFormLayoutItem Caption="CustomerName">
<DxTextBox @bind-Text="editModel.CustomerName" Width="100%" /> <DxTextBox @bind-Text="editModel.CustomerName" Width="100%" />
@@ -233,13 +172,13 @@ else
private int? focusedRowKey; private int? focusedRowKey;
private const string LayoutType = "GRID_BANDS"; private const string LayoutType = "GRID_BANDS";
private const string LayoutKey = "MassDataGrid"; private const string LayoutKey = "MassDataGrid";
private const string LayoutUserStorageKey = "layoutUser";
private string? layoutUser; private string? layoutUser;
private BandLayout bandLayout = new(); private BandLayout bandLayout = new();
private Dictionary<string, string> columnBandAssignments = new(); private Dictionary<string, string> columnBandAssignments = new();
private List<BandOption> bandOptions = new(); private List<BandOption> bandOptions = new();
private Dictionary<string, ColumnDefinition> columnLookup = new(); private Dictionary<string, ColumnDefinition> columnLookup = new();
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web); private bool gridLayoutApplied;
private List<ColumnDefinition> columnDefinitions = new() private List<ColumnDefinition> columnDefinitions = new()
{ {
new() { FieldName = nameof(MassDataReadDto.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text }, new() { FieldName = nameof(MassDataReadDto.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text },
@@ -251,15 +190,13 @@ else
new() { FieldName = nameof(MassDataReadDto.ChangedWhen), Caption = "Changed", ReadOnly = true, FilterType = ColumnFilterType.Date } new() { FieldName = nameof(MassDataReadDto.ChangedWhen), Caption = "Changed", ReadOnly = true, FilterType = ColumnFilterType.Date }
}; };
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
private readonly List<PageSizeOption> pageSizeOptions = new() private readonly List<PageSizeOption> pageSizeOptions = new()
{ {
new() { Value = 100, Text = "100" }, new() { Value = 100, Text = "100" },
new() { Value = 1000, Text = "1.000" }, new() { Value = 1000, Text = "1.000" },
new() { Value = 10000, Text = "10.000" }, new() { Value = 10000, Text = "10.000" },
new() { Value = 100000, Text = "100.000" }, new() { Value = 100000, Text = "100.000" },
new() { Value = null, Text = "Alle" } new() { Value = null, Text = "Alle" }
}; };
private readonly List<ProcedureOption> procedureOptions = new() private readonly List<ProcedureOption> procedureOptions = new()
@@ -267,33 +204,46 @@ else
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" } new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
}; };
private bool gridLayoutApplied; private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
private DevExpress.Blazor.SizeMode _sizeMode = DevExpress.Blazor.SizeMode.Medium; private SizeMode _sizeMode = SizeMode.Medium;
private static readonly List<DevExpress.Blazor.SizeMode> _sizeModes = private static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
Enum.GetValues<DevExpress.Blazor.SizeMode>().ToList();
private string FormatSizeText(DevExpress.Blazor.SizeMode size) => size switch private string FormatSizeText(SizeMode size) => size switch
{ {
DevExpress.Blazor.SizeMode.Small => "Klein", SizeMode.Small => "Klein",
DevExpress.Blazor.SizeMode.Medium => "Mittel", SizeMode.Medium => "Mittel",
DevExpress.Blazor.SizeMode.Large => "Groß", SizeMode.Large => "Groß",
_ => size.ToString() _ => size.ToString()
}; };
private void OnSizeChange(DropDownButtonItemClickEventArgs args) private void OnSizeChange(DropDownButtonItemClickEventArgs args)
{ {
_sizeMode = Enum.Parse<DevExpress.Blazor.SizeMode>(args.ItemInfo.Id); _sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
} }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase); columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
await EnsureLayoutUserAsync(); layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
await LoadBandLayoutAsync(); bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
ApplyColumnLayoutFromStorage();
_sizeMode = bandLayout.SizeMode;
UpdateBandOptions();
await LoadPage(0); await LoadPage(0);
} }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
{
gridRef.LoadLayout(bandLayout.GridLayout);
gridLayoutApplied = true;
await InvokeAsync(StateHasChanged);
}
}
private async Task LoadPage(int page) private async Task LoadPage(int page)
{ {
isLoading = true; isLoading = true;
@@ -304,7 +254,6 @@ else
var effectivePageSize = pageSize ?? (total == 0 ? 1 : total); var effectivePageSize = pageSize ?? (total == 0 ? 1 : total);
pageCount = Math.Max(1, (int)Math.Ceiling(total / (double)effectivePageSize)); pageCount = Math.Max(1, (int)Math.Ceiling(total / (double)effectivePageSize));
pageIndex = Math.Clamp(page, 0, pageCount - 1); pageIndex = Math.Clamp(page, 0, pageCount - 1);
var skip = pageSize.HasValue ? pageIndex * pageSize.Value : 0; var skip = pageSize.HasValue ? pageIndex * pageSize.Value : 0;
items = await Api.GetAllAsync(skip, pageSize); items = await Api.GetAllAsync(skip, pageSize);
} }
@@ -320,10 +269,7 @@ else
} }
} }
private async Task OnPageChanged(int index) private async Task OnPageChanged(int index) => await LoadPage(index);
{
await LoadPage(index);
}
private async Task OnPageSizeChanged(int? size) private async Task OnPageSizeChanged(int? size)
{ {
@@ -331,60 +277,15 @@ else
await LoadPage(0); await LoadPage(0);
} }
private async Task EnsureLayoutUserAsync()
{
layoutUser = await JsRuntime.InvokeAsync<string?>("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<BandLayout>(stored.LayoutData, jsonOptions);
bandLayout = NormalizeBandLayout(parsed);
}
else
{
bandLayout = new BandLayout();
}
columnBandAssignments = BuildAssignmentsFromLayout(bandLayout);
ApplyColumnLayoutFromStorage();
UpdateBandOptions();
}
private async Task SaveLayoutAsync() private async Task SaveLayoutAsync()
{ {
if (string.IsNullOrWhiteSpace(layoutUser)) if (string.IsNullOrWhiteSpace(layoutUser))
{
return; return;
}
try try
{ {
CaptureColumnLayoutFromGrid(); CaptureColumnLayoutFromGrid();
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
var layoutData = JsonSerializer.Serialize(bandLayout, jsonOptions);
await LayoutApi.UpsertAsync(new LayoutDto
{
LayoutType = LayoutType,
LayoutKey = LayoutKey,
UserName = layoutUser,
LayoutData = layoutData
});
infoMessage = "Layout gespeichert."; infoMessage = "Layout gespeichert.";
errorMessage = null; errorMessage = null;
} }
@@ -394,42 +295,49 @@ else
} }
} }
private void CaptureColumnLayoutFromGrid() private async Task ResetLayoutAsync()
{
if (gridRef == null)
{
return;
}
var layout = gridRef.SaveLayout();
bandLayout.GridLayout = layout;
var orderedColumns = layout.Columns
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
.OrderBy(column => column.VisibleIndex)
.ToList();
bandLayout.ColumnOrder = orderedColumns
.Select(column => column.FieldName)
.ToList();
bandLayout.ColumnWidths = orderedColumns
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
}
private async Task ResetBandLayoutAsync()
{ {
if (string.IsNullOrWhiteSpace(layoutUser)) if (string.IsNullOrWhiteSpace(layoutUser))
{
return; return;
}
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser); await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
bandLayout = new BandLayout(); bandLayout = new BandLayout();
columnBandAssignments.Clear(); columnBandAssignments.Clear();
UpdateBandOptions(); UpdateBandOptions();
infoMessage = "Band-Layout zur&#252;ckgesetzt.";
foreach (var column in columnDefinitions)
column.Width = null;
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
_sizeMode = SizeMode.Medium;
if (gridRef != null)
gridRef.LoadLayout(new GridPersistentLayout());
gridLayoutApplied = false;
infoMessage = "Layout zurückgesetzt.";
errorMessage = null;
}
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() private void ApplyColumnLayoutFromStorage()
@@ -437,34 +345,22 @@ else
foreach (var column in columnDefinitions) foreach (var column in columnDefinitions)
{ {
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width)) if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
{
column.Width = width; column.Width = width;
}
} }
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
} }
private void AddBand() private void AddBand()
{ {
bandLayout.Bands.Add(new BandDefinition bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
{
Id = Guid.NewGuid().ToString("N"),
Caption = "Band"
});
UpdateBandOptions(); UpdateBandOptions();
} }
private void RemoveBand(BandDefinition band) private void RemoveBand(BandDefinition band)
{ {
bandLayout.Bands.Remove(band); bandLayout.Bands.Remove(band);
var removedColumns = columnBandAssignments.Where(pair => pair.Value == band.Id) foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
.Select(pair => pair.Key) columnBandAssignments.Remove(key);
.ToList();
foreach (var column in removedColumns)
{
columnBandAssignments.Remove(column);
}
UpdateBandOptions(); UpdateBandOptions();
SyncBandsFromAssignments(); SyncBandsFromAssignments();
} }
@@ -478,77 +374,31 @@ else
private void UpdateColumnBand(string fieldName, string? bandId) private void UpdateColumnBand(string fieldName, string? bandId)
{ {
if (string.IsNullOrWhiteSpace(bandId)) if (string.IsNullOrWhiteSpace(bandId))
{
columnBandAssignments.Remove(fieldName); columnBandAssignments.Remove(fieldName);
}
else else
{
columnBandAssignments[fieldName] = bandId; columnBandAssignments[fieldName] = bandId;
}
SyncBandsFromAssignments(); SyncBandsFromAssignments();
} }
private string GetColumnBand(string fieldName) private string GetColumnBand(string fieldName)
{ => columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
return columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
}
private void SyncBandsFromAssignments() private void SyncBandsFromAssignments()
{ {
foreach (var band in bandLayout.Bands) foreach (var band in bandLayout.Bands)
{ {
band.Columns = columnDefinitions band.Columns = columnDefinitions
.Where(column => columnBandAssignments.TryGetValue(column.FieldName, out var bandId) && bandId == band.Id) .Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
.Select(column => column.FieldName) .Select(c => c.FieldName)
.ToList(); .ToList();
} }
StateHasChanged(); StateHasChanged();
} }
private void UpdateBandOptions() private void UpdateBandOptions()
{ {
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } }; bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
bandOptions.AddRange(bandLayout.Bands.Select(band => new BandOption { Id = band.Id, Caption = band.Caption })); bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
}
private Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
{
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var band in layout.Bands)
{
foreach (var column in band.Columns)
{
assignments[column] = band.Id;
}
}
return assignments;
}
private BandLayout NormalizeBandLayout(BandLayout? layout)
{
layout ??= new BandLayout();
layout.Bands ??= new List<BandDefinition>();
layout.ColumnOrder ??= new List<string>();
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
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<string>();
}
return layout;
} }
private RenderFragment RenderColumns() => builder => private RenderFragment RenderColumns() => builder =>
@@ -558,18 +408,13 @@ else
builder.AddAttribute(seq++, "Width", "120px"); builder.AddAttribute(seq++, "Width", "120px");
builder.CloseComponent(); builder.CloseComponent();
var grouped = bandLayout.Bands.SelectMany(band => band.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase); var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var column in columnDefinitions.Where(column => !grouped.Contains(column.FieldName))) foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
{
BuildDataColumn(builder, ref seq, column); BuildDataColumn(builder, ref seq, column);
}
foreach (var band in bandLayout.Bands) foreach (var band in bandLayout.Bands)
{ {
if (band.Columns.Count == 0) if (band.Columns.Count == 0) continue;
{
continue;
}
builder.OpenComponent<DxGridBandColumn>(seq++); builder.OpenComponent<DxGridBandColumn>(seq++);
builder.AddAttribute(seq++, "Caption", band.Caption); builder.AddAttribute(seq++, "Caption", band.Caption);
@@ -579,9 +424,7 @@ else
foreach (var columnName in band.Columns) foreach (var columnName in band.Columns)
{ {
if (columnLookup.TryGetValue(columnName, out var column)) if (columnLookup.TryGetValue(columnName, out var column))
{
BuildDataColumn(bandBuilder, ref bandSeq, column); BuildDataColumn(bandBuilder, ref bandSeq, column);
}
} }
})); }));
builder.CloseComponent(); builder.CloseComponent();
@@ -594,35 +437,19 @@ else
builder.AddAttribute(seq++, "FieldName", column.FieldName); builder.AddAttribute(seq++, "FieldName", column.FieldName);
builder.AddAttribute(seq++, "Caption", column.Caption); builder.AddAttribute(seq++, "Caption", column.Caption);
if (!string.IsNullOrWhiteSpace(column.Width)) if (!string.IsNullOrWhiteSpace(column.Width))
{
builder.AddAttribute(seq++, "Width", column.Width); builder.AddAttribute(seq++, "Width", column.Width);
}
if (!string.IsNullOrWhiteSpace(column.DisplayFormat)) if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
{
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat); builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
}
if (column.ReadOnly) if (column.ReadOnly)
{
builder.AddAttribute(seq++, "ReadOnly", true); builder.AddAttribute(seq++, "ReadOnly", true);
}
builder.CloseComponent(); builder.CloseComponent();
} }
private void SetEditContext(EditContext context) private void SetEditContext(EditContext context)
{ {
if (editContext == context) if (editContext == context) return;
{
return;
}
if (editContext != null) if (editContext != null)
{
editContext.OnFieldChanged -= OnEditFieldChanged; editContext.OnFieldChanged -= OnEditFieldChanged;
}
editContext = context; editContext = context;
validationMessageStore = new ValidationMessageStore(editContext); validationMessageStore = new ValidationMessageStore(editContext);
editContext.OnFieldChanged += OnEditFieldChanged; editContext.OnFieldChanged += OnEditFieldChanged;
@@ -630,10 +457,7 @@ else
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e) private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
{ {
if (validationMessageStore == null || editContext == null) if (validationMessageStore == null || editContext == null) return;
{
return;
}
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure)) if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure))
{ {
@@ -644,16 +468,12 @@ else
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.CustomerName)) if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.CustomerName))
{ {
var field = new FieldIdentifier(editContext.Model, nameof(MassDataEditModel.CustomerName)); validationMessageStore.Clear(new FieldIdentifier(editContext.Model, nameof(MassDataEditModel.CustomerName)));
validationMessageStore.Clear(field);
editContext.NotifyValidationStateChanged(); editContext.NotifyValidationStateChanged();
} }
} }
private void SetPopupHeaderText(bool isNew) private void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
{
popupHeaderText = isNew ? "Neu" : "Edit";
}
private async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e) private async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{ {
@@ -683,14 +503,13 @@ else
{ {
errorMessage = null; errorMessage = null;
infoMessage = null; infoMessage = null;
validationMessageStore?.Clear(); validationMessageStore?.Clear();
editContext?.NotifyValidationStateChanged(); editContext?.NotifyValidationStateChanged();
var editModel = (MassDataEditModel)e.EditModel; var editModel = (MassDataEditModel)e.EditModel;
if (!decimal.TryParse(editModel.AmountText, out var amount)) if (!decimal.TryParse(editModel.AmountText, out var amount))
{ {
AddValidationError(editModel, nameof(MassDataEditModel.AmountText), "Amount ist ung&#252;ltig."); AddValidationError(editModel, nameof(MassDataEditModel.AmountText), "Amount ist ungültig.");
e.Cancel = true; e.Cancel = true;
return; return;
} }
@@ -730,72 +549,19 @@ else
private void AddValidationError(MassDataEditModel editModel, string fieldName, string message) private void AddValidationError(MassDataEditModel editModel, string fieldName, string message)
{ {
if (editContext == null || validationMessageStore == null) if (editContext == null || validationMessageStore == null) return;
{ validationMessageStore.Add(new FieldIdentifier(editModel, fieldName), message);
return;
}
var field = new FieldIdentifier(editModel, fieldName);
validationMessageStore.Add(field, message);
editContext.NotifyValidationStateChanged(); editContext.NotifyValidationStateChanged();
} }
private Task OnDataItemDeleting(GridDataItemDeletingEventArgs e) private Task OnDataItemDeleting(GridDataItemDeletingEventArgs e)
{ {
errorMessage = null; errorMessage = null;
infoMessage = "L&#246;schen ist aktuell noch nicht verf&#252;gbar."; infoMessage = "Löschen ist aktuell noch nicht verfügbar.";
e.Cancel = true; e.Cancel = true;
return Task.CompletedTask; return Task.CompletedTask;
} }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
{
gridRef.LoadLayout(bandLayout.GridLayout);
gridLayoutApplied = true;
await InvokeAsync(StateHasChanged);
}
}
private sealed class BandLayout
{
public List<BandDefinition> Bands { get; set; } = new();
public List<string> ColumnOrder { get; set; } = new();
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public GridPersistentLayout? GridLayout { get; set; }
}
private sealed class BandDefinition
{
public string Id { get; set; } = string.Empty;
public string Caption { get; set; } = string.Empty;
public List<string> 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,
Bool,
Date
}
private sealed class MassDataEditModel private sealed class MassDataEditModel
{ {
public int Id { get; set; } public int Id { get; set; }
@@ -814,21 +580,9 @@ else
public string Text { get; set; } = string.Empty; public string Text { get; set; } = string.Empty;
} }
private sealed class BoolFilterOption
{
public bool? Value { get; set; }
public string Text { get; set; } = string.Empty;
}
private sealed class PageSizeOption private sealed class PageSizeOption
{ {
public int? Value { get; set; } public int? Value { get; set; }
public string Text { get; set; } = string.Empty; 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;
}
} }

View File

@@ -0,0 +1,3 @@
.massdata-edit-popup {
min-width: 720px;
}

View File

@@ -5,46 +5,6 @@
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject DashboardApiClient DashboardApi @inject DashboardApiClient DashboardApi
<style>
.dashboard-shell {
display: flex;
gap: 0;
min-height: 800px;
border: 1px solid #e6e6e6;
border-radius: 6px;
overflow: hidden;
background: #fff;
}
.dashboard-nav {
width: 220px;
border-right: 1px solid #e6e6e6;
background: #fafafa;
}
.dashboard-nav-title {
padding: 0.75rem 1rem 0.5rem;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #6c757d;
font-weight: 600;
}
.dashboard-nav-link {
display: block;
padding: 0.55rem 1rem;
color: inherit;
text-decoration: none;
}
.dashboard-nav-link.active {
background: #e9ecef;
font-weight: 600;
}
.dashboard-content {
flex: 1;
min-width: 0;
padding: 1rem;
}
</style>
<PageTitle>Dashboards</PageTitle> <PageTitle>Dashboards</PageTitle>
<div class="dashboard-shell"> <div class="dashboard-shell">

View File

@@ -0,0 +1,42 @@
.dashboard-shell {
display: flex;
gap: 0;
min-height: 800px;
border: 1px solid #e6e6e6;
border-radius: 6px;
overflow: hidden;
background: #fff;
}
.dashboard-nav {
width: 220px;
border-right: 1px solid #e6e6e6;
background: #fafafa;
}
.dashboard-nav-title {
padding: 0.75rem 1rem 0.5rem;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #6c757d;
font-weight: 600;
}
.dashboard-nav-link {
display: block;
padding: 0.55rem 1rem;
color: inherit;
text-decoration: none;
}
.dashboard-nav-link.active {
background: #e9ecef;
font-weight: 600;
}
.dashboard-content {
flex: 1;
min-width: 0;
padding: 1rem;
}

View File

@@ -1,6 +1,8 @@
@using System.Net.Http @using System.Net.Http
@using System.Net.Http.Json @using System.Net.Http.Json
@using System.Text.Json
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode @using static Microsoft.AspNetCore.Components.Web.RenderMode
@@ -10,8 +12,9 @@
@using DbFirst.BlazorWebApp @using DbFirst.BlazorWebApp
@using DbFirst.BlazorWebApp.Components @using DbFirst.BlazorWebApp.Components
@using DbFirst.BlazorWebApp.Models @using DbFirst.BlazorWebApp.Models
@using DbFirst.BlazorWebApp.Models.Grid
@using DbFirst.BlazorWebApp.Services @using DbFirst.BlazorWebApp.Services
@using DevExpress.Blazor @using DevExpress.Blazor
@using DevExpress.DashboardBlazor @using DevExpress.DashboardBlazor
@using DevExpress.DashboardWeb @using DevExpress.DashboardWeb
@using DbFirst.BlazorWebApp @using DevExpress.Data.Filtering

View File

@@ -11,9 +11,6 @@
<PackageReference Include="DevExpress.Blazor.Dashboard" Version="25.2.3" /> <PackageReference Include="DevExpress.Blazor.Dashboard" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Themes" Version="25.2.3" /> <PackageReference Include="DevExpress.Blazor.Themes" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" /> <PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DevExpress.Blazor" Version="25.2.3" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.22" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.22" />
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,43 @@
using DevExpress.Blazor;
namespace DbFirst.BlazorWebApp.Models.Grid
{
public class BandLayout
{
public List<BandDefinition> Bands { get; set; } = new();
public List<string> ColumnOrder { get; set; } = new();
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public GridPersistentLayout? GridLayout { get; set; }
public SizeMode SizeMode { get; set; } = SizeMode.Medium;
}
public class BandDefinition
{
public string Id { get; set; } = string.Empty;
public string Caption { get; set; } = string.Empty;
public List<string> Columns { get; set; } = new();
}
public class BandOption
{
public string Id { get; set; } = string.Empty;
public string Caption { get; set; } = string.Empty;
}
public 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; }
}
public enum ColumnFilterType
{
Text,
Bool,
Date
}
}

View File

@@ -10,6 +10,7 @@ builder.Services.AddRazorComponents()
builder.Services.AddDevExpressBlazor(options => options.BootstrapVersion = BootstrapVersion.v5); builder.Services.AddDevExpressBlazor(options => options.BootstrapVersion = BootstrapVersion.v5);
builder.Services.AddScoped<ThemeState>(); builder.Services.AddScoped<ThemeState>();
builder.Services.AddScoped<BandLayoutService>();
var apiBaseUrl = builder.Configuration["ApiBaseUrl"]; var apiBaseUrl = builder.Configuration["ApiBaseUrl"];
if (!string.IsNullOrWhiteSpace(apiBaseUrl)) if (!string.IsNullOrWhiteSpace(apiBaseUrl))

View File

@@ -0,0 +1,103 @@
using DbFirst.BlazorWebApp.Models;
using DbFirst.BlazorWebApp.Models.Grid;
using Microsoft.JSInterop;
using System.Text.Json;
namespace DbFirst.BlazorWebApp.Services
{
public class BandLayoutService(LayoutApiClient layoutApi, IJSRuntime jsRuntime)
{
private const string LayoutUserStorageKey = "layoutUser";
private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
public async Task<string> EnsureLayoutUserAsync()
{
var layoutUser = await jsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
if (string.IsNullOrWhiteSpace(layoutUser))
{
layoutUser = Guid.NewGuid().ToString("N");
await jsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
}
return layoutUser;
}
public async Task<BandLayout> LoadBandLayoutAsync(
string layoutType,
string layoutKey,
string layoutUser,
Dictionary<string, ColumnDefinition> columnLookup)
{
if (string.IsNullOrWhiteSpace(layoutUser))
return new BandLayout();
var stored = await layoutApi.GetAsync(layoutType, layoutKey, layoutUser);
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
{
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, _jsonOptions);
return NormalizeBandLayout(parsed, columnLookup);
}
return new BandLayout();
}
public async Task SaveBandLayoutAsync(
string layoutType,
string layoutKey,
string layoutUser,
BandLayout bandLayout)
{
var layoutData = JsonSerializer.Serialize(bandLayout, _jsonOptions);
await layoutApi.UpsertAsync(new LayoutDto
{
LayoutType = layoutType,
LayoutKey = layoutKey,
UserName = layoutUser,
LayoutData = layoutData
});
}
public async Task ResetBandLayoutAsync(
string layoutType,
string layoutKey,
string layoutUser)
{
await layoutApi.DeleteAsync(layoutType, layoutKey, layoutUser);
}
public Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
{
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var band in layout.Bands)
{
foreach (var column in band.Columns)
{
assignments[column] = band.Id;
}
}
return assignments;
}
public BandLayout NormalizeBandLayout(
BandLayout? layout,
Dictionary<string, ColumnDefinition> columnLookup)
{
layout ??= new BandLayout();
layout.Bands ??= new List<BandDefinition>();
layout.ColumnOrder ??= new List<string>();
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
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<string>();
}
return layout;
}
}
}

View File

@@ -27,7 +27,7 @@ a, .btn-link {
} }
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
} }
.content { .content {
@@ -51,7 +51,7 @@ h1:focus {
} }
.blazor-error-boundary { .blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA9NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDg2IDY2LjAxODMgMjYzLjU4NiA2Ni4wMTgzWk0yNjMuNTc2IDg2LjA1NDdDMjYxLjA0OSA4Ni4wNTQ3IDI1OS43ODUgODcuMzAwNSAxNTEuMDIyIDg5Ljc5MjEgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDg2IDY2LjAxODMgMjYzLjU4NiA2Ni4wMTgzWk0yNjMuNTc2IDg2LjA1NDdDMjYxLjA0OSA4Ni4wNTQ3IDI1OS43ODUgODcuMzAwNSAyNTkuNzg2IDg5Ljc5MjEgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem; padding: 1rem 1rem 1rem 3.7rem;
color: white; color: white;
} }
@@ -63,3 +63,76 @@ h1:focus {
.darker-border-checkbox.form-check-input { .darker-border-checkbox.form-check-input {
border-color: #929292; border-color: #929292;
} }
/* Grid Band-Editor */
.band-editor {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px;
margin-top: 4px;
margin-bottom: 16px;
border: 1px solid #dee2e6;
border-radius: 4px;
background-color: #f8f9fa;
}
.band-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.band-row {
display: flex;
gap: 8px;
align-items: center;
}
.band-columns {
max-width: 720px;
margin-top: 4px;
}
.grid-section {
margin-top: 4px;
}
/* MassData-spezifisch */
.page-size-selector {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
margin-bottom: 12px;
}
.page-size-label {
white-space: nowrap;
}
.page-size-combo {
width: 13ch;
min-width: 13ch;
max-width: 13ch;
}
.page-size-combo input {
text-align: left;
}
.pager-container {
display: flex;
justify-content: center;
margin-top: 12px;
margin-bottom: 16px;
}
/* Lade-Spinner */
.loading-container {
min-height: 160px;
display: flex;
align-items: center;
justify-content: center;
}