Compare commits
2 Commits
7552b34ced
...
feat/timer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86feec930b | ||
|
|
f5224e20f2 |
28
DbFirst.API/Controllers/TimeController.cs
Normal file
28
DbFirst.API/Controllers/TimeController.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using DbFirst.Application.Time.Commands;
|
||||||
|
using DbFirst.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace DbFirst.API.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class TimeController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public TimeController(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<TimeRecord>> InsertAndGetLast(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new InsertTimeCommand(), cancellationToken);
|
||||||
|
if (result == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,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.AddScoped<ILayoutRepository, LayoutRepository>();
|
||||||
|
builder.Services.AddScoped<ITimeRepository, TimeRepository>();
|
||||||
|
|
||||||
builder.Services.AddDevExpressControls();
|
builder.Services.AddDevExpressControls();
|
||||||
builder.Services.AddSignalR();
|
builder.Services.AddSignalR();
|
||||||
|
|||||||
9
DbFirst.Application/Repositories/ITimeRepository.cs
Normal file
9
DbFirst.Application/Repositories/ITimeRepository.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using DbFirst.Domain.Entities;
|
||||||
|
|
||||||
|
namespace DbFirst.Application.Repositories;
|
||||||
|
|
||||||
|
public interface ITimeRepository
|
||||||
|
{
|
||||||
|
Task InsertAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<TimeRecord?> GetLastAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
6
DbFirst.Application/Time/Commands/InsertTimeCommand.cs
Normal file
6
DbFirst.Application/Time/Commands/InsertTimeCommand.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
using DbFirst.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace DbFirst.Application.Time.Commands;
|
||||||
|
|
||||||
|
public record InsertTimeCommand : IRequest<TimeRecord?>;
|
||||||
21
DbFirst.Application/Time/Commands/InsertTimeHandler.cs
Normal file
21
DbFirst.Application/Time/Commands/InsertTimeHandler.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using DbFirst.Application.Repositories;
|
||||||
|
using DbFirst.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace DbFirst.Application.Time.Commands;
|
||||||
|
|
||||||
|
public class InsertTimeHandler : IRequestHandler<InsertTimeCommand, TimeRecord?>
|
||||||
|
{
|
||||||
|
private readonly ITimeRepository _repository;
|
||||||
|
|
||||||
|
public InsertTimeHandler(ITimeRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TimeRecord?> Handle(InsertTimeCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _repository.InsertAsync(cancellationToken);
|
||||||
|
return await _repository.GetLastAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
using DbFirst.BlazorWebApp.Models.Grid;
|
|
||||||
using DbFirst.BlazorWebApp.Services;
|
|
||||||
using DevExpress.Blazor;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using Microsoft.AspNetCore.Components.Rendering;
|
|
||||||
|
|
||||||
namespace DbFirst.BlazorWebApp.Components;
|
|
||||||
|
|
||||||
public abstract class BandGridBase<TItem> : ComponentBase
|
|
||||||
{
|
|
||||||
[Inject] protected BandLayoutService BandLayoutService { get; set; } = default!;
|
|
||||||
|
|
||||||
// --- Abstract: jedes Grid definiert diese selbst ---
|
|
||||||
protected abstract string LayoutKey { get; }
|
|
||||||
protected abstract List<ColumnDefinition> ColumnDefinitions { get; }
|
|
||||||
|
|
||||||
// --- Band-Layout Felder ---
|
|
||||||
protected BandLayout bandLayout = new();
|
|
||||||
protected Dictionary<string, string> columnBandAssignments = new();
|
|
||||||
protected List<BandOption> bandOptions = new();
|
|
||||||
protected Dictionary<string, ColumnDefinition> columnLookup = new();
|
|
||||||
protected string? layoutUser;
|
|
||||||
protected bool gridLayoutApplied;
|
|
||||||
protected bool bandEditorExpanded;
|
|
||||||
protected IGrid? gridRef;
|
|
||||||
|
|
||||||
// --- SizeMode ---
|
|
||||||
protected SizeMode _sizeMode = SizeMode.Medium;
|
|
||||||
protected static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
|
|
||||||
|
|
||||||
private const string LayoutType = "GRID_BANDS";
|
|
||||||
|
|
||||||
// --- Lifecycle ---
|
|
||||||
protected async Task InitializeBandLayoutAsync()
|
|
||||||
{
|
|
||||||
columnLookup = ColumnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
|
||||||
layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
|
|
||||||
bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
|
|
||||||
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
|
|
||||||
ApplyColumnLayoutFromStorage();
|
|
||||||
_sizeMode = bandLayout.SizeMode;
|
|
||||||
UpdateBandOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task ApplyGridLayoutAfterRenderAsync()
|
|
||||||
{
|
|
||||||
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
|
||||||
{
|
|
||||||
gridRef.LoadLayout(bandLayout.GridLayout);
|
|
||||||
gridLayoutApplied = true;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Layout speichern / zurücksetzen ---
|
|
||||||
protected async Task SaveLayoutAsync()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser)) return;
|
|
||||||
CaptureColumnLayoutFromGrid();
|
|
||||||
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task ResetLayoutAsync()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser)) return;
|
|
||||||
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
|
|
||||||
bandLayout = new BandLayout();
|
|
||||||
columnBandAssignments.Clear();
|
|
||||||
UpdateBandOptions();
|
|
||||||
foreach (var column in ColumnDefinitions)
|
|
||||||
column.Width = null;
|
|
||||||
columnLookup = ColumnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
|
||||||
_sizeMode = SizeMode.Medium;
|
|
||||||
gridRef?.LoadLayout(new GridPersistentLayout());
|
|
||||||
gridLayoutApplied = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CaptureColumnLayoutFromGrid()
|
|
||||||
{
|
|
||||||
if (gridRef == null) return;
|
|
||||||
var layout = gridRef.SaveLayout();
|
|
||||||
bandLayout.GridLayout = layout;
|
|
||||||
bandLayout.SizeMode = _sizeMode;
|
|
||||||
var orderedColumns = layout.Columns
|
|
||||||
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
|
|
||||||
.OrderBy(c => c.VisibleIndex)
|
|
||||||
.ToList();
|
|
||||||
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
|
|
||||||
bandLayout.ColumnWidths = orderedColumns
|
|
||||||
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
|
|
||||||
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyColumnLayoutFromStorage()
|
|
||||||
{
|
|
||||||
foreach (var column in ColumnDefinitions)
|
|
||||||
{
|
|
||||||
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
|
||||||
column.Width = width;
|
|
||||||
}
|
|
||||||
columnLookup = ColumnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Band-Methoden ---
|
|
||||||
protected bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
|
||||||
|
|
||||||
protected void AddBand()
|
|
||||||
{
|
|
||||||
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
|
|
||||||
UpdateBandOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void RemoveBand(BandDefinition band)
|
|
||||||
{
|
|
||||||
bandLayout.Bands.Remove(band);
|
|
||||||
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
|
|
||||||
columnBandAssignments.Remove(key);
|
|
||||||
UpdateBandOptions();
|
|
||||||
SyncBandsFromAssignments();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void UpdateBandCaption(BandDefinition band, string value)
|
|
||||||
{
|
|
||||||
band.Caption = value;
|
|
||||||
UpdateBandOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void UpdateColumnBand(string fieldName, string? bandId)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(bandId))
|
|
||||||
columnBandAssignments.Remove(fieldName);
|
|
||||||
else
|
|
||||||
columnBandAssignments[fieldName] = bandId;
|
|
||||||
SyncBandsFromAssignments();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected string GetColumnBand(string fieldName)
|
|
||||||
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
|
||||||
|
|
||||||
protected void SyncBandsFromAssignments()
|
|
||||||
{
|
|
||||||
foreach (var band in bandLayout.Bands)
|
|
||||||
{
|
|
||||||
band.Columns = ColumnDefinitions
|
|
||||||
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
|
|
||||||
.Select(c => c.FieldName)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void UpdateBandOptions()
|
|
||||||
{
|
|
||||||
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
|
||||||
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- SizeMode ---
|
|
||||||
protected string FormatSizeText(SizeMode size) => size switch
|
|
||||||
{
|
|
||||||
SizeMode.Small => "Klein",
|
|
||||||
SizeMode.Medium => "Mittel",
|
|
||||||
SizeMode.Large => "Groß",
|
|
||||||
_ => size.ToString()
|
|
||||||
};
|
|
||||||
|
|
||||||
protected void OnSizeChange(DropDownButtonItemClickEventArgs args)
|
|
||||||
{
|
|
||||||
_sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- RenderColumns / BuildDataColumn ---
|
|
||||||
protected RenderFragment RenderColumns() => builder =>
|
|
||||||
{
|
|
||||||
var seq = 0;
|
|
||||||
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
|
||||||
builder.AddAttribute(seq++, "Width", "120px");
|
|
||||||
builder.CloseComponent();
|
|
||||||
|
|
||||||
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var column in ColumnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
|
|
||||||
BuildDataColumn(builder, ref seq, column);
|
|
||||||
|
|
||||||
foreach (var band in bandLayout.Bands)
|
|
||||||
{
|
|
||||||
if (band.Columns.Count == 0) continue;
|
|
||||||
builder.OpenComponent<DxGridBandColumn>(seq++);
|
|
||||||
builder.AddAttribute(seq++, "Caption", band.Caption);
|
|
||||||
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
|
||||||
{
|
|
||||||
var bandSeq = 0;
|
|
||||||
foreach (var columnName in band.Columns)
|
|
||||||
{
|
|
||||||
if (columnLookup.TryGetValue(columnName, out var column))
|
|
||||||
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
builder.CloseComponent();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
@inherits BandGridBase<CatalogReadDto>
|
|
||||||
@inject CatalogApiClient Api
|
@inject CatalogApiClient Api
|
||||||
|
@inject BandLayoutService BandLayoutService
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||||
{
|
{
|
||||||
@@ -34,8 +34,8 @@ else
|
|||||||
<div class="band-editor-body">
|
<div class="band-editor-body">
|
||||||
<div class="band-controls">
|
<div class="band-controls">
|
||||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||||
<DxButton Text="Layout speichern" Click="SaveLayoutWithFeedbackAsync" Enabled="@CanSaveBandLayout" />
|
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||||
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutWithFeedbackAsync" />
|
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
|
||||||
</div>
|
</div>
|
||||||
@foreach (var band in bandLayout.Bands)
|
@foreach (var band in bandLayout.Bands)
|
||||||
{
|
{
|
||||||
@@ -45,7 +45,7 @@ else
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<DxFormLayout CssClass="band-columns" ColCount="2">
|
<DxFormLayout CssClass="band-columns" ColCount="2">
|
||||||
@foreach (var column in ColumnDefinitions)
|
@foreach (var column in columnDefinitions)
|
||||||
{
|
{
|
||||||
<DxFormLayoutItem Caption="@column.Caption">
|
<DxFormLayoutItem Caption="@column.Caption">
|
||||||
<DxComboBox Data="@bandOptions"
|
<DxComboBox Data="@bandOptions"
|
||||||
@@ -148,21 +148,29 @@ else
|
|||||||
private string? infoMessage;
|
private string? infoMessage;
|
||||||
private EditContext? editContext;
|
private EditContext? editContext;
|
||||||
private ValidationMessageStore? validationMessageStore;
|
private ValidationMessageStore? validationMessageStore;
|
||||||
|
private IGrid? gridRef;
|
||||||
private int? focusedRowKey;
|
private int? focusedRowKey;
|
||||||
private string popupHeaderText = "Edit";
|
private string popupHeaderText = "Edit";
|
||||||
|
private const string LayoutType = "GRID_BANDS";
|
||||||
|
private const string LayoutKey = "CatalogsGrid";
|
||||||
|
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 bool gridLayoutApplied;
|
||||||
|
private bool bandEditorExpanded;
|
||||||
|
|
||||||
protected override string LayoutKey => "CatalogsGrid";
|
private List<ColumnDefinition> columnDefinitions = new()
|
||||||
|
{
|
||||||
protected override List<ColumnDefinition> ColumnDefinitions { get; } = 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.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text },
|
new() { FieldName = nameof(CatalogReadDto.CatString), Caption = "String", FilterType = ColumnFilterType.Text },
|
||||||
new() { FieldName = nameof(CatalogReadDto.CatTitle), Caption = "Titel", FilterType = ColumnFilterType.Text },
|
new() { FieldName = nameof(CatalogReadDto.AddedWho), Caption = "Angelegt von", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||||
new() { FieldName = nameof(CatalogReadDto.CatString), Caption = "String", FilterType = ColumnFilterType.Text },
|
new() { FieldName = nameof(CatalogReadDto.AddedWhen), Caption = "Angelegt am", ReadOnly = true, FilterType = ColumnFilterType.Date },
|
||||||
new() { FieldName = nameof(CatalogReadDto.AddedWho), Caption = "Angelegt von", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
new() { FieldName = nameof(CatalogReadDto.ChangedWho), Caption = "Geändert von", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||||
new() { FieldName = nameof(CatalogReadDto.AddedWhen), Caption = "Angelegt am", ReadOnly = true, FilterType = ColumnFilterType.Date },
|
new() { FieldName = nameof(CatalogReadDto.ChangedWhen), Caption = "Geändert 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()
|
||||||
{
|
{
|
||||||
@@ -170,15 +178,44 @@ else
|
|||||||
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
|
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||||
|
|
||||||
|
private SizeMode _sizeMode = SizeMode.Medium;
|
||||||
|
private static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
|
||||||
|
|
||||||
|
private string FormatSizeText(SizeMode size) => size switch
|
||||||
|
{
|
||||||
|
SizeMode.Small => "Klein",
|
||||||
|
SizeMode.Medium => "Mittel",
|
||||||
|
SizeMode.Large => "Groß",
|
||||||
|
_ => size.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
private void OnSizeChange(DropDownButtonItemClickEventArgs args)
|
||||||
|
{
|
||||||
|
_sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
await InitializeBandLayoutAsync();
|
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
|
||||||
|
bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
|
||||||
|
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
|
||||||
|
ApplyColumnLayoutFromStorage();
|
||||||
|
_sizeMode = bandLayout.SizeMode;
|
||||||
|
UpdateBandOptions();
|
||||||
await LoadCatalogs();
|
await LoadCatalogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
await ApplyGridLayoutAfterRenderAsync();
|
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
||||||
|
{
|
||||||
|
gridRef.LoadLayout(bandLayout.GridLayout);
|
||||||
|
gridLayoutApplied = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadCatalogs()
|
private async Task LoadCatalogs()
|
||||||
@@ -201,6 +238,174 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SaveLayoutAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CaptureColumnLayoutFromGrid();
|
||||||
|
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
|
||||||
|
infoMessage = "Layout gespeichert.";
|
||||||
|
errorMessage = null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ResetLayoutAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
|
||||||
|
|
||||||
|
bandLayout = new BandLayout();
|
||||||
|
columnBandAssignments.Clear();
|
||||||
|
UpdateBandOptions();
|
||||||
|
|
||||||
|
foreach (var column in columnDefinitions)
|
||||||
|
column.Width = null;
|
||||||
|
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
_sizeMode = SizeMode.Medium;
|
||||||
|
|
||||||
|
if (gridRef != null)
|
||||||
|
gridRef.LoadLayout(new GridPersistentLayout());
|
||||||
|
gridLayoutApplied = false;
|
||||||
|
|
||||||
|
infoMessage = "Layout zurückgesetzt.";
|
||||||
|
errorMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CaptureColumnLayoutFromGrid()
|
||||||
|
{
|
||||||
|
if (gridRef == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var layout = gridRef.SaveLayout();
|
||||||
|
bandLayout.GridLayout = layout;
|
||||||
|
bandLayout.SizeMode = _sizeMode;
|
||||||
|
|
||||||
|
var orderedColumns = layout.Columns
|
||||||
|
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
|
||||||
|
.OrderBy(c => c.VisibleIndex)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
|
||||||
|
bandLayout.ColumnWidths = orderedColumns
|
||||||
|
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
|
||||||
|
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyColumnLayoutFromStorage()
|
||||||
|
{
|
||||||
|
foreach (var column in columnDefinitions)
|
||||||
|
{
|
||||||
|
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||||||
|
column.Width = width;
|
||||||
|
}
|
||||||
|
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddBand()
|
||||||
|
{
|
||||||
|
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
|
||||||
|
UpdateBandOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveBand(BandDefinition band)
|
||||||
|
{
|
||||||
|
bandLayout.Bands.Remove(band);
|
||||||
|
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
|
||||||
|
columnBandAssignments.Remove(key);
|
||||||
|
UpdateBandOptions();
|
||||||
|
SyncBandsFromAssignments();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateBandCaption(BandDefinition band, string value)
|
||||||
|
{
|
||||||
|
band.Caption = value;
|
||||||
|
UpdateBandOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateColumnBand(string fieldName, string? bandId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(bandId))
|
||||||
|
columnBandAssignments.Remove(fieldName);
|
||||||
|
else
|
||||||
|
columnBandAssignments[fieldName] = bandId;
|
||||||
|
SyncBandsFromAssignments();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetColumnBand(string fieldName)
|
||||||
|
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
||||||
|
|
||||||
|
private void SyncBandsFromAssignments()
|
||||||
|
{
|
||||||
|
foreach (var band in bandLayout.Bands)
|
||||||
|
{
|
||||||
|
band.Columns = columnDefinitions
|
||||||
|
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
|
||||||
|
.Select(c => c.FieldName)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateBandOptions()
|
||||||
|
{
|
||||||
|
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
||||||
|
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RenderFragment RenderColumns() => builder =>
|
||||||
|
{
|
||||||
|
var seq = 0;
|
||||||
|
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
||||||
|
builder.AddAttribute(seq++, "Width", "120px");
|
||||||
|
builder.CloseComponent();
|
||||||
|
|
||||||
|
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
|
||||||
|
BuildDataColumn(builder, ref seq, column);
|
||||||
|
|
||||||
|
foreach (var band in bandLayout.Bands)
|
||||||
|
{
|
||||||
|
if (band.Columns.Count == 0) continue;
|
||||||
|
|
||||||
|
builder.OpenComponent<DxGridBandColumn>(seq++);
|
||||||
|
builder.AddAttribute(seq++, "Caption", band.Caption);
|
||||||
|
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
||||||
|
{
|
||||||
|
var bandSeq = 0;
|
||||||
|
foreach (var columnName in band.Columns)
|
||||||
|
{
|
||||||
|
if (columnLookup.TryGetValue(columnName, out var column))
|
||||||
|
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
builder.CloseComponent();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
|
||||||
|
{
|
||||||
|
builder.OpenComponent<DxGridDataColumn>(seq++);
|
||||||
|
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
||||||
|
builder.AddAttribute(seq++, "Caption", column.Caption);
|
||||||
|
if (!string.IsNullOrWhiteSpace(column.Width))
|
||||||
|
builder.AddAttribute(seq++, "Width", column.Width);
|
||||||
|
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
||||||
|
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
||||||
|
if (column.ReadOnly)
|
||||||
|
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||||
|
builder.CloseComponent();
|
||||||
|
}
|
||||||
|
|
||||||
private void SetEditContext(EditContext context)
|
private void SetEditContext(EditContext context)
|
||||||
{
|
{
|
||||||
if (editContext == context) return;
|
if (editContext == context) return;
|
||||||
@@ -370,25 +575,4 @@ else
|
|||||||
public int Value { get; set; }
|
public int Value { get; set; }
|
||||||
public string Text { get; set; } = string.Empty;
|
public string Text { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveLayoutWithFeedbackAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await SaveLayoutAsync();
|
|
||||||
infoMessage = "Layout gespeichert.";
|
|
||||||
errorMessage = null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ResetLayoutWithFeedbackAsync()
|
|
||||||
{
|
|
||||||
await ResetLayoutAsync();
|
|
||||||
infoMessage = "Layout zurückgesetzt.";
|
|
||||||
errorMessage = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,12 @@
|
|||||||
<span class="bi bi-table-nav-menu" aria-hidden="true"></span> MassData
|
<span class="bi bi-table-nav-menu" aria-hidden="true"></span> MassData
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="clock">
|
||||||
|
<span class="bi bi-clock-nav-menu" aria-hidden="true"></span> Clock
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@inherits BandGridBase<MassDataReadDto>
|
|
||||||
@inject MassDataApiClient Api
|
@inject MassDataApiClient Api
|
||||||
|
@inject BandLayoutService BandLayoutService
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||||
{
|
{
|
||||||
@@ -35,8 +35,8 @@ else
|
|||||||
<div class="band-editor-body">
|
<div class="band-editor-body">
|
||||||
<div class="band-controls">
|
<div class="band-controls">
|
||||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||||
<DxButton Text="Layout speichern" Click="SaveLayoutWithFeedbackAsync" Enabled="@CanSaveBandLayout" />
|
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||||
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutWithFeedbackAsync" />
|
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
|
||||||
</div>
|
</div>
|
||||||
@foreach (var band in bandLayout.Bands)
|
@foreach (var band in bandLayout.Bands)
|
||||||
{
|
{
|
||||||
@@ -46,7 +46,7 @@ else
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<DxFormLayout CssClass="band-columns" ColCount="2">
|
<DxFormLayout CssClass="band-columns" ColCount="2">
|
||||||
@foreach (var column in ColumnDefinitions)
|
@foreach (var column in columnDefinitions)
|
||||||
{
|
{
|
||||||
<DxFormLayoutItem Caption="@column.Caption">
|
<DxFormLayoutItem Caption="@column.Caption">
|
||||||
<DxComboBox Data="@bandOptions"
|
<DxComboBox Data="@bandOptions"
|
||||||
@@ -178,20 +178,28 @@ else
|
|||||||
private string popupHeaderText = "Edit";
|
private string popupHeaderText = "Edit";
|
||||||
private EditContext? editContext;
|
private EditContext? editContext;
|
||||||
private ValidationMessageStore? validationMessageStore;
|
private ValidationMessageStore? validationMessageStore;
|
||||||
|
private IGrid? gridRef;
|
||||||
private int? focusedRowKey;
|
private int? focusedRowKey;
|
||||||
|
private const string LayoutType = "GRID_BANDS";
|
||||||
|
private const string LayoutKey = "MassDataGrid";
|
||||||
|
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 bool gridLayoutApplied;
|
||||||
|
private bool bandEditorExpanded;
|
||||||
|
|
||||||
protected override string LayoutKey => "MassDataGrid";
|
private List<ColumnDefinition> columnDefinitions = new()
|
||||||
|
{
|
||||||
protected override List<ColumnDefinition> ColumnDefinitions { get; } = 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.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
new() { FieldName = nameof(MassDataReadDto.Amount), Caption = "Amount", DisplayFormat = "c2", FilterType = ColumnFilterType.Text },
|
||||||
new() { FieldName = nameof(MassDataReadDto.CustomerName), Caption = "CustomerName", FilterType = ColumnFilterType.Text },
|
new() { FieldName = nameof(MassDataReadDto.Category), Caption = "Category", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||||
new() { FieldName = nameof(MassDataReadDto.Amount), Caption = "Amount", DisplayFormat = "c2", FilterType = ColumnFilterType.Text },
|
new() { FieldName = nameof(MassDataReadDto.StatusFlag), Caption = "Status", ReadOnly = true, FilterType = ColumnFilterType.Bool },
|
||||||
new() { FieldName = nameof(MassDataReadDto.Category), Caption = "Category", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
new() { FieldName = nameof(MassDataReadDto.AddedWhen), Caption = "Added", ReadOnly = true, FilterType = ColumnFilterType.Date },
|
||||||
new() { FieldName = nameof(MassDataReadDto.StatusFlag), Caption = "Status", ReadOnly = true, FilterType = ColumnFilterType.Bool },
|
new() { FieldName = nameof(MassDataReadDto.ChangedWhen), Caption = "Changed", ReadOnly = true, FilterType = ColumnFilterType.Date }
|
||||||
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 readonly List<PageSizeOption> pageSizeOptions = new()
|
private readonly List<PageSizeOption> pageSizeOptions = new()
|
||||||
{
|
{
|
||||||
@@ -207,15 +215,44 @@ else
|
|||||||
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
|
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||||
|
|
||||||
|
private SizeMode _sizeMode = SizeMode.Medium;
|
||||||
|
private static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
|
||||||
|
|
||||||
|
private string FormatSizeText(SizeMode size) => size switch
|
||||||
|
{
|
||||||
|
SizeMode.Small => "Klein",
|
||||||
|
SizeMode.Medium => "Mittel",
|
||||||
|
SizeMode.Large => "Groß",
|
||||||
|
_ => size.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
private void OnSizeChange(DropDownButtonItemClickEventArgs args)
|
||||||
|
{
|
||||||
|
_sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
await InitializeBandLayoutAsync();
|
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
|
||||||
|
bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
|
||||||
|
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
|
||||||
|
ApplyColumnLayoutFromStorage();
|
||||||
|
_sizeMode = bandLayout.SizeMode;
|
||||||
|
UpdateBandOptions();
|
||||||
await LoadPage(0);
|
await LoadPage(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
await ApplyGridLayoutAfterRenderAsync();
|
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
||||||
|
{
|
||||||
|
gridRef.LoadLayout(bandLayout.GridLayout);
|
||||||
|
gridLayoutApplied = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadPage(int page)
|
private async Task LoadPage(int page)
|
||||||
@@ -251,6 +288,160 @@ else
|
|||||||
await LoadPage(0);
|
await LoadPage(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SaveLayoutAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CaptureColumnLayoutFromGrid();
|
||||||
|
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
|
||||||
|
infoMessage = "Layout gespeichert.";
|
||||||
|
errorMessage = null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ResetLayoutAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||||
|
return;
|
||||||
|
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
|
||||||
|
bandLayout = new BandLayout();
|
||||||
|
columnBandAssignments.Clear();
|
||||||
|
UpdateBandOptions();
|
||||||
|
foreach (var column in columnDefinitions)
|
||||||
|
column.Width = null;
|
||||||
|
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
_sizeMode = SizeMode.Medium;
|
||||||
|
if (gridRef != null)
|
||||||
|
gridRef.LoadLayout(new GridPersistentLayout());
|
||||||
|
gridLayoutApplied = false;
|
||||||
|
infoMessage = "Layout zurückgesetzt.";
|
||||||
|
errorMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CaptureColumnLayoutFromGrid()
|
||||||
|
{
|
||||||
|
if (gridRef == null) return;
|
||||||
|
var layout = gridRef.SaveLayout();
|
||||||
|
bandLayout.GridLayout = layout;
|
||||||
|
bandLayout.SizeMode = _sizeMode;
|
||||||
|
var orderedColumns = layout.Columns
|
||||||
|
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
|
||||||
|
.OrderBy(c => c.VisibleIndex)
|
||||||
|
.ToList();
|
||||||
|
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
|
||||||
|
bandLayout.ColumnWidths = orderedColumns
|
||||||
|
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
|
||||||
|
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyColumnLayoutFromStorage()
|
||||||
|
{
|
||||||
|
foreach (var column in columnDefinitions)
|
||||||
|
{
|
||||||
|
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||||||
|
column.Width = width;
|
||||||
|
}
|
||||||
|
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddBand()
|
||||||
|
{
|
||||||
|
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
|
||||||
|
UpdateBandOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveBand(BandDefinition band)
|
||||||
|
{
|
||||||
|
bandLayout.Bands.Remove(band);
|
||||||
|
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
|
||||||
|
columnBandAssignments.Remove(key);
|
||||||
|
UpdateBandOptions();
|
||||||
|
SyncBandsFromAssignments();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateBandCaption(BandDefinition band, string value)
|
||||||
|
{
|
||||||
|
band.Caption = value;
|
||||||
|
UpdateBandOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateColumnBand(string fieldName, string? bandId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(bandId))
|
||||||
|
columnBandAssignments.Remove(fieldName);
|
||||||
|
else
|
||||||
|
columnBandAssignments[fieldName] = bandId;
|
||||||
|
SyncBandsFromAssignments();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetColumnBand(string fieldName)
|
||||||
|
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
||||||
|
|
||||||
|
private void SyncBandsFromAssignments()
|
||||||
|
{
|
||||||
|
foreach (var band in bandLayout.Bands)
|
||||||
|
{
|
||||||
|
band.Columns = columnDefinitions
|
||||||
|
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
|
||||||
|
.Select(c => c.FieldName)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateBandOptions()
|
||||||
|
{
|
||||||
|
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
||||||
|
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RenderFragment RenderColumns() => builder =>
|
||||||
|
{
|
||||||
|
var seq = 0;
|
||||||
|
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
||||||
|
builder.AddAttribute(seq++, "Width", "120px");
|
||||||
|
builder.CloseComponent();
|
||||||
|
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
|
||||||
|
BuildDataColumn(builder, ref seq, column);
|
||||||
|
foreach (var band in bandLayout.Bands)
|
||||||
|
{
|
||||||
|
if (band.Columns.Count == 0) continue;
|
||||||
|
builder.OpenComponent<DxGridBandColumn>(seq++);
|
||||||
|
builder.AddAttribute(seq++, "Caption", band.Caption);
|
||||||
|
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
||||||
|
{
|
||||||
|
var bandSeq = 0;
|
||||||
|
foreach (var columnName in band.Columns)
|
||||||
|
{
|
||||||
|
if (columnLookup.TryGetValue(columnName, out var column))
|
||||||
|
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
builder.CloseComponent();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
|
||||||
|
{
|
||||||
|
builder.OpenComponent<DxGridDataColumn>(seq++);
|
||||||
|
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
||||||
|
builder.AddAttribute(seq++, "Caption", column.Caption);
|
||||||
|
if (!string.IsNullOrWhiteSpace(column.Width))
|
||||||
|
builder.AddAttribute(seq++, "Width", column.Width);
|
||||||
|
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
||||||
|
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
||||||
|
if (column.ReadOnly)
|
||||||
|
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||||
|
builder.CloseComponent();
|
||||||
|
}
|
||||||
|
|
||||||
private void SetEditContext(EditContext context)
|
private void SetEditContext(EditContext context)
|
||||||
{
|
{
|
||||||
if (editContext == context) return;
|
if (editContext == context) return;
|
||||||
@@ -384,25 +575,4 @@ else
|
|||||||
public int? Value { get; set; }
|
public int? Value { get; set; }
|
||||||
public string Text { get; set; } = string.Empty;
|
public string Text { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveLayoutWithFeedbackAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await SaveLayoutAsync();
|
|
||||||
infoMessage = "Layout gespeichert.";
|
|
||||||
errorMessage = null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ResetLayoutWithFeedbackAsync()
|
|
||||||
{
|
|
||||||
await ResetLayoutAsync();
|
|
||||||
infoMessage = "Layout zurückgesetzt.";
|
|
||||||
errorMessage = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
100
DbFirst.BlazorWebApp/Components/Pages/Clock.razor
Normal file
100
DbFirst.BlazorWebApp/Components/Pages/Clock.razor
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
@rendermode InteractiveServer
|
||||||
|
@page "/clock"
|
||||||
|
@inject TimeApiClient TimeApi
|
||||||
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
|
<PageTitle>Clock</PageTitle>
|
||||||
|
|
||||||
|
<h3>DB Server Clock</h3>
|
||||||
|
|
||||||
|
<div class="clock-wrapper">
|
||||||
|
<div class="clock-display @(_error != null ? "clock-error" : "")">
|
||||||
|
@if (_dbTime.HasValue)
|
||||||
|
{
|
||||||
|
<span class="clock-time">@_dbTime.Value.ToString("HH:mm:ss")</span>
|
||||||
|
<span class="clock-date">@_dbTime.Value.ToString("dd.MM.yyyy")</span>
|
||||||
|
}
|
||||||
|
else if (_error != null)
|
||||||
|
{
|
||||||
|
<span class="clock-time">--:--:--</span>
|
||||||
|
<span class="clock-date text-danger">@_error</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="clock-time">...</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.clock-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 40vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-display {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bs-body-bg, #1e1e2e);
|
||||||
|
border: 2px solid var(--bs-border-color, #444);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2rem 4rem;
|
||||||
|
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-time {
|
||||||
|
font-size: 5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--bs-primary, #0d6efd);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-date {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-error .clock-time {
|
||||||
|
color: var(--bs-danger, #dc3545);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private DateTime? _dbTime;
|
||||||
|
private string? _error;
|
||||||
|
private Timer? _timer;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await TickAsync();
|
||||||
|
_timer = new Timer(async _ =>
|
||||||
|
{
|
||||||
|
await TickAsync();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TickAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_dbTime = await TimeApi.InsertAndGetLastAsync();
|
||||||
|
_error = null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_error = ex.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_timer != null)
|
||||||
|
await _timer.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,10 @@ if (!string.IsNullOrWhiteSpace(apiBaseUrl))
|
|||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
});
|
||||||
|
builder.Services.AddHttpClient<TimeApiClient>(client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -38,6 +42,7 @@ else
|
|||||||
builder.Services.AddHttpClient<DashboardApiClient>();
|
builder.Services.AddHttpClient<DashboardApiClient>();
|
||||||
builder.Services.AddHttpClient<MassDataApiClient>();
|
builder.Services.AddHttpClient<MassDataApiClient>();
|
||||||
builder.Services.AddHttpClient<LayoutApiClient>();
|
builder.Services.AddHttpClient<LayoutApiClient>();
|
||||||
|
builder.Services.AddHttpClient<TimeApiClient>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|||||||
27
DbFirst.BlazorWebApp/Services/TimeApiClient.cs
Normal file
27
DbFirst.BlazorWebApp/Services/TimeApiClient.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
|
namespace DbFirst.BlazorWebApp.Services;
|
||||||
|
|
||||||
|
public class TimeApiClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private const string Endpoint = "api/time";
|
||||||
|
|
||||||
|
public TimeApiClient(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DateTime?> InsertAndGetLastAsync()
|
||||||
|
{
|
||||||
|
var response = await _httpClient.PostAsync(Endpoint, null);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<TimeResponse>();
|
||||||
|
return result?.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TimeResponse
|
||||||
|
{
|
||||||
|
public DateTime? Now { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
6
DbFirst.Domain/Entities/Time.cs
Normal file
6
DbFirst.Domain/Entities/Time.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace DbFirst.Domain.Entities;
|
||||||
|
|
||||||
|
public class TimeRecord
|
||||||
|
{
|
||||||
|
public DateTime? Now { get; set; }
|
||||||
|
}
|
||||||
@@ -16,6 +16,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; }
|
public virtual DbSet<SmfLayout> SmfLayouts { get; set; }
|
||||||
|
public virtual DbSet<TimeRecord> Times { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -83,6 +84,16 @@ public partial class ApplicationDbContext : DbContext
|
|||||||
.HasColumnName("CHANGED_WHEN");
|
.HasColumnName("CHANGED_WHEN");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<TimeRecord>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasNoKey();
|
||||||
|
entity.ToTable("TIME");
|
||||||
|
|
||||||
|
entity.Property(e => e.Now)
|
||||||
|
.HasColumnType("datetime")
|
||||||
|
.HasColumnName("NOW");
|
||||||
|
});
|
||||||
|
|
||||||
OnModelCreatingPartial(modelBuilder);
|
OnModelCreatingPartial(modelBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
DbFirst.Infrastructure/Repositories/TimeRepository.cs
Normal file
28
DbFirst.Infrastructure/Repositories/TimeRepository.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using DbFirst.Application.Repositories;
|
||||||
|
using DbFirst.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace DbFirst.Infrastructure.Repositories;
|
||||||
|
|
||||||
|
public class TimeRepository : ITimeRepository
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _db;
|
||||||
|
|
||||||
|
public TimeRepository(ApplicationDbContext db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InsertAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _db.Database.ExecuteSqlRawAsync("INSERT INTO [TIME] (NOW) VALUES (GETDATE())", cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TimeRecord?> GetLastAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _db.Times
|
||||||
|
.AsNoTracking()
|
||||||
|
.OrderByDescending(t => t.Now)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user