Compare commits
2 Commits
c73c7e63fe
...
feat/timer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86feec930b | ||
|
|
f5224e20f2 |
@@ -50,6 +50,17 @@ public class CatalogsController : ControllerBase
|
||||
[HttpPut("{id:int}")]
|
||||
public async Task<ActionResult<CatalogReadDto>> Update(int id, CatalogWriteDto dto, CancellationToken cancellationToken)
|
||||
{
|
||||
var current = await _mediator.Send(new GetCatalogByIdQuery(id), cancellationToken);
|
||||
if (current == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
if (dto.UpdateProcedure == CatalogUpdateProcedure.Update &&
|
||||
!string.Equals(current.CatTitle, dto.CatTitle, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return BadRequest("Titel kann nicht geändert werden.");
|
||||
}
|
||||
|
||||
var updated = await _mediator.Send(new UpdateCatalogCommand(id, dto), cancellationToken);
|
||||
if (updated == null)
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@ using System.Text;
|
||||
using DbFirst.Application.Repositories;
|
||||
using DbFirst.Domain.Entities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using DbFirst.Application.Layouts;
|
||||
|
||||
namespace DbFirst.API.Controllers;
|
||||
|
||||
@@ -84,4 +83,12 @@ public class LayoutsController : ControllerBase
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ using DbFirst.API.Middleware;
|
||||
using DbFirst.API.Dashboards;
|
||||
using DbFirst.API.Hubs;
|
||||
using DbFirst.Application;
|
||||
using DbFirst.Application.Repositories;
|
||||
using DbFirst.Domain;
|
||||
using DbFirst.Domain.Entities;
|
||||
using DbFirst.Infrastructure;
|
||||
using DbFirst.Infrastructure.Repositories;
|
||||
using DevExpress.AspNetCore;
|
||||
using DevExpress.DashboardAspNetCore;
|
||||
using DevExpress.DashboardCommon;
|
||||
@@ -49,6 +51,11 @@ builder.Services.AddCors(options =>
|
||||
builder.Services.AddInfrastructure(builder.Configuration);
|
||||
builder.Services.AddApplication();
|
||||
|
||||
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
|
||||
builder.Services.AddScoped<IMassDataRepository, MassDataRepository>();
|
||||
builder.Services.AddScoped<ILayoutRepository, LayoutRepository>();
|
||||
builder.Services.AddScoped<ITimeRepository, TimeRepository>();
|
||||
|
||||
builder.Services.AddDevExpressControls();
|
||||
builder.Services.AddSignalR();
|
||||
builder.Services.AddSingleton<IDashboardChangeNotifier, DashboardChangeNotifier>();
|
||||
|
||||
@@ -25,20 +25,18 @@ public class UpdateCatalogHandler : IRequestHandler<UpdateCatalogCommand, Catalo
|
||||
return null;
|
||||
}
|
||||
|
||||
if (request.Dto.UpdateProcedure == CatalogUpdateProcedure.Update &&
|
||||
!string.Equals(existing.CatTitle, request.Dto.CatTitle, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException("Titel kann nicht geändert werden.");
|
||||
}
|
||||
|
||||
var entity = _mapper.Map<VwmyCatalog>(request.Dto);
|
||||
entity.Guid = request.Id;
|
||||
entity.CatTitle = request.Dto.UpdateProcedure == CatalogUpdateProcedure.Update
|
||||
? existing.CatTitle
|
||||
: request.Dto.CatTitle;
|
||||
entity.AddedWho = existing.AddedWho;
|
||||
entity.AddedWhen = existing.AddedWhen;
|
||||
entity.ChangedWho = "system";
|
||||
entity.ChangedWhen = DateTime.UtcNow;
|
||||
|
||||
var updated = await _repository.UpdateAsync(request.Id, entity, request.Dto.UpdateProcedure, cancellationToken);
|
||||
var procedure = request.Dto.UpdateProcedure;
|
||||
var updated = await _repository.UpdateAsync(request.Id, entity, procedure, cancellationToken);
|
||||
return updated == null ? null : _mapper.Map<CatalogReadDto>(updated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace DbFirst.Application.Layouts;
|
||||
|
||||
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;
|
||||
}
|
||||
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,6 +0,0 @@
|
||||
namespace DbFirst.BlazorWebApp;
|
||||
|
||||
public class AppSettings
|
||||
{
|
||||
public string ApiBaseUrl { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -5,7 +5,10 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
@DxResourceManager.RegisterTheme(Themes.Fluent)
|
||||
@DxResourceManager.RegisterTheme(Themes.Fluent.Clone(properties =>
|
||||
{
|
||||
properties.ApplyToPageElements = true;
|
||||
}))
|
||||
@DxResourceManager.RegisterScripts()
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" />
|
||||
@@ -18,7 +21,6 @@
|
||||
<link href="_content/DevExpress.Blazor.Dashboard/dx-dashboard.light.min.css" rel="stylesheet" />
|
||||
|
||||
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
|
||||
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<link rel="stylesheet" href="DbFirst.BlazorWebApp.styles.css" />
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<div class="band-editor">
|
||||
<button class="band-editor-toggle" @onclick="() => IsExpanded = !IsExpanded">
|
||||
<span class="band-editor-toggle-icon @(IsExpanded ? "expanded" : "")">►</span>
|
||||
<span>Layout</span>
|
||||
</button>
|
||||
@if (IsExpanded)
|
||||
{
|
||||
<div class="band-editor-body">
|
||||
<div class="band-controls">
|
||||
<DxButton Text="Band hinzufügen" Click="OnAddBand" />
|
||||
<DxButton Text="Layout speichern" Click="OnSaveLayout" Enabled="@CanSave" />
|
||||
<DxButton Text="Layout zurücksetzen" Click="OnResetLayout" />
|
||||
</div>
|
||||
@foreach (var band in Bands)
|
||||
{
|
||||
<div class="band-row">
|
||||
<DxTextBox Text="@band.Caption" TextChanged="@(value => OnBandCaptionChanged.InvokeAsync((band, value)))" />
|
||||
<DxButton Text="Entfernen" Click="@(() => OnRemoveBand.InvokeAsync(band))" />
|
||||
</div>
|
||||
}
|
||||
<DxFormLayout CssClass="band-columns" ColCount="2">
|
||||
@foreach (var column in Columns)
|
||||
{
|
||||
<DxFormLayoutItem Caption="@column.Caption">
|
||||
<DxComboBox Data="@BandOptions"
|
||||
TData="BandOption"
|
||||
TValue="string"
|
||||
TextFieldName="Caption"
|
||||
ValueFieldName="Id"
|
||||
Value="@GetColumnBand(column.FieldName)"
|
||||
ValueChanged="@(value => OnColumnBandChanged.InvokeAsync((column.FieldName, value)))"
|
||||
Width="100%" />
|
||||
</DxFormLayoutItem>
|
||||
}
|
||||
</DxFormLayout>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool IsExpanded { get; set; }
|
||||
|
||||
[Parameter, EditorRequired] public List<BandDefinition> Bands { get; set; } = new();
|
||||
[Parameter, EditorRequired] public List<BandOption> BandOptions { get; set; } = new();
|
||||
[Parameter, EditorRequired] public List<ColumnDefinition> Columns { get; set; } = new();
|
||||
[Parameter, EditorRequired] public Func<string, string> GetColumnBand { get; set; } = _ => string.Empty;
|
||||
[Parameter, EditorRequired] public bool CanSave { get; set; }
|
||||
|
||||
[Parameter] public EventCallback OnAddBand { get; set; }
|
||||
[Parameter] public EventCallback OnSaveLayout { get; set; }
|
||||
[Parameter] public EventCallback OnResetLayout { get; set; }
|
||||
[Parameter] public EventCallback<BandDefinition> OnRemoveBand { get; set; }
|
||||
[Parameter] public EventCallback<(BandDefinition Band, string Value)> OnBandCaptionChanged { get; set; }
|
||||
[Parameter] public EventCallback<(string FieldName, string? BandId)> OnColumnBandChanged { get; set; }
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
using DbFirst.BlazorWebApp.Models.Grid;
|
||||
using DbFirst.BlazorWebApp.Services;
|
||||
using DevExpress.Blazor;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
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 IGrid? gridRef;
|
||||
|
||||
// --- SizeMode ---
|
||||
protected SizeMode _sizeMode = SizeMode.Medium;
|
||||
protected static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
|
||||
|
||||
protected string? errorMessage;
|
||||
protected string? infoMessage;
|
||||
protected bool isLoading;
|
||||
protected bool hasLoaded;
|
||||
protected EditContext? editContext;
|
||||
protected ValidationMessageStore? validationMessageStore;
|
||||
protected string popupHeaderText = "Edit";
|
||||
protected int _focusedVisibleIndex;
|
||||
|
||||
private const string LayoutType = "GRID_BANDS";
|
||||
|
||||
// --- Lifecycle ---
|
||||
protected async Task InitializeBandLayoutAsync()
|
||||
{
|
||||
columnLookup = ColumnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
|
||||
bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
|
||||
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
|
||||
ApplyColumnLayoutFromStorage();
|
||||
_sizeMode = bandLayout.SizeMode;
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
protected async Task ApplyGridLayoutAfterRenderAsync()
|
||||
{
|
||||
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
||||
{
|
||||
gridRef.LoadLayout(bandLayout.GridLayout);
|
||||
gridLayoutApplied = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Layout speichern / zurücksetzen ---
|
||||
protected async Task SaveLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser)) return;
|
||||
CaptureColumnLayoutFromGrid();
|
||||
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
|
||||
}
|
||||
|
||||
protected async Task ResetLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser)) return;
|
||||
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
|
||||
bandLayout = new BandLayout();
|
||||
columnBandAssignments.Clear();
|
||||
UpdateBandOptions();
|
||||
foreach (var column in ColumnDefinitions)
|
||||
column.Width = null;
|
||||
columnLookup = ColumnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
_sizeMode = SizeMode.Medium;
|
||||
gridRef?.LoadLayout(new GridPersistentLayout());
|
||||
gridLayoutApplied = false;
|
||||
}
|
||||
|
||||
private void CaptureColumnLayoutFromGrid()
|
||||
{
|
||||
if (gridRef == null) return;
|
||||
var layout = gridRef.SaveLayout();
|
||||
bandLayout.GridLayout = layout;
|
||||
bandLayout.SizeMode = _sizeMode;
|
||||
var orderedColumns = layout.Columns
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
|
||||
.OrderBy(c => c.VisibleIndex)
|
||||
.ToList();
|
||||
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
|
||||
bandLayout.ColumnWidths = orderedColumns
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
|
||||
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void ApplyColumnLayoutFromStorage()
|
||||
{
|
||||
foreach (var column in ColumnDefinitions)
|
||||
{
|
||||
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||||
column.Width = width;
|
||||
}
|
||||
columnLookup = ColumnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// --- Band-Methoden ---
|
||||
protected bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||
|
||||
protected virtual bool ShowCommandColumn => true;
|
||||
|
||||
protected void AddBand()
|
||||
{
|
||||
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
protected 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();
|
||||
}
|
||||
_ = InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
protected void UpdateBandOptions()
|
||||
{
|
||||
bandOptions = [new() { Id = string.Empty, Caption = "Ohne Band" }];
|
||||
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
|
||||
}
|
||||
|
||||
// --- SizeMode ---
|
||||
protected string FormatSizeText(SizeMode size) => size switch
|
||||
{
|
||||
SizeMode.Small => "Klein",
|
||||
SizeMode.Medium => "Mittel",
|
||||
SizeMode.Large => "Groß",
|
||||
_ => size.ToString()
|
||||
};
|
||||
|
||||
protected void OnSizeChange(DropDownButtonItemClickEventArgs args)
|
||||
{
|
||||
_sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
|
||||
}
|
||||
|
||||
// --- RenderColumns / BuildDataColumn ---
|
||||
protected RenderFragment RenderColumns() => builder =>
|
||||
{
|
||||
var seq = 0;
|
||||
if (ShowCommandColumn)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
protected void SetEditContext(EditContext context)
|
||||
{
|
||||
if (editContext == context) return;
|
||||
if (editContext != null)
|
||||
editContext.OnFieldChanged -= OnEditFieldChanged;
|
||||
editContext = context;
|
||||
validationMessageStore = new ValidationMessageStore(editContext);
|
||||
editContext.OnFieldChanged += OnEditFieldChanged;
|
||||
}
|
||||
|
||||
protected virtual void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
||||
{
|
||||
validationMessageStore?.Clear();
|
||||
editContext?.NotifyValidationStateChanged();
|
||||
}
|
||||
|
||||
protected void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
|
||||
|
||||
protected async Task EditFocusedRow()
|
||||
=> await gridRef!.StartEditRowAsync(_focusedVisibleIndex);
|
||||
|
||||
protected Task DeleteFocusedRow()
|
||||
{
|
||||
gridRef!.ShowRowDeleteConfirmation(_focusedVisibleIndex);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected async Task SaveLayoutWithFeedbackAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await SaveLayoutAsync();
|
||||
infoMessage = "Layout gespeichert.";
|
||||
errorMessage = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task ResetLayoutWithFeedbackAsync()
|
||||
{
|
||||
await ResetLayoutAsync();
|
||||
infoMessage = "Layout zurückgesetzt.";
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
@inherits BandGridBase<CatalogReadDto>
|
||||
@inject ICatalogApiClient Api
|
||||
@inject CatalogApiClient Api
|
||||
@inject BandLayoutService BandLayoutService
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||
{
|
||||
@@ -24,21 +24,47 @@ else if (items.Count == 0)
|
||||
}
|
||||
else
|
||||
{
|
||||
<BandEditor Bands="@bandLayout.Bands"
|
||||
BandOptions="@bandOptions"
|
||||
Columns="@ColumnDefinitions"
|
||||
GetColumnBand="GetColumnBand"
|
||||
CanSave="@CanSaveBandLayout"
|
||||
OnAddBand="AddBand"
|
||||
OnSaveLayout="SaveLayoutWithFeedbackAsync"
|
||||
OnResetLayout="ResetLayoutWithFeedbackAsync"
|
||||
OnRemoveBand="RemoveBand"
|
||||
OnBandCaptionChanged="@(args => UpdateBandCaption(args.Band, args.Value))"
|
||||
OnColumnBandChanged="@(args => UpdateColumnBand(args.FieldName, args.BandId))" />
|
||||
<div class="band-editor">
|
||||
<button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded">
|
||||
<span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">►</span>
|
||||
<span>Layout</span>
|
||||
</button>
|
||||
@if (bandEditorExpanded)
|
||||
{
|
||||
<div class="band-editor-body">
|
||||
<div class="band-controls">
|
||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
|
||||
</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>
|
||||
|
||||
<div class="grid-section">
|
||||
<DxGrid Data="@items"
|
||||
ColumnChooserButtonDisplayMode="GridColumnChooserButtonDisplayMode.Always"
|
||||
TItem="CatalogReadDto"
|
||||
KeyFieldName="@nameof(CatalogReadDto.Guid)"
|
||||
SizeMode="@_sizeMode"
|
||||
@@ -59,42 +85,9 @@ else
|
||||
DataItemDeleting="OnDataItemDeleting"
|
||||
FocusedRowEnabled="true"
|
||||
@bind-FocusedRowKey="focusedRowKey"
|
||||
RowClick="@(args => _focusedVisibleIndex = args.VisibleIndex)"
|
||||
@ref="gridRef">
|
||||
<ToolbarTemplate>
|
||||
<DxToolbar>
|
||||
<DxToolbarItem Alignment="ToolbarItemAlignment.Left">
|
||||
<Template Context="_">
|
||||
<DxButton IconCssClass="bi bi-plus-lg"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
RenderStyleMode="ButtonRenderStyleMode.Text"
|
||||
Click="@(() => gridRef!.StartEditNewRowAsync())" />
|
||||
</Template>
|
||||
</DxToolbarItem>
|
||||
<DxToolbarItem Alignment="ToolbarItemAlignment.Left">
|
||||
<Template Context="_">
|
||||
<DxButton IconCssClass="bi bi-pencil"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
RenderStyleMode="ButtonRenderStyleMode.Text"
|
||||
Click="EditFocusedRow" />
|
||||
</Template>
|
||||
</DxToolbarItem>
|
||||
<DxToolbarItem Alignment="ToolbarItemAlignment.Left">
|
||||
<Template Context="_">
|
||||
<DxButton IconCssClass="bi bi-trash"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
RenderStyleMode="ButtonRenderStyleMode.Text"
|
||||
Click="DeleteFocusedRow" />
|
||||
</Template>
|
||||
</DxToolbarItem>
|
||||
<DxToolbarItem Alignment="ToolbarItemAlignment.Right">
|
||||
<Template Context="_">
|
||||
<DxButton Text="Spalten"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
RenderStyleMode="ButtonRenderStyleMode.Text"
|
||||
Click="@(() => gridRef!.ShowColumnChooser())" />
|
||||
</Template>
|
||||
</DxToolbarItem>
|
||||
<DxToolbarItem Alignment="ToolbarItemAlignment.Right">
|
||||
<Template Context="_">
|
||||
<DxDropDownButton Text="@FormatSizeText(_sizeMode)"
|
||||
@@ -149,21 +142,35 @@ else
|
||||
|
||||
@code {
|
||||
private List<CatalogReadDto> items = new();
|
||||
private bool isLoading;
|
||||
private bool hasLoaded;
|
||||
private string? errorMessage;
|
||||
private string? infoMessage;
|
||||
private EditContext? editContext;
|
||||
private ValidationMessageStore? validationMessageStore;
|
||||
private IGrid? gridRef;
|
||||
private int? focusedRowKey;
|
||||
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";
|
||||
protected override bool ShowCommandColumn => false;
|
||||
|
||||
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.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 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()
|
||||
{
|
||||
@@ -171,15 +178,44 @@ else
|
||||
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()
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -202,7 +238,185 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
||||
private async Task SaveLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
CaptureColumnLayoutFromGrid();
|
||||
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
|
||||
infoMessage = "Layout gespeichert.";
|
||||
errorMessage = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ResetLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
return;
|
||||
|
||||
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
|
||||
|
||||
bandLayout = new BandLayout();
|
||||
columnBandAssignments.Clear();
|
||||
UpdateBandOptions();
|
||||
|
||||
foreach (var column in columnDefinitions)
|
||||
column.Width = null;
|
||||
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
_sizeMode = SizeMode.Medium;
|
||||
|
||||
if (gridRef != null)
|
||||
gridRef.LoadLayout(new GridPersistentLayout());
|
||||
gridLayoutApplied = false;
|
||||
|
||||
infoMessage = "Layout zurückgesetzt.";
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
private void CaptureColumnLayoutFromGrid()
|
||||
{
|
||||
if (gridRef == null)
|
||||
return;
|
||||
|
||||
var layout = gridRef.SaveLayout();
|
||||
bandLayout.GridLayout = layout;
|
||||
bandLayout.SizeMode = _sizeMode;
|
||||
|
||||
var orderedColumns = layout.Columns
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
|
||||
.OrderBy(c => c.VisibleIndex)
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
|
||||
bandLayout.ColumnWidths = orderedColumns
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
|
||||
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void ApplyColumnLayoutFromStorage()
|
||||
{
|
||||
foreach (var column in columnDefinitions)
|
||||
{
|
||||
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||||
column.Width = width;
|
||||
}
|
||||
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void AddBand()
|
||||
{
|
||||
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private void RemoveBand(BandDefinition band)
|
||||
{
|
||||
bandLayout.Bands.Remove(band);
|
||||
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
|
||||
columnBandAssignments.Remove(key);
|
||||
UpdateBandOptions();
|
||||
SyncBandsFromAssignments();
|
||||
}
|
||||
|
||||
private void UpdateBandCaption(BandDefinition band, string value)
|
||||
{
|
||||
band.Caption = value;
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private void UpdateColumnBand(string fieldName, string? bandId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(bandId))
|
||||
columnBandAssignments.Remove(fieldName);
|
||||
else
|
||||
columnBandAssignments[fieldName] = bandId;
|
||||
SyncBandsFromAssignments();
|
||||
}
|
||||
|
||||
private string GetColumnBand(string fieldName)
|
||||
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
||||
|
||||
private void SyncBandsFromAssignments()
|
||||
{
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
band.Columns = columnDefinitions
|
||||
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
|
||||
.Select(c => c.FieldName)
|
||||
.ToList();
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void UpdateBandOptions()
|
||||
{
|
||||
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
||||
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
|
||||
}
|
||||
|
||||
private RenderFragment RenderColumns() => builder =>
|
||||
{
|
||||
var seq = 0;
|
||||
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "Width", "120px");
|
||||
builder.CloseComponent();
|
||||
|
||||
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
|
||||
BuildDataColumn(builder, ref seq, column);
|
||||
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
if (band.Columns.Count == 0) continue;
|
||||
|
||||
builder.OpenComponent<DxGridBandColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "Caption", band.Caption);
|
||||
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
||||
{
|
||||
var bandSeq = 0;
|
||||
foreach (var columnName in band.Columns)
|
||||
{
|
||||
if (columnLookup.TryGetValue(columnName, out var column))
|
||||
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
||||
}
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
};
|
||||
|
||||
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
|
||||
{
|
||||
builder.OpenComponent<DxGridDataColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
||||
builder.AddAttribute(seq++, "Caption", column.Caption);
|
||||
if (!string.IsNullOrWhiteSpace(column.Width))
|
||||
builder.AddAttribute(seq++, "Width", column.Width);
|
||||
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
||||
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
||||
if (column.ReadOnly)
|
||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void SetEditContext(EditContext context)
|
||||
{
|
||||
if (editContext == context) return;
|
||||
if (editContext != null)
|
||||
editContext.OnFieldChanged -= OnEditFieldChanged;
|
||||
editContext = context;
|
||||
validationMessageStore = new ValidationMessageStore(editContext);
|
||||
editContext.OnFieldChanged += OnEditFieldChanged;
|
||||
}
|
||||
|
||||
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
||||
{
|
||||
if (validationMessageStore == null || editContext == null) return;
|
||||
|
||||
@@ -220,6 +434,8 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
|
||||
|
||||
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||
{
|
||||
popupHeaderText = e.IsNew ? "Neu" : "Edit";
|
||||
|
||||
@@ -31,6 +31,12 @@
|
||||
<span class="bi bi-table-nav-menu" aria-hidden="true"></span> MassData
|
||||
</NavLink>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@inherits BandGridBase<MassDataReadDto>
|
||||
@inject IMassDataApiClient Api
|
||||
@inject MassDataApiClient Api
|
||||
@inject BandLayoutService BandLayoutService
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||
{
|
||||
@@ -25,17 +25,44 @@ else if (items.Count == 0)
|
||||
else
|
||||
{
|
||||
|
||||
<BandEditor Bands="@bandLayout.Bands"
|
||||
BandOptions="@bandOptions"
|
||||
Columns="@ColumnDefinitions"
|
||||
GetColumnBand="GetColumnBand"
|
||||
CanSave="@CanSaveBandLayout"
|
||||
OnAddBand="AddBand"
|
||||
OnSaveLayout="SaveLayoutWithFeedbackAsync"
|
||||
OnResetLayout="ResetLayoutWithFeedbackAsync"
|
||||
OnRemoveBand="RemoveBand"
|
||||
OnBandCaptionChanged="@(args => UpdateBandCaption(args.Band, args.Value))"
|
||||
OnColumnBandChanged="@(args => UpdateColumnBand(args.FieldName, args.BandId))" />
|
||||
<div class="band-editor">
|
||||
<button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded">
|
||||
<span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">►</span>
|
||||
<span>Layout</span>
|
||||
</button>
|
||||
@if (bandEditorExpanded)
|
||||
{
|
||||
<div class="band-editor-body">
|
||||
<div class="band-controls">
|
||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
|
||||
</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>
|
||||
|
||||
<div class="mb-3 page-size-selector">
|
||||
<span class="page-size-label">Datensätze je Seite:</span>
|
||||
@@ -51,7 +78,6 @@ else
|
||||
|
||||
<div class="grid-section">
|
||||
<DxGrid Data="@items"
|
||||
ColumnChooserButtonDisplayMode="GridColumnChooserButtonDisplayMode.Always"
|
||||
TItem="MassDataReadDto"
|
||||
KeyFieldName="@nameof(MassDataReadDto.Id)"
|
||||
SizeMode="@_sizeMode"
|
||||
@@ -72,42 +98,9 @@ else
|
||||
DataItemDeleting="OnDataItemDeleting"
|
||||
FocusedRowEnabled="true"
|
||||
@bind-FocusedRowKey="focusedRowKey"
|
||||
RowClick="@(args => _focusedVisibleIndex = args.VisibleIndex)"
|
||||
@ref="gridRef">
|
||||
<ToolbarTemplate>
|
||||
<DxToolbar>
|
||||
<DxToolbarItem Alignment="ToolbarItemAlignment.Left">
|
||||
<Template Context="_">
|
||||
<DxButton IconCssClass="bi bi-plus-lg"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
RenderStyleMode="ButtonRenderStyleMode.Text"
|
||||
Click="@(() => gridRef!.StartEditNewRowAsync())" />
|
||||
</Template>
|
||||
</DxToolbarItem>
|
||||
<DxToolbarItem Alignment="ToolbarItemAlignment.Left">
|
||||
<Template Context="_">
|
||||
<DxButton IconCssClass="bi bi-pencil"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
RenderStyleMode="ButtonRenderStyleMode.Text"
|
||||
Click="EditFocusedRow" />
|
||||
</Template>
|
||||
</DxToolbarItem>
|
||||
<DxToolbarItem Alignment="ToolbarItemAlignment.Left">
|
||||
<Template Context="_">
|
||||
<DxButton IconCssClass="bi bi-trash"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
RenderStyleMode="ButtonRenderStyleMode.Text"
|
||||
Click="DeleteFocusedRow" />
|
||||
</Template>
|
||||
</DxToolbarItem>
|
||||
<DxToolbarItem Alignment="ToolbarItemAlignment.Right">
|
||||
<Template Context="_">
|
||||
<DxButton Text="Spalten"
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
RenderStyleMode="ButtonRenderStyleMode.Text"
|
||||
Click="@(() => gridRef!.ShowColumnChooser())" />
|
||||
</Template>
|
||||
</DxToolbarItem>
|
||||
<DxToolbarItem Alignment="ToolbarItemAlignment.Right">
|
||||
<Template Context="_">
|
||||
<DxDropDownButton Text="@FormatSizeText(_sizeMode)"
|
||||
@@ -175,24 +168,38 @@ else
|
||||
|
||||
@code {
|
||||
private List<MassDataReadDto> items = new();
|
||||
private bool isLoading;
|
||||
private bool hasLoaded;
|
||||
private string? errorMessage;
|
||||
private string? infoMessage;
|
||||
private int pageIndex;
|
||||
private int pageCount = 1;
|
||||
private int? pageSize = 100;
|
||||
private string popupHeaderText = "Edit";
|
||||
private EditContext? editContext;
|
||||
private ValidationMessageStore? validationMessageStore;
|
||||
private IGrid? gridRef;
|
||||
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";
|
||||
protected override bool ShowCommandColumn => false;
|
||||
|
||||
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.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 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 readonly List<PageSizeOption> pageSizeOptions = new()
|
||||
{
|
||||
@@ -208,15 +215,44 @@ else
|
||||
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()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -252,7 +288,171 @@ else
|
||||
await LoadPage(0);
|
||||
}
|
||||
|
||||
protected override void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
||||
private async Task SaveLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
return;
|
||||
try
|
||||
{
|
||||
CaptureColumnLayoutFromGrid();
|
||||
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
|
||||
infoMessage = "Layout gespeichert.";
|
||||
errorMessage = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ResetLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
return;
|
||||
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
|
||||
bandLayout = new BandLayout();
|
||||
columnBandAssignments.Clear();
|
||||
UpdateBandOptions();
|
||||
foreach (var column in columnDefinitions)
|
||||
column.Width = null;
|
||||
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
_sizeMode = SizeMode.Medium;
|
||||
if (gridRef != null)
|
||||
gridRef.LoadLayout(new GridPersistentLayout());
|
||||
gridLayoutApplied = false;
|
||||
infoMessage = "Layout zurückgesetzt.";
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
private void CaptureColumnLayoutFromGrid()
|
||||
{
|
||||
if (gridRef == null) return;
|
||||
var layout = gridRef.SaveLayout();
|
||||
bandLayout.GridLayout = layout;
|
||||
bandLayout.SizeMode = _sizeMode;
|
||||
var orderedColumns = layout.Columns
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
|
||||
.OrderBy(c => c.VisibleIndex)
|
||||
.ToList();
|
||||
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
|
||||
bandLayout.ColumnWidths = orderedColumns
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
|
||||
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void ApplyColumnLayoutFromStorage()
|
||||
{
|
||||
foreach (var column in columnDefinitions)
|
||||
{
|
||||
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||||
column.Width = width;
|
||||
}
|
||||
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void AddBand()
|
||||
{
|
||||
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private void RemoveBand(BandDefinition band)
|
||||
{
|
||||
bandLayout.Bands.Remove(band);
|
||||
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
|
||||
columnBandAssignments.Remove(key);
|
||||
UpdateBandOptions();
|
||||
SyncBandsFromAssignments();
|
||||
}
|
||||
|
||||
private void UpdateBandCaption(BandDefinition band, string value)
|
||||
{
|
||||
band.Caption = value;
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private void UpdateColumnBand(string fieldName, string? bandId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(bandId))
|
||||
columnBandAssignments.Remove(fieldName);
|
||||
else
|
||||
columnBandAssignments[fieldName] = bandId;
|
||||
SyncBandsFromAssignments();
|
||||
}
|
||||
|
||||
private string GetColumnBand(string fieldName)
|
||||
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
||||
|
||||
private void SyncBandsFromAssignments()
|
||||
{
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
band.Columns = columnDefinitions
|
||||
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
|
||||
.Select(c => c.FieldName)
|
||||
.ToList();
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void UpdateBandOptions()
|
||||
{
|
||||
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
||||
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
|
||||
}
|
||||
|
||||
private RenderFragment RenderColumns() => builder =>
|
||||
{
|
||||
var seq = 0;
|
||||
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "Width", "120px");
|
||||
builder.CloseComponent();
|
||||
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
|
||||
BuildDataColumn(builder, ref seq, column);
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
if (band.Columns.Count == 0) continue;
|
||||
builder.OpenComponent<DxGridBandColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "Caption", band.Caption);
|
||||
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
||||
{
|
||||
var bandSeq = 0;
|
||||
foreach (var columnName in band.Columns)
|
||||
{
|
||||
if (columnLookup.TryGetValue(columnName, out var column))
|
||||
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
||||
}
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
};
|
||||
|
||||
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
|
||||
{
|
||||
builder.OpenComponent<DxGridDataColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
||||
builder.AddAttribute(seq++, "Caption", column.Caption);
|
||||
if (!string.IsNullOrWhiteSpace(column.Width))
|
||||
builder.AddAttribute(seq++, "Width", column.Width);
|
||||
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
||||
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
||||
if (column.ReadOnly)
|
||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void SetEditContext(EditContext context)
|
||||
{
|
||||
if (editContext == context) return;
|
||||
if (editContext != null)
|
||||
editContext.OnFieldChanged -= OnEditFieldChanged;
|
||||
editContext = context;
|
||||
validationMessageStore = new ValidationMessageStore(editContext);
|
||||
editContext.OnFieldChanged += OnEditFieldChanged;
|
||||
}
|
||||
|
||||
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
||||
{
|
||||
if (validationMessageStore == null || editContext == null) return;
|
||||
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure))
|
||||
@@ -268,7 +468,9 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||
private void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
|
||||
|
||||
private async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||
{
|
||||
if (e.IsNew)
|
||||
{
|
||||
@@ -321,17 +523,18 @@ else
|
||||
Category = editModel.Category,
|
||||
StatusFlag = editModel.StatusFlag
|
||||
};
|
||||
|
||||
var result = await Api.UpsertAsync(dto);
|
||||
if (!result.Success)
|
||||
try
|
||||
{
|
||||
errorMessage = result.Error ?? "Speichern fehlgeschlagen.";
|
||||
e.Cancel = true;
|
||||
return;
|
||||
var saved = await Api.UpsertAsync(dto);
|
||||
infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert.";
|
||||
focusedRowKey = saved.Id;
|
||||
await LoadPage(pageIndex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Speichern: {ex.Message}";
|
||||
e.Cancel = true;
|
||||
}
|
||||
infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert.";
|
||||
focusedRowKey = result.Value?.Id;
|
||||
await LoadPage(pageIndex);
|
||||
}
|
||||
|
||||
private void AddValidationError(MassDataEditModel editModel, string fieldName, string message)
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
@page "/dashboard"
|
||||
@page "/dashboards/{DashboardId?}"
|
||||
@implements IAsyncDisposable
|
||||
@inject IOptions<AppSettings> AppSettingsOptions
|
||||
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
|
||||
@inject NavigationManager Navigation
|
||||
@inject IDashboardApiClient DashboardApi
|
||||
@inject DashboardApiClient DashboardApi
|
||||
|
||||
<PageTitle>Dashboards</PageTitle>
|
||||
|
||||
@@ -45,17 +45,12 @@
|
||||
private string SelectedDashboardId { get; set; } = string.Empty;
|
||||
private string DashboardKey => $"{SelectedDashboardId}-{(IsDesigner ? "designer" : "viewer")}";
|
||||
|
||||
private string DashboardEndpoint => $"{AppSettingsOptions.Value.ApiBaseUrl.TrimEnd('/')}/api/dashboard";
|
||||
private string HubEndpoint => $"{AppSettingsOptions.Value.ApiBaseUrl.TrimEnd('/')}/hubs/dashboards";
|
||||
private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
|
||||
private string HubEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/hubs/dashboards";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await RefreshDashboards();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender) return;
|
||||
|
||||
_hubConnection = new HubConnectionBuilder()
|
||||
.WithUrl(HubEndpoint)
|
||||
@@ -72,6 +67,11 @@
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (dashboards.Count == 0)
|
||||
{
|
||||
await RefreshDashboards();
|
||||
}
|
||||
|
||||
var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase)
|
||||
? null
|
||||
: DashboardId;
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
|
||||
public class CarouselData
|
||||
{
|
||||
public string Source { get; set; } = string.Empty;
|
||||
public string AlternateText { get; set; } = string.Empty;
|
||||
public string Source { get; set; }
|
||||
public string AlternateText { get; set; }
|
||||
|
||||
public CarouselData(string source, string alt)
|
||||
{
|
||||
|
||||
@@ -17,5 +17,4 @@
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.DashboardBlazor
|
||||
@using DevExpress.DashboardWeb
|
||||
@using DevExpress.Data.Filtering
|
||||
@using Microsoft.Extensions.Options
|
||||
@using DevExpress.Data.Filtering
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace DbFirst.BlazorWebApp.Models;
|
||||
|
||||
public record ApiResult<T>(bool Success, T? Value, string? Error)
|
||||
{
|
||||
public static ApiResult<T> Ok(T? value) => new(true, value, null);
|
||||
public static ApiResult<T> Fail(string? error) => new(false, default, error);
|
||||
}
|
||||
@@ -4,8 +4,8 @@ namespace DbFirst.BlazorWebApp.Models.Grid
|
||||
{
|
||||
public class BandLayout
|
||||
{
|
||||
public List<BandDefinition> Bands { get; set; } = [];
|
||||
public List<string> ColumnOrder { get; set; } = [];
|
||||
public List<BandDefinition> Bands { get; set; } = new();
|
||||
public List<string> ColumnOrder { get; set; } = new();
|
||||
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
public GridPersistentLayout? GridLayout { get; set; }
|
||||
public SizeMode SizeMode { get; set; } = SizeMode.Medium;
|
||||
@@ -15,7 +15,7 @@ namespace DbFirst.BlazorWebApp.Models.Grid
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Caption { get; set; } = string.Empty;
|
||||
public List<string> Columns { get; set; } = [];
|
||||
public List<string> Columns { get; set; } = new();
|
||||
}
|
||||
|
||||
public class BandOption
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using DbFirst.BlazorWebApp;
|
||||
using DbFirst.BlazorWebApp.Components;
|
||||
using DbFirst.BlazorWebApp.Services;
|
||||
using DevExpress.Blazor;
|
||||
@@ -14,17 +13,37 @@ builder.Services.AddScoped<ThemeState>();
|
||||
builder.Services.AddScoped<BandLayoutService>();
|
||||
|
||||
var apiBaseUrl = builder.Configuration["ApiBaseUrl"];
|
||||
builder.Services.Configure<AppSettings>(builder.Configuration);
|
||||
void ConfigureClient(HttpClient client)
|
||||
if (!string.IsNullOrWhiteSpace(apiBaseUrl))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(apiBaseUrl))
|
||||
builder.Services.AddHttpClient<CatalogApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
builder.Services.AddHttpClient<DashboardApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
builder.Services.AddHttpClient<MassDataApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
builder.Services.AddHttpClient<LayoutApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
builder.Services.AddHttpClient<TimeApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Services.AddHttpClient<CatalogApiClient>();
|
||||
builder.Services.AddHttpClient<DashboardApiClient>();
|
||||
builder.Services.AddHttpClient<MassDataApiClient>();
|
||||
builder.Services.AddHttpClient<LayoutApiClient>();
|
||||
builder.Services.AddHttpClient<TimeApiClient>();
|
||||
}
|
||||
|
||||
builder.Services.AddHttpClient<ICatalogApiClient, CatalogApiClient>(ConfigureClient);
|
||||
builder.Services.AddHttpClient<IDashboardApiClient, DashboardApiClient>(ConfigureClient);
|
||||
builder.Services.AddHttpClient<IMassDataApiClient, MassDataApiClient>(ConfigureClient);
|
||||
builder.Services.AddHttpClient<ILayoutApiClient, LayoutApiClient>(ConfigureClient);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.Services;
|
||||
|
||||
internal sealed class ProblemDetailsDto
|
||||
{
|
||||
public string? Type { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Detail { get; set; }
|
||||
}
|
||||
|
||||
internal static class ApiClientHelper
|
||||
{
|
||||
public static async Task<string> ReadErrorAsync(HttpResponseMessage response)
|
||||
{
|
||||
string? problemTitle = null;
|
||||
string? problemDetail = null;
|
||||
|
||||
try
|
||||
{
|
||||
var problem = await response.Content.ReadFromJsonAsync<ProblemDetailsDto>();
|
||||
if (problem != null)
|
||||
{
|
||||
problemTitle = problem.Title;
|
||||
problemDetail = problem.Detail ?? problem.Type;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
var status = response.StatusCode;
|
||||
var reason = response.ReasonPhrase;
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
|
||||
string? detail = problemDetail;
|
||||
if (string.IsNullOrWhiteSpace(detail) && !string.IsNullOrWhiteSpace(body))
|
||||
detail = body;
|
||||
|
||||
return status switch
|
||||
{
|
||||
HttpStatusCode.BadRequest => $"Eingabe ungültig{FormatSuffix(problemTitle, detail, reason)}",
|
||||
HttpStatusCode.NotFound => $"Nicht gefunden{FormatSuffix(problemTitle, detail, reason)}",
|
||||
HttpStatusCode.Conflict => $"Konflikt{FormatSuffix(problemTitle, detail, reason)}",
|
||||
HttpStatusCode.Unauthorized => $"Nicht autorisiert{FormatSuffix(problemTitle, detail, reason)}",
|
||||
HttpStatusCode.Forbidden => $"Nicht erlaubt{FormatSuffix(problemTitle, detail, reason)}",
|
||||
HttpStatusCode.InternalServerError => $"Serverfehler{FormatSuffix(problemTitle, detail, reason)}",
|
||||
_ => $"Fehler {(int)status} {reason ?? string.Empty}{FormatSuffix(problemTitle, detail, reason)}"
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatSuffix(string? title, string? detail, string? reason)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(title)) parts.Add(title);
|
||||
if (!string.IsNullOrWhiteSpace(detail)) parts.Add(detail);
|
||||
if (parts.Count == 0 && !string.IsNullOrWhiteSpace(reason)) parts.Add(reason);
|
||||
if (parts.Count == 0) return string.Empty;
|
||||
return ": " + string.Join(" | ", parts);
|
||||
}
|
||||
}
|
||||
@@ -5,26 +5,20 @@ using System.Text.Json;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.Services
|
||||
{
|
||||
public class BandLayoutService(ILayoutApiClient layoutApi, IJSRuntime jsRuntime)
|
||||
public class BandLayoutService(LayoutApiClient layoutApi, IJSRuntime jsRuntime)
|
||||
{
|
||||
private const string LayoutUserStorageKey = "layoutUser";
|
||||
private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
private string? _cachedLayoutUser;
|
||||
|
||||
public async Task<string> EnsureLayoutUserAsync()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_cachedLayoutUser))
|
||||
return _cachedLayoutUser;
|
||||
|
||||
var layoutUser = await jsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
layoutUser = Guid.NewGuid().ToString("N");
|
||||
await jsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
|
||||
}
|
||||
|
||||
_cachedLayoutUser = layoutUser;
|
||||
return _cachedLayoutUser;
|
||||
return layoutUser;
|
||||
}
|
||||
|
||||
public async Task<BandLayout> LoadBandLayoutAsync(
|
||||
@@ -88,9 +82,9 @@ namespace DbFirst.BlazorWebApp.Services
|
||||
Dictionary<string, ColumnDefinition> columnLookup)
|
||||
{
|
||||
layout ??= new BandLayout();
|
||||
layout.Bands ??= [];
|
||||
layout.ColumnOrder ??= [];
|
||||
layout.ColumnWidths ??= [];
|
||||
layout.Bands ??= new List<BandDefinition>();
|
||||
layout.ColumnOrder ??= new List<string>();
|
||||
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var band in layout.Bands)
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using DbFirst.BlazorWebApp.Models;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.Services;
|
||||
|
||||
public class CatalogApiClient : ICatalogApiClient
|
||||
public class CatalogApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string Endpoint = "api/catalogs";
|
||||
@@ -14,51 +14,123 @@ public class CatalogApiClient : ICatalogApiClient
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<List<CatalogReadDto>> GetAllAsync(CancellationToken ct = default)
|
||||
public async Task<List<CatalogReadDto>> GetAllAsync()
|
||||
{
|
||||
var result = await _httpClient.GetFromJsonAsync<List<CatalogReadDto>>(Endpoint, ct);
|
||||
return result ?? [];
|
||||
var result = await _httpClient.GetFromJsonAsync<List<CatalogReadDto>>(Endpoint);
|
||||
return result ?? new List<CatalogReadDto>();
|
||||
}
|
||||
|
||||
public async Task<CatalogReadDto?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||
public async Task<CatalogReadDto?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _httpClient.GetFromJsonAsync<CatalogReadDto>($"{Endpoint}/{id}", ct);
|
||||
return await _httpClient.GetFromJsonAsync<CatalogReadDto>($"{Endpoint}/{id}");
|
||||
}
|
||||
|
||||
public async Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto, CancellationToken ct = default)
|
||||
public async Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto, ct);
|
||||
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var payload = await response.Content.ReadFromJsonAsync<CatalogReadDto>();
|
||||
return ApiResult<CatalogReadDto?>.Ok(payload);
|
||||
}
|
||||
|
||||
var error = await ApiClientHelper.ReadErrorAsync(response);
|
||||
var error = await ReadErrorAsync(response);
|
||||
return ApiResult<CatalogReadDto?>.Fail(error);
|
||||
}
|
||||
|
||||
public async Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto, CancellationToken ct = default)
|
||||
public async Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto)
|
||||
{
|
||||
var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto, ct);
|
||||
var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return ApiResult<bool>.Ok(true);
|
||||
}
|
||||
|
||||
var error = await ApiClientHelper.ReadErrorAsync(response);
|
||||
var error = await ReadErrorAsync(response);
|
||||
return ApiResult<bool>.Fail(error);
|
||||
}
|
||||
|
||||
public async Task<ApiResult<bool>> DeleteAsync(int id, CancellationToken ct = default)
|
||||
public async Task<ApiResult<bool>> DeleteAsync(int id)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}", ct);
|
||||
var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}");
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return ApiResult<bool>.Ok(true);
|
||||
}
|
||||
|
||||
var error = await ApiClientHelper.ReadErrorAsync(response);
|
||||
var error = await ReadErrorAsync(response);
|
||||
return ApiResult<bool>.Fail(error);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
|
||||
{
|
||||
string? problemTitle = null;
|
||||
string? problemDetail = null;
|
||||
|
||||
try
|
||||
{
|
||||
var problem = await response.Content.ReadFromJsonAsync<ProblemDetailsDto>();
|
||||
if (problem != null)
|
||||
{
|
||||
problemTitle = problem.Title;
|
||||
problemDetail = problem.Detail ?? problem.Type;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var status = response.StatusCode;
|
||||
var reason = response.ReasonPhrase;
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
|
||||
string? detail = problemDetail;
|
||||
if (string.IsNullOrWhiteSpace(detail) && !string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
detail = body;
|
||||
}
|
||||
|
||||
if (status == HttpStatusCode.Conflict)
|
||||
{
|
||||
return "Datensatz existiert bereits. Bitte wählen Sie einen anderen Titel.";
|
||||
}
|
||||
if (status == HttpStatusCode.BadRequest && (detail?.Contains("CatTitle cannot be changed", StringComparison.OrdinalIgnoreCase) ?? false))
|
||||
{
|
||||
return "Titel kann nicht geändert werden.";
|
||||
}
|
||||
|
||||
return status switch
|
||||
{
|
||||
HttpStatusCode.BadRequest => $"Eingabe ungültig{FormatSuffix(problemTitle, detail, reason)}",
|
||||
HttpStatusCode.NotFound => $"Nicht gefunden{FormatSuffix(problemTitle, detail, reason)}",
|
||||
HttpStatusCode.Conflict => $"Konflikt{FormatSuffix(problemTitle, detail, reason)}",
|
||||
HttpStatusCode.Unauthorized => $"Nicht autorisiert{FormatSuffix(problemTitle, detail, reason)}",
|
||||
HttpStatusCode.Forbidden => $"Nicht erlaubt{FormatSuffix(problemTitle, detail, reason)}",
|
||||
HttpStatusCode.InternalServerError => $"Serverfehler{FormatSuffix(problemTitle, detail, reason)}",
|
||||
_ => $"Fehler {(int)status} {reason ?? string.Empty}{FormatSuffix(problemTitle, detail, reason)}"
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatSuffix(string? title, string? detail, string? reason)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(title)) parts.Add(title);
|
||||
if (!string.IsNullOrWhiteSpace(detail)) parts.Add(detail);
|
||||
if (parts.Count == 0 && !string.IsNullOrWhiteSpace(reason)) parts.Add(reason);
|
||||
if (parts.Count == 0) return string.Empty;
|
||||
return ": " + string.Join(" | ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
public record ApiResult<T>(bool Success, T? Value, string? Error)
|
||||
{
|
||||
public static ApiResult<T> Ok(T? value) => new(true, value, null);
|
||||
public static ApiResult<T> Fail(string? error) => new(false, default, error);
|
||||
}
|
||||
|
||||
internal sealed class ProblemDetailsDto
|
||||
{
|
||||
public string? Type { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Detail { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using DbFirst.BlazorWebApp.Models;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.Services;
|
||||
|
||||
public class DashboardApiClient : IDashboardApiClient
|
||||
public class DashboardApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string Endpoint = "api/dashboard/dashboards";
|
||||
@@ -13,9 +13,9 @@ public class DashboardApiClient : IDashboardApiClient
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<List<DashboardInfoDto>> GetAllAsync(CancellationToken ct = default)
|
||||
public async Task<List<DashboardInfoDto>> GetAllAsync()
|
||||
{
|
||||
var result = await _httpClient.GetFromJsonAsync<List<DashboardInfoDto>>(Endpoint, ct);
|
||||
return result ?? [];
|
||||
var result = await _httpClient.GetFromJsonAsync<List<DashboardInfoDto>>(Endpoint);
|
||||
return result ?? new List<DashboardInfoDto>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using DbFirst.BlazorWebApp.Models;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.Services;
|
||||
|
||||
public interface ICatalogApiClient
|
||||
{
|
||||
Task<List<CatalogReadDto>> GetAllAsync(CancellationToken ct = default);
|
||||
Task<CatalogReadDto?> GetByIdAsync(int id, CancellationToken ct = default);
|
||||
Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto, CancellationToken ct = default);
|
||||
Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto, CancellationToken ct = default);
|
||||
Task<ApiResult<bool>> DeleteAsync(int id, CancellationToken ct = default);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using DbFirst.BlazorWebApp.Models;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.Services
|
||||
{
|
||||
public interface IDashboardApiClient
|
||||
{
|
||||
Task<List<DashboardInfoDto>> GetAllAsync(CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using DbFirst.BlazorWebApp.Models;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.Services
|
||||
{
|
||||
public interface ILayoutApiClient
|
||||
{
|
||||
Task<LayoutDto?> GetAsync(string layoutType, string layoutKey, string userName, CancellationToken ct = default);
|
||||
Task<LayoutDto> UpsertAsync(LayoutDto dto, CancellationToken ct = default);
|
||||
Task DeleteAsync(string layoutType, string layoutKey, string userName, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using DbFirst.BlazorWebApp.Models;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.Services
|
||||
{
|
||||
public interface IMassDataApiClient
|
||||
{
|
||||
Task<int> GetCountAsync(CancellationToken ct = default);
|
||||
Task<List<MassDataReadDto>> GetAllAsync(int? skip, int? take, CancellationToken ct = default);
|
||||
Task<ApiResult<MassDataReadDto?>> UpsertAsync(MassDataWriteDto dto, CancellationToken ct = default);
|
||||
Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using DbFirst.BlazorWebApp.Models;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.Services;
|
||||
|
||||
public class LayoutApiClient : ILayoutApiClient
|
||||
public class LayoutApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string Endpoint = "api/layouts";
|
||||
@@ -13,10 +13,10 @@ public class LayoutApiClient : ILayoutApiClient
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<LayoutDto?> GetAsync(string layoutType, string layoutKey, string userName, CancellationToken ct = default)
|
||||
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, ct);
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
@@ -26,12 +26,12 @@ public class LayoutApiClient : ILayoutApiClient
|
||||
return await response.Content.ReadFromJsonAsync<LayoutDto>();
|
||||
}
|
||||
|
||||
public async Task<LayoutDto> UpsertAsync(LayoutDto dto, CancellationToken ct = default)
|
||||
public async Task<LayoutDto> UpsertAsync(LayoutDto dto)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto, ct);
|
||||
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var detail = await ApiClientHelper.ReadErrorAsync(response);
|
||||
var detail = await ReadErrorAsync(response);
|
||||
throw new InvalidOperationException(detail);
|
||||
}
|
||||
|
||||
@@ -39,10 +39,21 @@ public class LayoutApiClient : ILayoutApiClient
|
||||
return payload ?? dto;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string layoutType, string layoutKey, string userName, CancellationToken ct = default)
|
||||
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, ct);
|
||||
var response = await _httpClient.DeleteAsync(url);
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -3,7 +3,7 @@ using DbFirst.BlazorWebApp.Models;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.Services;
|
||||
|
||||
public class MassDataApiClient : IMassDataApiClient
|
||||
public class MassDataApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string Endpoint = "api/massdata";
|
||||
@@ -13,13 +13,13 @@ public class MassDataApiClient : IMassDataApiClient
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<int> GetCountAsync(CancellationToken ct = default)
|
||||
public async Task<int> GetCountAsync()
|
||||
{
|
||||
var result = await _httpClient.GetFromJsonAsync<int?>("api/massdata/count", ct);
|
||||
var result = await _httpClient.GetFromJsonAsync<int?>("api/massdata/count");
|
||||
return result ?? 0;
|
||||
}
|
||||
|
||||
public async Task<List<MassDataReadDto>> GetAllAsync(int? skip, int? take, CancellationToken ct = default)
|
||||
public async Task<List<MassDataReadDto>> GetAllAsync(int? skip, int? take)
|
||||
{
|
||||
var query = new List<string>();
|
||||
if (skip.HasValue)
|
||||
@@ -32,31 +32,26 @@ public class MassDataApiClient : IMassDataApiClient
|
||||
}
|
||||
|
||||
var url = query.Count == 0 ? Endpoint : $"{Endpoint}?{string.Join("&", query)}";
|
||||
var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>(url, ct);
|
||||
return result ?? [];
|
||||
var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>(url);
|
||||
return result ?? new List<MassDataReadDto>();
|
||||
}
|
||||
|
||||
public async Task<ApiResult<MassDataReadDto?>> UpsertAsync(MassDataWriteDto dto, CancellationToken ct = default)
|
||||
public async Task<MassDataReadDto> UpsertAsync(MassDataWriteDto dto)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync($"{Endpoint}/upsert", dto, ct);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>();
|
||||
return ApiResult<MassDataReadDto?>.Ok(payload);
|
||||
}
|
||||
|
||||
var error = await ApiClientHelper.ReadErrorAsync(response);
|
||||
return ApiResult<MassDataReadDto?>.Fail(error);
|
||||
var response = await _httpClient.PostAsJsonAsync($"{Endpoint}/upsert", dto);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>();
|
||||
return payload ?? new MassDataReadDto();
|
||||
}
|
||||
|
||||
public async Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName, CancellationToken ct = default)
|
||||
public async Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(customerName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var response = await _httpClient.GetAsync($"{Endpoint}/{Uri.EscapeDataString(customerName)}", ct);
|
||||
var response = await _httpClient.GetAsync($"{Endpoint}/{Uri.EscapeDataString(customerName)}");
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
|
||||
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<SmfLayout> SmfLayouts { get; set; }
|
||||
public virtual DbSet<TimeRecord> Times { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -83,6 +84,16 @@ public partial class ApplicationDbContext : DbContext
|
||||
.HasColumnName("CHANGED_WHEN");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<TimeRecord>(entity =>
|
||||
{
|
||||
entity.HasNoKey();
|
||||
entity.ToTable("TIME");
|
||||
|
||||
entity.Property(e => e.Now)
|
||||
.HasColumnType("datetime")
|
||||
.HasColumnName("NOW");
|
||||
});
|
||||
|
||||
OnModelCreatingPartial(modelBuilder);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using DbFirst.Application.Repositories;
|
||||
using DbFirst.Infrastructure.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -17,10 +15,6 @@ public static class DependencyInjection
|
||||
services.AddDbContext<MassDataDbContext>(options =>
|
||||
options.UseSqlServer(configuration.GetConnectionString("MassDataConnection")));
|
||||
|
||||
services.AddScoped<ICatalogRepository, CatalogRepository>();
|
||||
services.AddScoped<IMassDataRepository, MassDataRepository>();
|
||||
services.AddScoped<ILayoutRepository, LayoutRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class LayoutRepository : ILayoutRepository
|
||||
UserName = userName,
|
||||
LayoutData = layoutData,
|
||||
AddedWho = userName,
|
||||
AddedWhen = DateTime.UtcNow
|
||||
AddedWhen = DateTime.Now
|
||||
};
|
||||
_db.SmfLayouts.Add(entity);
|
||||
}
|
||||
|
||||
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