Compare commits
3 Commits
9d7b3591cc
...
2a730ddfcc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a730ddfcc | ||
|
|
d78fd5e3d1 | ||
|
|
52b2cf9a5b |
94
DbFirst.API/Controllers/LayoutsController.cs
Normal file
94
DbFirst.API/Controllers/LayoutsController.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using System.Text;
|
||||||
|
using DbFirst.Application.Repositories;
|
||||||
|
using DbFirst.Domain.Entities;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace DbFirst.API.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class LayoutsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ILayoutRepository _repository;
|
||||||
|
|
||||||
|
public LayoutsController(ILayoutRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<LayoutDto>> Get([FromQuery] string layoutType, [FromQuery] string layoutKey, [FromQuery] string userName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(layoutType) || string.IsNullOrWhiteSpace(layoutKey) || string.IsNullOrWhiteSpace(userName))
|
||||||
|
{
|
||||||
|
return BadRequest("layoutType, layoutKey und userName sind erforderlich.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var entity = await _repository.GetAsync(layoutType, layoutKey, userName, cancellationToken);
|
||||||
|
if (entity == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(Map(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<LayoutDto>> Upsert(LayoutDto dto, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(dto.LayoutType) || string.IsNullOrWhiteSpace(dto.LayoutKey) || string.IsNullOrWhiteSpace(dto.UserName))
|
||||||
|
{
|
||||||
|
return BadRequest("LayoutType, LayoutKey und UserName sind erforderlich.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = string.IsNullOrWhiteSpace(dto.LayoutData)
|
||||||
|
? Array.Empty<byte>()
|
||||||
|
: Encoding.UTF8.GetBytes(dto.LayoutData);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await _repository.UpsertAsync(dto.LayoutType, dto.LayoutKey, dto.UserName, data, cancellationToken);
|
||||||
|
return Ok(Map(entity));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var detail = ex.InnerException?.Message ?? ex.Message;
|
||||||
|
return Problem(detail: detail, statusCode: StatusCodes.Status500InternalServerError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
public async Task<IActionResult> Delete([FromQuery] string layoutType, [FromQuery] string layoutKey, [FromQuery] string userName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(layoutType) || string.IsNullOrWhiteSpace(layoutKey) || string.IsNullOrWhiteSpace(userName))
|
||||||
|
{
|
||||||
|
return BadRequest("layoutType, layoutKey und userName sind erforderlich.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleted = await _repository.DeleteAsync(layoutType, layoutKey, userName, cancellationToken);
|
||||||
|
return deleted ? NoContent() : NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LayoutDto Map(SmfLayout entity)
|
||||||
|
{
|
||||||
|
var layoutData = entity.LayoutData.Length == 0
|
||||||
|
? string.Empty
|
||||||
|
: Encoding.UTF8.GetString(entity.LayoutData);
|
||||||
|
|
||||||
|
return new LayoutDto
|
||||||
|
{
|
||||||
|
LayoutType = entity.LayoutType,
|
||||||
|
LayoutKey = entity.LayoutKey,
|
||||||
|
UserName = entity.UserName,
|
||||||
|
LayoutData = layoutData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LayoutDto
|
||||||
|
{
|
||||||
|
public string LayoutType { get; set; } = string.Empty;
|
||||||
|
public string LayoutKey { get; set; } = string.Empty;
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
public string LayoutData { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,12 @@ public class MassDataController : ControllerBase
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<IEnumerable<MassDataReadDto>>> GetAll([FromQuery] int? skip, [FromQuery] int? take, CancellationToken cancellationToken)
|
public async Task<ActionResult<IEnumerable<MassDataReadDto>>> GetAll([FromQuery] int? skip, [FromQuery] int? take, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var resolvedTake = take is null or <= 0 ? 200 : take;
|
int? resolvedTake = take;
|
||||||
|
if (resolvedTake is <= 0)
|
||||||
|
{
|
||||||
|
resolvedTake = null;
|
||||||
|
}
|
||||||
|
|
||||||
var result = await _mediator.Send(new GetAllMassDataQuery(skip, resolvedTake), cancellationToken);
|
var result = await _mediator.Send(new GetAllMassDataQuery(skip, resolvedTake), cancellationToken);
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ builder.Services.AddApplication();
|
|||||||
|
|
||||||
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
|
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
|
||||||
builder.Services.AddScoped<IMassDataRepository, MassDataRepository>();
|
builder.Services.AddScoped<IMassDataRepository, MassDataRepository>();
|
||||||
|
builder.Services.AddScoped<ILayoutRepository, LayoutRepository>();
|
||||||
|
|
||||||
builder.Services.AddDevExpressControls();
|
builder.Services.AddDevExpressControls();
|
||||||
builder.Services.AddSignalR();
|
builder.Services.AddSignalR();
|
||||||
|
|||||||
10
DbFirst.Application/Repositories/ILayoutRepository.cs
Normal file
10
DbFirst.Application/Repositories/ILayoutRepository.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using DbFirst.Domain.Entities;
|
||||||
|
|
||||||
|
namespace DbFirst.Application.Repositories;
|
||||||
|
|
||||||
|
public interface ILayoutRepository
|
||||||
|
{
|
||||||
|
Task<SmfLayout?> GetAsync(string layoutType, string layoutKey, string userName, CancellationToken cancellationToken = default);
|
||||||
|
Task<SmfLayout> UpsertAsync(string layoutType, string layoutKey, string userName, byte[] layoutData, CancellationToken cancellationToken = default);
|
||||||
|
Task<bool> DeleteAsync(string layoutType, string layoutKey, string userName, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
|
@using System.Text.Json
|
||||||
|
@using Microsoft.AspNetCore.Components
|
||||||
|
@using Microsoft.AspNetCore.Components.Rendering
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using DevExpress.Blazor
|
||||||
@inject CatalogApiClient Api
|
@inject CatalogApiClient Api
|
||||||
|
@inject LayoutApiClient LayoutApi
|
||||||
|
@inject IJSRuntime JsRuntime
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.action-panel { margin-bottom: 16px; }
|
.action-panel { margin-bottom: 16px; }
|
||||||
@@ -48,6 +54,25 @@
|
|||||||
.catalog-edit-popup {
|
.catalog-edit-popup {
|
||||||
min-width: 720px;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||||
@@ -69,11 +94,44 @@ else if (items.Count == 0)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<div class="band-editor">
|
||||||
|
<div class="band-controls">
|
||||||
|
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||||
|
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||||
|
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
||||||
|
</div>
|
||||||
|
@foreach (var band in bandLayout.Bands)
|
||||||
|
{
|
||||||
|
<div class="band-row">
|
||||||
|
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
|
||||||
|
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<DxFormLayout CssClass="band-columns" ColCount="2">
|
||||||
|
@foreach (var column in columnDefinitions)
|
||||||
|
{
|
||||||
|
<DxFormLayoutItem Caption="@column.Caption">
|
||||||
|
<DxComboBox Data="@bandOptions"
|
||||||
|
TData="BandOption"
|
||||||
|
TValue="string"
|
||||||
|
TextFieldName="Caption"
|
||||||
|
ValueFieldName="Id"
|
||||||
|
Value="@GetColumnBand(column.FieldName)"
|
||||||
|
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
|
||||||
|
Width="100%" />
|
||||||
|
</DxFormLayoutItem>
|
||||||
|
}
|
||||||
|
</DxFormLayout>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid-section">
|
<div class="grid-section">
|
||||||
<DxGrid Data="@items"
|
<DxGrid Data="@items"
|
||||||
TItem="CatalogReadDto"
|
TItem="CatalogReadDto"
|
||||||
KeyFieldName="@nameof(CatalogReadDto.Guid)"
|
KeyFieldName="@nameof(CatalogReadDto.Guid)"
|
||||||
ShowFilterRow="true"
|
ShowFilterRow="true"
|
||||||
|
AllowColumnResize="true"
|
||||||
|
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||||
|
AllowColumnReorder="true"
|
||||||
PageSize="10"
|
PageSize="10"
|
||||||
CssClass="mb-4 catalog-grid"
|
CssClass="mb-4 catalog-grid"
|
||||||
EditMode="GridEditMode.PopupEditForm"
|
EditMode="GridEditMode.PopupEditForm"
|
||||||
@@ -81,58 +139,10 @@ else
|
|||||||
PopupEditFormHeaderText="@popupHeaderText"
|
PopupEditFormHeaderText="@popupHeaderText"
|
||||||
CustomizeEditModel="OnCustomizeEditModel"
|
CustomizeEditModel="OnCustomizeEditModel"
|
||||||
EditModelSaving="OnEditModelSaving"
|
EditModelSaving="OnEditModelSaving"
|
||||||
DataItemDeleting="OnDataItemDeleting">
|
DataItemDeleting="OnDataItemDeleting"
|
||||||
|
@ref="gridRef">
|
||||||
<Columns>
|
<Columns>
|
||||||
<DxGridCommandColumn Width="120px" />
|
@RenderColumns()
|
||||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.Guid)" Caption="Id" Width="140px" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatTitle)" Caption="Titel">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatString)" Caption="String">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
|
||||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
|
||||||
Width="100%" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
|
||||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
|
||||||
Width="100%" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
</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); }
|
||||||
@@ -171,7 +181,27 @@ else
|
|||||||
private string? infoMessage;
|
private string? infoMessage;
|
||||||
private EditContext? editContext;
|
private EditContext? editContext;
|
||||||
private ValidationMessageStore? validationMessageStore;
|
private ValidationMessageStore? validationMessageStore;
|
||||||
|
private IGrid? gridRef;
|
||||||
private string popupHeaderText = "Edit";
|
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<string, string> columnBandAssignments = new();
|
||||||
|
private List<BandOption> bandOptions = new();
|
||||||
|
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
||||||
|
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
private List<ColumnDefinition> 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<ProcedureOption> procedureOptions = new()
|
private readonly List<ProcedureOption> procedureOptions = new()
|
||||||
{
|
{
|
||||||
@@ -179,11 +209,28 @@ else
|
|||||||
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
|
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||||
|
|
||||||
|
private bool gridLayoutApplied;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
await EnsureLayoutUserAsync();
|
||||||
|
await LoadBandLayoutAsync();
|
||||||
await LoadCatalogs();
|
await LoadCatalogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
||||||
|
{
|
||||||
|
gridRef.LoadLayout(bandLayout.GridLayout);
|
||||||
|
gridLayoutApplied = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void SetEditContext(EditContext context)
|
private void SetEditContext(EditContext context)
|
||||||
{
|
{
|
||||||
if (editContext == context)
|
if (editContext == context)
|
||||||
@@ -223,6 +270,11 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetPopupHeaderText(bool isNew)
|
||||||
|
{
|
||||||
|
popupHeaderText = isNew ? "Neu" : "Edit";
|
||||||
|
}
|
||||||
|
|
||||||
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||||
{
|
{
|
||||||
popupHeaderText = e.IsNew ? "Neu" : "Edit";
|
popupHeaderText = e.IsNew ? "Neu" : "Edit";
|
||||||
@@ -385,9 +437,379 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetPopupHeaderText(bool isNew)
|
private async Task EnsureLayoutUserAsync()
|
||||||
{
|
{
|
||||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
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()
|
||||||
|
{
|
||||||
|
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 ResetBandLayoutAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
|
||||||
|
bandLayout = new BandLayout();
|
||||||
|
columnBandAssignments.Clear();
|
||||||
|
UpdateBandOptions();
|
||||||
|
infoMessage = "Band-Layout zurückgesetzt.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyColumnLayoutFromStorage()
|
||||||
|
{
|
||||||
|
foreach (var column in columnDefinitions)
|
||||||
|
{
|
||||||
|
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||||||
|
{
|
||||||
|
column.Width = width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyBandOrderingFromColumnOrder()
|
||||||
|
{
|
||||||
|
if (bandLayout.ColumnOrder.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bandById = bandLayout.Bands.ToDictionary(band => band.Id, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var orderedBandIds = new List<string>();
|
||||||
|
var orderedColumnsByBand = bandLayout.Bands.ToDictionary(
|
||||||
|
band => band.Id,
|
||||||
|
_ => new List<string>(),
|
||||||
|
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<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 RenderFragment? BuildFilterTemplate(ColumnDefinition column)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RenderFragment? BuildTextFilterTemplate()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RenderFragment? BuildDateFilterTemplate()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
@using System.Text.Json
|
||||||
|
@using Microsoft.AspNetCore.Components
|
||||||
|
@using Microsoft.AspNetCore.Components.Rendering
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using DevExpress.Blazor
|
||||||
@inject MassDataApiClient Api
|
@inject MassDataApiClient Api
|
||||||
|
@inject LayoutApiClient LayoutApi
|
||||||
|
@inject IJSRuntime JsRuntime
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.action-panel { margin-bottom: 16px; }
|
.action-panel { margin-bottom: 16px; }
|
||||||
@@ -10,6 +16,23 @@
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
margin-bottom: 16px;
|
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-grid .dxbl-grid-sort-asc,
|
.massdata-grid .dxbl-grid-sort-asc,
|
||||||
.massdata-grid .dxbl-grid-sort-desc {
|
.massdata-grid .dxbl-grid-sort-desc {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -54,6 +77,25 @@
|
|||||||
.massdata-edit-popup {
|
.massdata-edit-popup {
|
||||||
min-width: 720px;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||||
@@ -75,78 +117,67 @@ else if (items.Count == 0)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<div class="mb-3 page-size-selector">
|
||||||
|
<span class="page-size-label">Datensätze je Seite:</span>
|
||||||
|
<DxComboBox Data="@pageSizeOptions"
|
||||||
|
TData="PageSizeOption"
|
||||||
|
TValue="int?"
|
||||||
|
TextFieldName="Text"
|
||||||
|
ValueFieldName="Value"
|
||||||
|
Value="@pageSize"
|
||||||
|
ValueChanged="OnPageSizeChanged"
|
||||||
|
CssClass="page-size-combo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="band-editor">
|
||||||
|
<div class="band-controls">
|
||||||
|
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||||
|
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||||
|
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
||||||
|
</div>
|
||||||
|
@foreach (var band in bandLayout.Bands)
|
||||||
|
{
|
||||||
|
<div class="band-row">
|
||||||
|
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
|
||||||
|
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<DxFormLayout CssClass="band-columns" ColCount="2">
|
||||||
|
@foreach (var column in columnDefinitions)
|
||||||
|
{
|
||||||
|
<DxFormLayoutItem Caption="@column.Caption">
|
||||||
|
<DxComboBox Data="@bandOptions"
|
||||||
|
TData="BandOption"
|
||||||
|
TValue="string"
|
||||||
|
TextFieldName="Caption"
|
||||||
|
ValueFieldName="Id"
|
||||||
|
Value="@GetColumnBand(column.FieldName)"
|
||||||
|
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
|
||||||
|
Width="100%" />
|
||||||
|
</DxFormLayoutItem>
|
||||||
|
}
|
||||||
|
</DxFormLayout>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid-section">
|
<div class="grid-section">
|
||||||
<DxGrid Data="@items"
|
<DxGrid Data="@items"
|
||||||
TItem="MassDataReadDto"
|
TItem="MassDataReadDto"
|
||||||
KeyFieldName="@nameof(MassDataReadDto.Id)"
|
KeyFieldName="@nameof(MassDataReadDto.Id)"
|
||||||
ShowFilterRow="true"
|
ShowFilterRow="true"
|
||||||
ShowGroupPanel="true"
|
|
||||||
AllowColumnResize="true"
|
AllowColumnResize="true"
|
||||||
|
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||||
|
AllowColumnReorder="true"
|
||||||
PagerVisible="false"
|
PagerVisible="false"
|
||||||
PageSize="100"
|
PageSize="@(pageSize ?? 100)"
|
||||||
CssClass="mb-3 massdata-grid"
|
CssClass="mb-3 massdata-grid"
|
||||||
EditMode="GridEditMode.PopupEditForm"
|
EditMode="GridEditMode.PopupEditForm"
|
||||||
PopupEditFormCssClass="massdata-edit-popup"
|
|
||||||
PopupEditFormHeaderText="@popupHeaderText"
|
PopupEditFormHeaderText="@popupHeaderText"
|
||||||
CustomizeEditModel="OnCustomizeEditModel"
|
CustomizeEditModel="OnCustomizeEditModel"
|
||||||
EditModelSaving="OnEditModelSaving"
|
EditModelSaving="OnEditModelSaving"
|
||||||
DataItemDeleting="OnDataItemDeleting">
|
DataItemDeleting="OnDataItemDeleting"
|
||||||
|
@ref="gridRef">
|
||||||
<Columns>
|
<Columns>
|
||||||
<DxGridCommandColumn Width="120px" />
|
@RenderColumns()
|
||||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Id)" Caption="Id" Width="90px" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.CustomerName)" Caption="CustomerName">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Amount)" Caption="Amount" DisplayFormat="c2">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Category)" Caption="Category" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.StatusFlag)" Caption="Status" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxComboBox Data="@statusFilterOptions"
|
|
||||||
TData="BoolFilterOption"
|
|
||||||
TValue="bool?"
|
|
||||||
TextFieldName="Text"
|
|
||||||
ValueFieldName="Value"
|
|
||||||
Value="@(filter.FilterRowValue as bool?)"
|
|
||||||
ValueChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
Width="100%" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.AddedWhen)" Caption="Angelegt am" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
|
||||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
|
||||||
Width="100%" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.ChangedWhen)" Caption="Geändert am" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
|
||||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
|
||||||
Width="100%" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
</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); }
|
||||||
@@ -182,23 +213,57 @@ else
|
|||||||
</EditFormTemplate>
|
</EditFormTemplate>
|
||||||
</DxGrid>
|
</DxGrid>
|
||||||
|
|
||||||
|
@if (pageCount > 1)
|
||||||
|
{
|
||||||
<div class="pager-container">
|
<div class="pager-container">
|
||||||
<DxPager PageCount="@pageCount" ActivePageIndex="@pageIndex" ActivePageIndexChanged="OnPageChanged" />
|
<DxPager PageCount="@pageCount" ActivePageIndex="@pageIndex" ActivePageIndexChanged="OnPageChanged" />
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private const int PageSize = 100;
|
|
||||||
private List<MassDataReadDto> items = new();
|
private List<MassDataReadDto> items = new();
|
||||||
private bool isLoading;
|
private bool isLoading;
|
||||||
private string? errorMessage;
|
private string? errorMessage;
|
||||||
private string? infoMessage;
|
private string? infoMessage;
|
||||||
private int pageIndex;
|
private int pageIndex;
|
||||||
private int pageCount = 1;
|
private int pageCount = 1;
|
||||||
|
private int? pageSize = 100;
|
||||||
private string popupHeaderText = "Edit";
|
private string popupHeaderText = "Edit";
|
||||||
private EditContext? editContext;
|
private EditContext? editContext;
|
||||||
private ValidationMessageStore? validationMessageStore;
|
private ValidationMessageStore? validationMessageStore;
|
||||||
|
private IGrid? gridRef;
|
||||||
|
private const string LayoutType = "GRID_BANDS";
|
||||||
|
private const string LayoutKey = "MassDataGrid";
|
||||||
|
private const string LayoutUserStorageKey = "layoutUser";
|
||||||
|
private string? layoutUser;
|
||||||
|
private BandLayout bandLayout = new();
|
||||||
|
private Dictionary<string, string> columnBandAssignments = new();
|
||||||
|
private List<BandOption> bandOptions = new();
|
||||||
|
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
||||||
|
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
private List<ColumnDefinition> columnDefinitions = new()
|
||||||
|
{
|
||||||
|
new() { FieldName = nameof(MassDataReadDto.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||||
|
new() { FieldName = nameof(MassDataReadDto.CustomerName), Caption = "CustomerName", FilterType = ColumnFilterType.Text },
|
||||||
|
new() { FieldName = nameof(MassDataReadDto.Amount), Caption = "Amount", DisplayFormat = "c2", FilterType = ColumnFilterType.Text },
|
||||||
|
new() { FieldName = nameof(MassDataReadDto.Category), Caption = "Category", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||||
|
new() { FieldName = nameof(MassDataReadDto.StatusFlag), Caption = "Status", ReadOnly = true, FilterType = ColumnFilterType.Bool },
|
||||||
|
new() { FieldName = nameof(MassDataReadDto.AddedWhen), Caption = "Angelegt am", ReadOnly = true, FilterType = ColumnFilterType.Date },
|
||||||
|
new() { FieldName = nameof(MassDataReadDto.ChangedWhen), Caption = "Geändert am", ReadOnly = true, FilterType = ColumnFilterType.Date }
|
||||||
|
};
|
||||||
|
|
||||||
|
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||||
|
|
||||||
|
private readonly List<PageSizeOption> pageSizeOptions = new()
|
||||||
|
{
|
||||||
|
new() { Value = 100, Text = "100" },
|
||||||
|
new() { Value = 1000, Text = "1.000" },
|
||||||
|
new() { Value = 10000, Text = "10.000" },
|
||||||
|
new() { Value = 100000, Text = "100.000" },
|
||||||
|
new() { Value = null, Text = "Alle" }
|
||||||
|
};
|
||||||
|
|
||||||
private readonly List<BoolFilterOption> statusFilterOptions = new()
|
private readonly List<BoolFilterOption> statusFilterOptions = new()
|
||||||
{
|
{
|
||||||
@@ -214,6 +279,9 @@ else
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
await EnsureLayoutUserAsync();
|
||||||
|
await LoadBandLayoutAsync();
|
||||||
await LoadPage(0);
|
await LoadPage(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,11 +292,12 @@ else
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var total = await Api.GetCountAsync();
|
var total = await Api.GetCountAsync();
|
||||||
pageCount = Math.Max(1, (int)Math.Ceiling(total / (double)PageSize));
|
var effectivePageSize = pageSize ?? (total == 0 ? 1 : total);
|
||||||
|
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 = pageIndex * PageSize;
|
var skip = pageSize.HasValue ? pageIndex * pageSize.Value : 0;
|
||||||
items = await Api.GetAllAsync(skip, PageSize);
|
items = await Api.GetAllAsync(skip, pageSize);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -246,6 +315,292 @@ else
|
|||||||
await LoadPage(index);
|
await LoadPage(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OnPageSizeChanged(int? size)
|
||||||
|
{
|
||||||
|
pageSize = size;
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
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 ResetBandLayoutAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
|
||||||
|
bandLayout = new BandLayout();
|
||||||
|
columnBandAssignments.Clear();
|
||||||
|
UpdateBandOptions();
|
||||||
|
infoMessage = "Band-Layout zurückgesetzt.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyColumnLayoutFromStorage()
|
||||||
|
{
|
||||||
|
foreach (var column in columnDefinitions)
|
||||||
|
{
|
||||||
|
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||||||
|
{
|
||||||
|
column.Width = width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddBand()
|
||||||
|
{
|
||||||
|
bandLayout.Bands.Add(new BandDefinition
|
||||||
|
{
|
||||||
|
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 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 =>
|
||||||
|
{
|
||||||
|
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 void SetEditContext(EditContext context)
|
private void SetEditContext(EditContext context)
|
||||||
{
|
{
|
||||||
if (editContext == context)
|
if (editContext == context)
|
||||||
@@ -290,7 +645,7 @@ else
|
|||||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
popupHeaderText = isNew ? "Neu" : "Edit";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
private async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.IsNew)
|
if (e.IsNew)
|
||||||
{
|
{
|
||||||
@@ -382,6 +737,44 @@ else
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
@@ -405,4 +798,22 @@ else
|
|||||||
public bool? Value { get; set; }
|
public bool? Value { get; set; }
|
||||||
public string Text { get; set; } = string.Empty;
|
public string Text { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class PageSizeOption
|
||||||
|
{
|
||||||
|
public int? Value { get; set; }
|
||||||
|
public string Text { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool gridLayoutApplied;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
||||||
|
{
|
||||||
|
gridRef.LoadLayout(bandLayout.GridLayout);
|
||||||
|
gridLayoutApplied = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
DbFirst.BlazorWasm/Models/LayoutDto.cs
Normal file
9
DbFirst.BlazorWasm/Models/LayoutDto.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace DbFirst.BlazorWasm.Models;
|
||||||
|
|
||||||
|
public class LayoutDto
|
||||||
|
{
|
||||||
|
public string LayoutType { get; set; } = string.Empty;
|
||||||
|
public string LayoutKey { get; set; } = string.Empty;
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
public string LayoutData { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -19,5 +19,6 @@ builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiBaseU
|
|||||||
builder.Services.AddScoped<CatalogApiClient>();
|
builder.Services.AddScoped<CatalogApiClient>();
|
||||||
builder.Services.AddScoped<DashboardApiClient>();
|
builder.Services.AddScoped<DashboardApiClient>();
|
||||||
builder.Services.AddScoped<MassDataApiClient>();
|
builder.Services.AddScoped<MassDataApiClient>();
|
||||||
|
builder.Services.AddScoped<LayoutApiClient>();
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
await builder.Build().RunAsync();
|
||||||
|
|||||||
64
DbFirst.BlazorWasm/Services/LayoutApiClient.cs
Normal file
64
DbFirst.BlazorWasm/Services/LayoutApiClient.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using DbFirst.BlazorWasm.Models;
|
||||||
|
|
||||||
|
namespace DbFirst.BlazorWasm.Services;
|
||||||
|
|
||||||
|
public class LayoutApiClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private const string Endpoint = "api/layouts";
|
||||||
|
|
||||||
|
public LayoutApiClient(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LayoutDto?> GetAsync(string layoutType, string layoutKey, string userName)
|
||||||
|
{
|
||||||
|
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
|
||||||
|
var response = await _httpClient.GetAsync(url);
|
||||||
|
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return await response.Content.ReadFromJsonAsync<LayoutDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LayoutDto> UpsertAsync(LayoutDto dto)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var detail = await ReadErrorAsync(response);
|
||||||
|
throw new InvalidOperationException(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadFromJsonAsync<LayoutDto>();
|
||||||
|
return payload ?? dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
|
||||||
|
{
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
if (!string.IsNullOrWhiteSpace(body))
|
||||||
|
{
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{(int)response.StatusCode} {response.ReasonPhrase}".Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(string layoutType, string layoutKey, string userName)
|
||||||
|
{
|
||||||
|
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
|
||||||
|
var response = await _httpClient.DeleteAsync(url);
|
||||||
|
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,9 +19,20 @@ public class MassDataApiClient
|
|||||||
return result ?? 0;
|
return result ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<MassDataReadDto>> GetAllAsync(int skip, int take)
|
public async Task<List<MassDataReadDto>> GetAllAsync(int? skip, int? take)
|
||||||
{
|
{
|
||||||
var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>($"{Endpoint}?skip={skip}&take={take}");
|
var query = new List<string>();
|
||||||
|
if (skip.HasValue)
|
||||||
|
{
|
||||||
|
query.Add($"skip={skip.Value}");
|
||||||
|
}
|
||||||
|
if (take.HasValue)
|
||||||
|
{
|
||||||
|
query.Add($"take={take.Value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = query.Count == 0 ? Endpoint : $"{Endpoint}?{string.Join("&", query)}";
|
||||||
|
var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>(url);
|
||||||
return result ?? new List<MassDataReadDto>();
|
return result ?? new List<MassDataReadDto>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
@using System.Text.Json
|
||||||
|
@using Microsoft.AspNetCore.Components
|
||||||
|
@using Microsoft.AspNetCore.Components.Rendering
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using DevExpress.Blazor
|
||||||
@inject CatalogApiClient Api
|
@inject CatalogApiClient Api
|
||||||
|
@inject LayoutApiClient LayoutApi
|
||||||
|
@inject IJSRuntime JsRuntime
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.action-panel { margin-bottom: 16px; }
|
.action-panel { margin-bottom: 16px; }
|
||||||
@@ -7,6 +13,25 @@
|
|||||||
.catalog-edit-popup {
|
.catalog-edit-popup {
|
||||||
min-width: 720px;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||||
@@ -28,11 +53,44 @@ else if (items.Count == 0)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<div class="band-editor">
|
||||||
|
<div class="band-controls">
|
||||||
|
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||||
|
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||||
|
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
||||||
|
</div>
|
||||||
|
@foreach (var band in bandLayout.Bands)
|
||||||
|
{
|
||||||
|
<div class="band-row">
|
||||||
|
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
|
||||||
|
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<DxFormLayout CssClass="band-columns" ColCount="2">
|
||||||
|
@foreach (var column in columnDefinitions)
|
||||||
|
{
|
||||||
|
<DxFormLayoutItem Caption="@column.Caption">
|
||||||
|
<DxComboBox Data="@bandOptions"
|
||||||
|
TData="BandOption"
|
||||||
|
TValue="string"
|
||||||
|
TextFieldName="Caption"
|
||||||
|
ValueFieldName="Id"
|
||||||
|
Value="@GetColumnBand(column.FieldName)"
|
||||||
|
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
|
||||||
|
Width="100%" />
|
||||||
|
</DxFormLayoutItem>
|
||||||
|
}
|
||||||
|
</DxFormLayout>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid-section">
|
<div class="grid-section">
|
||||||
<DxGrid Data="@items"
|
<DxGrid Data="@items"
|
||||||
TItem="CatalogReadDto"
|
TItem="CatalogReadDto"
|
||||||
KeyFieldName="@nameof(CatalogReadDto.Guid)"
|
KeyFieldName="@nameof(CatalogReadDto.Guid)"
|
||||||
ShowFilterRow="true"
|
ShowFilterRow="true"
|
||||||
|
AllowColumnResize="true"
|
||||||
|
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||||
|
AllowColumnReorder="true"
|
||||||
PageSize="10"
|
PageSize="10"
|
||||||
CssClass="mb-4 catalog-grid"
|
CssClass="mb-4 catalog-grid"
|
||||||
EditMode="GridEditMode.PopupEditForm"
|
EditMode="GridEditMode.PopupEditForm"
|
||||||
@@ -40,58 +98,10 @@ else
|
|||||||
PopupEditFormHeaderText="@popupHeaderText"
|
PopupEditFormHeaderText="@popupHeaderText"
|
||||||
CustomizeEditModel="OnCustomizeEditModel"
|
CustomizeEditModel="OnCustomizeEditModel"
|
||||||
EditModelSaving="OnEditModelSaving"
|
EditModelSaving="OnEditModelSaving"
|
||||||
DataItemDeleting="OnDataItemDeleting">
|
DataItemDeleting="OnDataItemDeleting"
|
||||||
|
@ref="gridRef">
|
||||||
<Columns>
|
<Columns>
|
||||||
<DxGridCommandColumn Width="120px" />
|
@RenderColumns()
|
||||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.Guid)" Caption="Id" Width="140px" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatTitle)" Caption="Titel">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatString)" Caption="String">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
|
||||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
|
||||||
Width="100%" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
|
||||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
|
||||||
Width="100%" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
</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); }
|
||||||
@@ -130,7 +140,27 @@ else
|
|||||||
private string? infoMessage;
|
private string? infoMessage;
|
||||||
private EditContext? editContext;
|
private EditContext? editContext;
|
||||||
private ValidationMessageStore? validationMessageStore;
|
private ValidationMessageStore? validationMessageStore;
|
||||||
|
private IGrid? gridRef;
|
||||||
private string popupHeaderText = "Edit";
|
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<string, string> columnBandAssignments = new();
|
||||||
|
private List<BandOption> bandOptions = new();
|
||||||
|
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
||||||
|
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
private List<ColumnDefinition> 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<ProcedureOption> procedureOptions = new()
|
private readonly List<ProcedureOption> procedureOptions = new()
|
||||||
{
|
{
|
||||||
@@ -138,8 +168,13 @@ else
|
|||||||
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
|
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
await EnsureLayoutUserAsync();
|
||||||
|
await LoadBandLayoutAsync();
|
||||||
await LoadCatalogs();
|
await LoadCatalogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,6 +384,296 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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; }
|
||||||
@@ -364,4 +689,43 @@ 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 async Task ResetBandLayoutAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
|
||||||
|
bandLayout = new BandLayout();
|
||||||
|
columnBandAssignments.Clear();
|
||||||
|
UpdateBandOptions();
|
||||||
|
infoMessage = "Band-Layout zurückgesetzt.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyColumnLayoutFromStorage()
|
||||||
|
{
|
||||||
|
foreach (var column in columnDefinitions)
|
||||||
|
{
|
||||||
|
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||||||
|
{
|
||||||
|
column.Width = width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool gridLayoutApplied;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
||||||
|
{
|
||||||
|
gridRef.LoadLayout(bandLayout.GridLayout);
|
||||||
|
gridLayoutApplied = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
@using System.Text.Json
|
||||||
|
@using Microsoft.AspNetCore.Components
|
||||||
|
@using Microsoft.AspNetCore.Components.Rendering
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using DevExpress.Blazor
|
||||||
@inject MassDataApiClient Api
|
@inject MassDataApiClient Api
|
||||||
|
@inject LayoutApiClient LayoutApi
|
||||||
|
@inject IJSRuntime JsRuntime
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.action-panel { margin-bottom: 16px; }
|
.action-panel { margin-bottom: 16px; }
|
||||||
@@ -10,6 +16,23 @@
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
margin-bottom: 16px;
|
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-grid .dxbl-grid-sort-asc,
|
.massdata-grid .dxbl-grid-sort-asc,
|
||||||
.massdata-grid .dxbl-grid-sort-desc {
|
.massdata-grid .dxbl-grid-sort-desc {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -54,6 +77,25 @@
|
|||||||
.massdata-edit-popup {
|
.massdata-edit-popup {
|
||||||
min-width: 720px;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||||
@@ -75,78 +117,67 @@ else if (items.Count == 0)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<div class="mb-3 page-size-selector">
|
||||||
|
<span class="page-size-label">Datensätze je Seite:</span>
|
||||||
|
<DxComboBox Data="@pageSizeOptions"
|
||||||
|
TData="PageSizeOption"
|
||||||
|
TValue="int?"
|
||||||
|
TextFieldName="Text"
|
||||||
|
ValueFieldName="Value"
|
||||||
|
Value="@pageSize"
|
||||||
|
ValueChanged="OnPageSizeChanged"
|
||||||
|
CssClass="page-size-combo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="band-editor">
|
||||||
|
<div class="band-controls">
|
||||||
|
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||||
|
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||||
|
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
||||||
|
</div>
|
||||||
|
@foreach (var band in bandLayout.Bands)
|
||||||
|
{
|
||||||
|
<div class="band-row">
|
||||||
|
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
|
||||||
|
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<DxFormLayout CssClass="band-columns" ColCount="2">
|
||||||
|
@foreach (var column in columnDefinitions)
|
||||||
|
{
|
||||||
|
<DxFormLayoutItem Caption="@column.Caption">
|
||||||
|
<DxComboBox Data="@bandOptions"
|
||||||
|
TData="BandOption"
|
||||||
|
TValue="string"
|
||||||
|
TextFieldName="Caption"
|
||||||
|
ValueFieldName="Id"
|
||||||
|
Value="@GetColumnBand(column.FieldName)"
|
||||||
|
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
|
||||||
|
Width="100%" />
|
||||||
|
</DxFormLayoutItem>
|
||||||
|
}
|
||||||
|
</DxFormLayout>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid-section">
|
<div class="grid-section">
|
||||||
<DxGrid Data="@items"
|
<DxGrid Data="@items"
|
||||||
TItem="MassDataReadDto"
|
TItem="MassDataReadDto"
|
||||||
KeyFieldName="@nameof(MassDataReadDto.Id)"
|
KeyFieldName="@nameof(MassDataReadDto.Id)"
|
||||||
ShowFilterRow="true"
|
ShowFilterRow="true"
|
||||||
ShowGroupPanel="true"
|
|
||||||
AllowColumnResize="true"
|
AllowColumnResize="true"
|
||||||
|
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||||
|
AllowColumnReorder="true"
|
||||||
PagerVisible="false"
|
PagerVisible="false"
|
||||||
PageSize="100"
|
PageSize="@(pageSize ?? 100)"
|
||||||
CssClass="mb-3 massdata-grid"
|
CssClass="mb-3 massdata-grid"
|
||||||
EditMode="GridEditMode.PopupEditForm"
|
EditMode="GridEditMode.PopupEditForm"
|
||||||
PopupEditFormCssClass="massdata-edit-popup"
|
|
||||||
PopupEditFormHeaderText="@popupHeaderText"
|
PopupEditFormHeaderText="@popupHeaderText"
|
||||||
CustomizeEditModel="OnCustomizeEditModel"
|
CustomizeEditModel="OnCustomizeEditModel"
|
||||||
EditModelSaving="OnEditModelSaving"
|
EditModelSaving="OnEditModelSaving"
|
||||||
DataItemDeleting="OnDataItemDeleting">
|
DataItemDeleting="OnDataItemDeleting"
|
||||||
|
@ref="gridRef">
|
||||||
<Columns>
|
<Columns>
|
||||||
<DxGridCommandColumn Width="120px" />
|
@RenderColumns()
|
||||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Id)" Caption="Id" Width="90px" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.CustomerName)" Caption="CustomerName">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Amount)" Caption="Amount" DisplayFormat="c2">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Category)" Caption="Category" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
|
||||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
CssClass="filter-search-input" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.StatusFlag)" Caption="Status" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxComboBox Data="@statusFilterOptions"
|
|
||||||
TData="BoolFilterOption"
|
|
||||||
TValue="bool?"
|
|
||||||
TextFieldName="Text"
|
|
||||||
ValueFieldName="Value"
|
|
||||||
Value="@(filter.FilterRowValue as bool?)"
|
|
||||||
ValueChanged="@(value => filter.FilterRowValue = value)"
|
|
||||||
Width="100%" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.AddedWhen)" Caption="Added" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
|
||||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
|
||||||
Width="100%" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.ChangedWhen)" Caption="Changed" ReadOnly="true">
|
|
||||||
<FilterRowCellTemplate Context="filter">
|
|
||||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
|
||||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
|
||||||
Width="100%" />
|
|
||||||
</FilterRowCellTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
</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); }
|
||||||
@@ -182,23 +213,57 @@ else
|
|||||||
</EditFormTemplate>
|
</EditFormTemplate>
|
||||||
</DxGrid>
|
</DxGrid>
|
||||||
|
|
||||||
|
@if (pageCount > 1)
|
||||||
|
{
|
||||||
<div class="pager-container">
|
<div class="pager-container">
|
||||||
<DxPager PageCount="@pageCount" ActivePageIndex="@pageIndex" ActivePageIndexChanged="OnPageChanged" />
|
<DxPager PageCount="@pageCount" ActivePageIndex="@pageIndex" ActivePageIndexChanged="OnPageChanged" />
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private const int PageSize = 100;
|
|
||||||
private List<MassDataReadDto> items = new();
|
private List<MassDataReadDto> items = new();
|
||||||
private bool isLoading;
|
private bool isLoading;
|
||||||
private string? errorMessage;
|
private string? errorMessage;
|
||||||
private string? infoMessage;
|
private string? infoMessage;
|
||||||
private int pageIndex;
|
private int pageIndex;
|
||||||
private int pageCount = 1;
|
private int pageCount = 1;
|
||||||
|
private int? pageSize = 100;
|
||||||
private string popupHeaderText = "Edit";
|
private string popupHeaderText = "Edit";
|
||||||
private EditContext? editContext;
|
private EditContext? editContext;
|
||||||
private ValidationMessageStore? validationMessageStore;
|
private ValidationMessageStore? validationMessageStore;
|
||||||
|
private IGrid? gridRef;
|
||||||
|
private const string LayoutType = "GRID_BANDS";
|
||||||
|
private const string LayoutKey = "MassDataGrid";
|
||||||
|
private const string LayoutUserStorageKey = "layoutUser";
|
||||||
|
private string? layoutUser;
|
||||||
|
private BandLayout bandLayout = new();
|
||||||
|
private Dictionary<string, string> columnBandAssignments = new();
|
||||||
|
private List<BandOption> bandOptions = new();
|
||||||
|
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
||||||
|
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
private List<ColumnDefinition> columnDefinitions = new()
|
||||||
|
{
|
||||||
|
new() { FieldName = nameof(MassDataReadDto.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||||
|
new() { FieldName = nameof(MassDataReadDto.CustomerName), Caption = "CustomerName", FilterType = ColumnFilterType.Text },
|
||||||
|
new() { FieldName = nameof(MassDataReadDto.Amount), Caption = "Amount", DisplayFormat = "c2", FilterType = ColumnFilterType.Text },
|
||||||
|
new() { FieldName = nameof(MassDataReadDto.Category), Caption = "Category", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||||
|
new() { FieldName = nameof(MassDataReadDto.StatusFlag), Caption = "Status", ReadOnly = true, FilterType = ColumnFilterType.Bool },
|
||||||
|
new() { FieldName = nameof(MassDataReadDto.AddedWhen), Caption = "Added", 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()
|
||||||
|
{
|
||||||
|
new() { Value = 100, Text = "100" },
|
||||||
|
new() { Value = 1000, Text = "1.000" },
|
||||||
|
new() { Value = 10000, Text = "10.000" },
|
||||||
|
new() { Value = 100000, Text = "100.000" },
|
||||||
|
new() { Value = null, Text = "Alle" }
|
||||||
|
};
|
||||||
|
|
||||||
private readonly List<BoolFilterOption> statusFilterOptions = new()
|
private readonly List<BoolFilterOption> statusFilterOptions = new()
|
||||||
{
|
{
|
||||||
@@ -214,6 +279,9 @@ else
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
await EnsureLayoutUserAsync();
|
||||||
|
await LoadBandLayoutAsync();
|
||||||
await LoadPage(0);
|
await LoadPage(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,11 +292,12 @@ else
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var total = await Api.GetCountAsync();
|
var total = await Api.GetCountAsync();
|
||||||
pageCount = Math.Max(1, (int)Math.Ceiling(total / (double)PageSize));
|
var effectivePageSize = pageSize ?? (total == 0 ? 1 : total);
|
||||||
|
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 = pageIndex * PageSize;
|
var skip = pageSize.HasValue ? pageIndex * pageSize.Value : 0;
|
||||||
items = await Api.GetAllAsync(skip, PageSize);
|
items = await Api.GetAllAsync(skip, pageSize);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -246,6 +315,336 @@ else
|
|||||||
await LoadPage(index);
|
await LoadPage(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OnPageSizeChanged(int? size)
|
||||||
|
{
|
||||||
|
pageSize = size;
|
||||||
|
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();
|
||||||
|
//ApplyBandOrderingFromColumnOrder();
|
||||||
|
UpdateBandOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 ApplyBandOrderingFromColumnOrder()
|
||||||
|
{
|
||||||
|
if (bandLayout.ColumnOrder.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bandById = bandLayout.Bands.ToDictionary(band => band.Id, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var orderedBandIds = new List<string>();
|
||||||
|
var orderedColumnsByBand = bandLayout.Bands.ToDictionary(
|
||||||
|
band => band.Id,
|
||||||
|
_ => new List<string>(),
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var field in bandLayout.ColumnOrder)
|
||||||
|
{
|
||||||
|
if (columnBandAssignments.TryGetValue(field, out var bandId) && bandById.ContainsKey(bandId))
|
||||||
|
{
|
||||||
|
if (!orderedBandIds.Contains(bandId, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
orderedBandIds.Add(bandId);
|
||||||
|
}
|
||||||
|
|
||||||
|
orderedColumnsByBand[bandId].Add(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var band in bandLayout.Bands)
|
||||||
|
{
|
||||||
|
var orderedColumns = orderedColumnsByBand[band.Id];
|
||||||
|
orderedColumns.AddRange(band.Columns.Where(column => !orderedColumns.Contains(column, StringComparer.OrdinalIgnoreCase)));
|
||||||
|
band.Columns = orderedColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orderedBandIds.Count > 0)
|
||||||
|
{
|
||||||
|
bandLayout.Bands = orderedBandIds
|
||||||
|
.Select(id => bandById[id])
|
||||||
|
.Concat(bandLayout.Bands.Where(band => !orderedBandIds.Contains(band.Id, StringComparer.OrdinalIgnoreCase)))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyColumnLayoutFromStorage()
|
||||||
|
{
|
||||||
|
foreach (var column in columnDefinitions)
|
||||||
|
{
|
||||||
|
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 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 =>
|
||||||
|
{
|
||||||
|
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 void SetEditContext(EditContext context)
|
private void SetEditContext(EditContext context)
|
||||||
{
|
{
|
||||||
if (editContext == context)
|
if (editContext == context)
|
||||||
@@ -290,7 +689,7 @@ else
|
|||||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
popupHeaderText = isNew ? "Neu" : "Edit";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
private async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.IsNew)
|
if (e.IsNew)
|
||||||
{
|
{
|
||||||
@@ -382,6 +781,44 @@ else
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
@@ -405,4 +842,22 @@ else
|
|||||||
public bool? Value { get; set; }
|
public bool? Value { get; set; }
|
||||||
public string Text { get; set; } = string.Empty;
|
public string Text { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class PageSizeOption
|
||||||
|
{
|
||||||
|
public int? Value { get; set; }
|
||||||
|
public string Text { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool gridLayoutApplied;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
||||||
|
{
|
||||||
|
gridRef.LoadLayout(bandLayout.GridLayout);
|
||||||
|
gridLayoutApplied = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
DbFirst.BlazorWebApp/Models/LayoutDto.cs
Normal file
9
DbFirst.BlazorWebApp/Models/LayoutDto.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace DbFirst.BlazorWebApp.Models;
|
||||||
|
|
||||||
|
public class LayoutDto
|
||||||
|
{
|
||||||
|
public string LayoutType { get; set; } = string.Empty;
|
||||||
|
public string LayoutKey { get; set; } = string.Empty;
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
public string LayoutData { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -25,12 +25,17 @@ if (!string.IsNullOrWhiteSpace(apiBaseUrl))
|
|||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
});
|
||||||
|
builder.Services.AddHttpClient<LayoutApiClient>(client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
builder.Services.AddHttpClient<CatalogApiClient>();
|
builder.Services.AddHttpClient<CatalogApiClient>();
|
||||||
builder.Services.AddHttpClient<DashboardApiClient>();
|
builder.Services.AddHttpClient<DashboardApiClient>();
|
||||||
builder.Services.AddHttpClient<MassDataApiClient>();
|
builder.Services.AddHttpClient<MassDataApiClient>();
|
||||||
|
builder.Services.AddHttpClient<LayoutApiClient>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|||||||
64
DbFirst.BlazorWebApp/Services/LayoutApiClient.cs
Normal file
64
DbFirst.BlazorWebApp/Services/LayoutApiClient.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using DbFirst.BlazorWebApp.Models;
|
||||||
|
|
||||||
|
namespace DbFirst.BlazorWebApp.Services;
|
||||||
|
|
||||||
|
public class LayoutApiClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private const string Endpoint = "api/layouts";
|
||||||
|
|
||||||
|
public LayoutApiClient(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LayoutDto?> GetAsync(string layoutType, string layoutKey, string userName)
|
||||||
|
{
|
||||||
|
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
|
||||||
|
var response = await _httpClient.GetAsync(url);
|
||||||
|
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return await response.Content.ReadFromJsonAsync<LayoutDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LayoutDto> UpsertAsync(LayoutDto dto)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var detail = await ReadErrorAsync(response);
|
||||||
|
throw new InvalidOperationException(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadFromJsonAsync<LayoutDto>();
|
||||||
|
return payload ?? dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
|
||||||
|
{
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
if (!string.IsNullOrWhiteSpace(body))
|
||||||
|
{
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{(int)response.StatusCode} {response.ReasonPhrase}".Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(string layoutType, string layoutKey, string userName)
|
||||||
|
{
|
||||||
|
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
|
||||||
|
var response = await _httpClient.DeleteAsync(url);
|
||||||
|
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,9 +19,20 @@ public class MassDataApiClient
|
|||||||
return result ?? 0;
|
return result ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<MassDataReadDto>> GetAllAsync(int skip, int take)
|
public async Task<List<MassDataReadDto>> GetAllAsync(int? skip, int? take)
|
||||||
{
|
{
|
||||||
var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>($"{Endpoint}?skip={skip}&take={take}");
|
var query = new List<string>();
|
||||||
|
if (skip.HasValue)
|
||||||
|
{
|
||||||
|
query.Add($"skip={skip.Value}");
|
||||||
|
}
|
||||||
|
if (take.HasValue)
|
||||||
|
{
|
||||||
|
query.Add($"take={take.Value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = query.Count == 0 ? Endpoint : $"{Endpoint}?{string.Join("&", query)}";
|
||||||
|
var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>(url);
|
||||||
return result ?? new List<MassDataReadDto>();
|
return result ?? new List<MassDataReadDto>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
DbFirst.Domain/Entities/SmfLayout.cs
Normal file
15
DbFirst.Domain/Entities/SmfLayout.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace DbFirst.Domain.Entities;
|
||||||
|
|
||||||
|
public class SmfLayout
|
||||||
|
{
|
||||||
|
public long Guid { get; set; }
|
||||||
|
public bool Active { get; set; }
|
||||||
|
public string LayoutType { get; set; } = string.Empty;
|
||||||
|
public string LayoutKey { get; set; } = string.Empty;
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
public byte[] LayoutData { get; set; } = Array.Empty<byte>();
|
||||||
|
public string AddedWho { get; set; } = string.Empty;
|
||||||
|
public DateTime AddedWhen { get; set; }
|
||||||
|
public string? ChangedWho { get; set; }
|
||||||
|
public DateTime? ChangedWhen { get; set; }
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ public partial class ApplicationDbContext : DbContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
public virtual DbSet<VwmyCatalog> VwmyCatalogs { get; set; }
|
public virtual DbSet<VwmyCatalog> VwmyCatalogs { get; set; }
|
||||||
|
public virtual DbSet<SmfLayout> SmfLayouts { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -51,6 +52,37 @@ public partial class ApplicationDbContext : DbContext
|
|||||||
.HasColumnName(catCfg.ChangedWhoColumnName);
|
.HasColumnName(catCfg.ChangedWhoColumnName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<SmfLayout>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Guid);
|
||||||
|
entity.ToTable("TBDD_SMF_LAYOUT", tb => tb.HasTrigger("TBDD_SMF_LAYOUT_AFT_UPD"));
|
||||||
|
|
||||||
|
entity.Property(e => e.Guid).HasColumnName("GUID");
|
||||||
|
entity.Property(e => e.Active).HasColumnName("ACTIVE");
|
||||||
|
entity.Property(e => e.LayoutType)
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnName("LAYOUT_TYPE");
|
||||||
|
entity.Property(e => e.LayoutKey)
|
||||||
|
.HasMaxLength(150)
|
||||||
|
.HasColumnName("LAYOUT_KEY");
|
||||||
|
entity.Property(e => e.UserName)
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnName("USER_NAME");
|
||||||
|
entity.Property(e => e.LayoutData).HasColumnName("LAYOUT_DATA");
|
||||||
|
entity.Property(e => e.AddedWho)
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnName("ADDED_WHO");
|
||||||
|
entity.Property(e => e.AddedWhen)
|
||||||
|
.HasColumnType("datetime")
|
||||||
|
.HasColumnName("ADDED_WHEN");
|
||||||
|
entity.Property(e => e.ChangedWho)
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnName("CHANGED_WHO");
|
||||||
|
entity.Property(e => e.ChangedWhen)
|
||||||
|
.HasColumnType("datetime")
|
||||||
|
.HasColumnName("CHANGED_WHEN");
|
||||||
|
});
|
||||||
|
|
||||||
OnModelCreatingPartial(modelBuilder);
|
OnModelCreatingPartial(modelBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
66
DbFirst.Infrastructure/Repositories/LayoutRepository.cs
Normal file
66
DbFirst.Infrastructure/Repositories/LayoutRepository.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
using DbFirst.Application.Repositories;
|
||||||
|
using DbFirst.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace DbFirst.Infrastructure.Repositories;
|
||||||
|
|
||||||
|
public class LayoutRepository : ILayoutRepository
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _db;
|
||||||
|
|
||||||
|
public LayoutRepository(ApplicationDbContext db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SmfLayout?> GetAsync(string layoutType, string layoutKey, string userName, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _db.SmfLayouts.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.LayoutType == layoutType && x.LayoutKey == layoutKey && x.UserName == userName, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SmfLayout> UpsertAsync(string layoutType, string layoutKey, string userName, byte[] layoutData, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var entity = await _db.SmfLayouts
|
||||||
|
.FirstOrDefaultAsync(x => x.LayoutType == layoutType && x.LayoutKey == layoutKey && x.UserName == userName, cancellationToken);
|
||||||
|
|
||||||
|
if (entity == null)
|
||||||
|
{
|
||||||
|
entity = new SmfLayout
|
||||||
|
{
|
||||||
|
Active = true,
|
||||||
|
LayoutType = layoutType,
|
||||||
|
LayoutKey = layoutKey,
|
||||||
|
UserName = userName,
|
||||||
|
LayoutData = layoutData,
|
||||||
|
AddedWho = userName,
|
||||||
|
AddedWhen = DateTime.Now
|
||||||
|
};
|
||||||
|
_db.SmfLayouts.Add(entity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entity.Active = true;
|
||||||
|
entity.LayoutData = layoutData;
|
||||||
|
entity.ChangedWho = userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync(cancellationToken);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteAsync(string layoutType, string layoutKey, string userName, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var entity = await _db.SmfLayouts
|
||||||
|
.FirstOrDefaultAsync(x => x.LayoutType == layoutType && x.LayoutKey == layoutKey && x.UserName == userName, cancellationToken);
|
||||||
|
|
||||||
|
if (entity == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_db.SmfLayouts.Remove(entity);
|
||||||
|
await _db.SaveChangesAsync(cancellationToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user