Compare commits

...

15 Commits

Author SHA1 Message Date
OlgunR
6ac48a472d Simplify Fluent theme registration in App.razor
Replaced custom Fluent theme registration (with ApplyToPageElements set to true) with the default Fluent theme registration, removing unnecessary property customization for a cleaner setup.
2026-04-15 14:16:35 +02:00
OlgunR
96dfd5b3c6 Set default values for CarouselData string properties
Initialized Source and AlternateText with empty strings in the
CarouselData class to prevent potential null reference issues.
2026-04-15 14:16:03 +02:00
OlgunR
810771f385 Remove auto-refresh of dashboards in OnParametersSetAsync
Previously, RefreshDashboards() was called automatically if the dashboards list was empty during parameter setting. This logic has been removed, so dashboards will no longer refresh automatically in this scenario.
2026-04-15 14:08:14 +02:00
OlgunR
a0e0d7ed03 Centralize API error handling in ApiClientHelper
Refactored error handling logic for API responses into a new static ApiClientHelper class, consolidating the ReadErrorAsync method and ProblemDetailsDto. Updated CatalogApiClient and MassDataApiClient to use the shared helper, removing redundant local methods and classes. This change improves consistency, reduces duplication, and streamlines error message formatting across the application.
2026-04-15 13:55:57 +02:00
OlgunR
39cb63a78d Refactor UpsertAsync to use ApiResult for error handling
MassDataApiClient.UpsertAsync now returns ApiResult to standardize success and error reporting, including detailed error extraction from API responses. Updated MassDataGrid.razor to handle the new result type and display error messages accordingly. Removed obsolete try-catch logic in favor of the new pattern.
2026-04-15 11:27:48 +02:00
OlgunR
7d08923444 Remove unused bandEditorExpanded field from BandGridBase
The protected bool bandEditorExpanded, which tracked the expansion
state of the band editor, has been removed from BandGridBase<TItem>
as it is no longer used. This helps clean up unused state from
the component.
2026-04-15 11:01:22 +02:00
OlgunR
11374347d3 Refactor band editor into reusable BandEditor component
Extract band editor UI and logic from CatalogsGrid.razor and MassDataGrid.razor into a new BandEditor.razor component. This centralizes band management features (add/remove bands, edit captions, assign columns, save/reset layout) and reduces code duplication, improving maintainability and reusability. Existing UI and event handling remain functionally unchanged.
2026-04-15 10:57:28 +02:00
OlgunR
7552b34ced Refactor MassDataGrid to use BandGridBase for layout logic
Move band and column management to BandGridBase, reducing duplication in MassDataGrid. Column definitions and layout key are now provided via overridden properties. Band editor actions now use feedback methods for user messages. Grid layout application and error display are streamlined. Improves maintainability and enables reuse across grid components.
2026-04-14 16:55:14 +02:00
OlgunR
13d134df0e Refactor CatalogsGrid to use BandGridBase and async layout ops
Refactored CatalogsGrid.razor to inherit from BandGridBase<CatalogReadDto>, moving band layout and grid logic into the base class. Updated UI to use new async methods for saving and resetting the layout with user feedback. Removed redundant local fields and methods, centralizing layout management and improving maintainability and code reuse. Error and info messages are now shown after layout operations.
2026-04-14 16:37:59 +02:00
OlgunR
285f029311 Add BandGridBase<TItem> for reusable banded grid logic
Introduced an abstract base class BandGridBase<TItem> to centralize banded grid functionality in Blazor apps using DevExpress. This class manages band layouts, column definitions, user-specific layout persistence, grid size modes, and rendering logic. It integrates with BandLayoutService for layout storage and retrieval, and is intended for inheritance by specific grid components.
2026-04-14 16:10:14 +02:00
OlgunR
cdf225bad1 Add full project documentation (PROJEKTDOKUMENTATION.md)
Comprehensive documentation for the DbFirst solution, detailing project goals, architecture (Clean Architecture), technology stack, solution/project structure, Database-First approach, CQRS with MediatR, API endpoints, Blazor/DevExpress frontend, dashboard and layout persistence, SignalR integration, configuration, development setup, architecture decisions (ADRs), extensibility, and glossary. Includes diagrams, code snippets, and workflow guidance for the developer team.
2026-04-09 15:45:32 +02:00
OlgunR
59e051f162 Test push für Marvin No. 4 2026-03-31 11:52:42 +02:00
OlgunR
1352dda20e Test push für Marvin No. 3 2026-03-31 10:39:12 +02:00
OlgunR
33b5b03bf4 Test push für Marvin No. 2 2026-03-31 09:42:46 +02:00
OlgunR
6b986db62e Test push für Marvin 2026-03-31 09:33:08 +02:00
11 changed files with 1603 additions and 594 deletions

View File

@@ -5,10 +5,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
@DxResourceManager.RegisterTheme(Themes.Fluent.Clone(properties =>
{
properties.ApplyToPageElements = true;
}))
@DxResourceManager.RegisterTheme(Themes.Fluent)
@DxResourceManager.RegisterScripts()
<link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" />

View File

@@ -0,0 +1,55 @@
<div class="band-editor">
<button class="band-editor-toggle" @onclick="() => IsExpanded = !IsExpanded">
<span class="band-editor-toggle-icon @(IsExpanded ? "expanded" : "")">&#9658;</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; }
}

View File

@@ -0,0 +1,214 @@
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 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();
}
}

View File

@@ -1,5 +1,5 @@
@inherits BandGridBase<CatalogReadDto>
@inject CatalogApiClient Api
@inject BandLayoutService BandLayoutService
@if (!string.IsNullOrWhiteSpace(errorMessage))
{
@@ -24,44 +24,17 @@ else if (items.Count == 0)
}
else
{
<div class="band-editor">
<button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded">
<span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">&#9658;</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>
<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="grid-section">
<DxGrid Data="@items"
@@ -148,29 +121,21 @@ else
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;
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 }
};
protected override string LayoutKey => "CatalogsGrid";
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 readonly List<ProcedureOption> procedureOptions = new()
{
@@ -178,44 +143,15 @@ 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()
{
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 InitializeBandLayoutAsync();
await LoadCatalogs();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
{
gridRef.LoadLayout(bandLayout.GridLayout);
gridLayoutApplied = true;
await InvokeAsync(StateHasChanged);
}
await ApplyGridLayoutAfterRenderAsync();
}
private async Task LoadCatalogs()
@@ -238,174 +174,6 @@ else
}
}
private async Task SaveLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
return;
try
{
CaptureColumnLayoutFromGrid();
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
infoMessage = "Layout gespeichert.";
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
}
}
private async Task ResetLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
return;
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
bandLayout = new BandLayout();
columnBandAssignments.Clear();
UpdateBandOptions();
foreach (var column in columnDefinitions)
column.Width = null;
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
_sizeMode = SizeMode.Medium;
if (gridRef != null)
gridRef.LoadLayout(new GridPersistentLayout());
gridLayoutApplied = false;
infoMessage = "Layout zurückgesetzt.";
errorMessage = null;
}
private void CaptureColumnLayoutFromGrid()
{
if (gridRef == null)
return;
var layout = gridRef.SaveLayout();
bandLayout.GridLayout = layout;
bandLayout.SizeMode = _sizeMode;
var orderedColumns = layout.Columns
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
.OrderBy(c => c.VisibleIndex)
.ToList();
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
bandLayout.ColumnWidths = orderedColumns
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
}
private void ApplyColumnLayoutFromStorage()
{
foreach (var column in columnDefinitions)
{
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
column.Width = width;
}
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
}
private void AddBand()
{
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
UpdateBandOptions();
}
private void RemoveBand(BandDefinition band)
{
bandLayout.Bands.Remove(band);
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
columnBandAssignments.Remove(key);
UpdateBandOptions();
SyncBandsFromAssignments();
}
private void UpdateBandCaption(BandDefinition band, string value)
{
band.Caption = value;
UpdateBandOptions();
}
private void UpdateColumnBand(string fieldName, string? bandId)
{
if (string.IsNullOrWhiteSpace(bandId))
columnBandAssignments.Remove(fieldName);
else
columnBandAssignments[fieldName] = bandId;
SyncBandsFromAssignments();
}
private string GetColumnBand(string fieldName)
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
private void SyncBandsFromAssignments()
{
foreach (var band in bandLayout.Bands)
{
band.Columns = columnDefinitions
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
.Select(c => c.FieldName)
.ToList();
}
StateHasChanged();
}
private void UpdateBandOptions()
{
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
}
private RenderFragment RenderColumns() => builder =>
{
var seq = 0;
builder.OpenComponent<DxGridCommandColumn>(seq++);
builder.AddAttribute(seq++, "Width", "120px");
builder.CloseComponent();
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
BuildDataColumn(builder, ref seq, column);
foreach (var band in bandLayout.Bands)
{
if (band.Columns.Count == 0) continue;
builder.OpenComponent<DxGridBandColumn>(seq++);
builder.AddAttribute(seq++, "Caption", band.Caption);
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
{
var bandSeq = 0;
foreach (var columnName in band.Columns)
{
if (columnLookup.TryGetValue(columnName, out var column))
BuildDataColumn(bandBuilder, ref bandSeq, column);
}
}));
builder.CloseComponent();
}
};
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
{
builder.OpenComponent<DxGridDataColumn>(seq++);
builder.AddAttribute(seq++, "FieldName", column.FieldName);
builder.AddAttribute(seq++, "Caption", column.Caption);
if (!string.IsNullOrWhiteSpace(column.Width))
builder.AddAttribute(seq++, "Width", column.Width);
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
if (column.ReadOnly)
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
}
private void SetEditContext(EditContext context)
{
if (editContext == context) return;
@@ -575,4 +343,25 @@ else
public int Value { get; set; }
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;
}
}

View File

@@ -1,5 +1,5 @@
@inherits BandGridBase<MassDataReadDto>
@inject MassDataApiClient Api
@inject BandLayoutService BandLayoutService
@if (!string.IsNullOrWhiteSpace(errorMessage))
{
@@ -25,44 +25,17 @@ else if (items.Count == 0)
else
{
<div class="band-editor">
<button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded">
<span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">&#9658;</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>
<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="mb-3 page-size-selector">
<span class="page-size-label">Datensätze je Seite:</span>
@@ -178,28 +151,20 @@ else
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;
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 }
};
protected override string LayoutKey => "MassDataGrid";
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 readonly List<PageSizeOption> pageSizeOptions = new()
{
@@ -215,44 +180,15 @@ 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()
{
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 InitializeBandLayoutAsync();
await LoadPage(0);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
{
gridRef.LoadLayout(bandLayout.GridLayout);
gridLayoutApplied = true;
await InvokeAsync(StateHasChanged);
}
await ApplyGridLayoutAfterRenderAsync();
}
private async Task LoadPage(int page)
@@ -288,160 +224,6 @@ else
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)
{
if (editContext == context) return;
@@ -523,18 +305,17 @@ else
Category = editModel.Category,
StatusFlag = editModel.StatusFlag
};
try
var result = await Api.UpsertAsync(dto);
if (!result.Success)
{
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}";
errorMessage = result.Error ?? "Speichern fehlgeschlagen.";
e.Cancel = true;
return;
}
infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert.";
focusedRowKey = result.Value?.Id;
await LoadPage(pageIndex);
}
private void AddValidationError(MassDataEditModel editModel, string fieldName, string message)
@@ -575,4 +356,25 @@ else
public int? Value { get; set; }
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;
}
}

View File

@@ -67,11 +67,6 @@
protected override async Task OnParametersSetAsync()
{
if (dashboards.Count == 0)
{
await RefreshDashboards();
}
var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase)
? null
: DashboardId;

View File

@@ -31,8 +31,8 @@
public class CarouselData
{
public string Source { get; set; }
public string AlternateText { get; set; }
public string Source { get; set; } = string.Empty;
public string AlternateText { get; set; } = string.Empty;
public CarouselData(string source, string alt)
{

View File

@@ -0,0 +1,60 @@
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);
}
}

View File

@@ -34,7 +34,7 @@ public class CatalogApiClient
return ApiResult<CatalogReadDto?>.Ok(payload);
}
var error = await ReadErrorAsync(response);
var error = await ApiClientHelper.ReadErrorAsync(response);
return ApiResult<CatalogReadDto?>.Fail(error);
}
@@ -46,7 +46,7 @@ public class CatalogApiClient
return ApiResult<bool>.Ok(true);
}
var error = await ReadErrorAsync(response);
var error = await ApiClientHelper.ReadErrorAsync(response);
return ApiResult<bool>.Fail(error);
}
@@ -58,68 +58,9 @@ public class CatalogApiClient
return ApiResult<bool>.Ok(true);
}
var error = await ReadErrorAsync(response);
var error = await ApiClientHelper.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)
@@ -128,9 +69,3 @@ public record ApiResult<T>(bool Success, T? Value, string? Error)
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; }
}

View File

@@ -36,12 +36,17 @@ public class MassDataApiClient
return result ?? new List<MassDataReadDto>();
}
public async Task<MassDataReadDto> UpsertAsync(MassDataWriteDto dto)
public async Task<ApiResult<MassDataReadDto?>> UpsertAsync(MassDataWriteDto dto)
{
var response = await _httpClient.PostAsJsonAsync($"{Endpoint}/upsert", dto);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>();
return payload ?? new MassDataReadDto();
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);
}
public async Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName)

1157
docs/PROJEKTDOKUMENTATION.md Normal file

File diff suppressed because it is too large Load Diff