Refactor dashboard navigation, catalog grid, and API client
- Add sidebar dashboard navigation and support multiple dashboards - Extract catalog grid/form logic to reusable CatalogsGrid component - Add CatalogApiClient and DTOs for catalog CRUD operations - Define dashboards with JSON data sources (Default, CatalogsGrid) - Update configuration for dashboard and API endpoints - Improve styling and imports for modularity and maintainability
This commit is contained in:
52
DbFirst.API/Data/Dashboards/CatalogsGrid.xml
Normal file
52
DbFirst.API/Data/Dashboards/CatalogsGrid.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Dashboard CurrencyCulture="de-DE" RequestParameters="false">
|
||||
<Title Text="Catalogs (Dashboard Grid)" />
|
||||
<DataSources>
|
||||
<JsonDataSource Name="Catalogs (API)" ComponentName="catalogsDataSource">
|
||||
<Source SourceType="DevExpress.DataAccess.Json.UriJsonSource" Uri="https://localhost:7204/api/catalogs" />
|
||||
</JsonDataSource>
|
||||
</DataSources>
|
||||
<Items>
|
||||
<Grid ComponentName="gridDashboardItem1" Name="Catalogs" DataSource="catalogsDataSource">
|
||||
<DataItems>
|
||||
<Dimension DataMember="Guid" DefaultId="DataItem0" />
|
||||
<Dimension DataMember="CatTitle" DefaultId="DataItem1" />
|
||||
<Dimension DataMember="CatString" DefaultId="DataItem2" />
|
||||
<Dimension DataMember="AddedWho" DefaultId="DataItem3" />
|
||||
<Dimension DataMember="AddedWhen" DefaultId="DataItem4" />
|
||||
<Dimension DataMember="ChangedWho" DefaultId="DataItem5" />
|
||||
<Dimension DataMember="ChangedWhen" DefaultId="DataItem6" />
|
||||
</DataItems>
|
||||
<GridColumns>
|
||||
<GridDimensionColumn Name="Id">
|
||||
<Dimension DefaultId="DataItem0" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn Name="Titel">
|
||||
<Dimension DefaultId="DataItem1" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn Name="String">
|
||||
<Dimension DefaultId="DataItem2" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn Name="Angelegt von">
|
||||
<Dimension DefaultId="DataItem3" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn Name="Angelegt am">
|
||||
<Dimension DefaultId="DataItem4" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn Name="Geändert von">
|
||||
<Dimension DefaultId="DataItem5" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn Name="Geändert am">
|
||||
<Dimension DefaultId="DataItem6" />
|
||||
</GridDimensionColumn>
|
||||
</GridColumns>
|
||||
<GridOptions />
|
||||
<ColumnFilterOptions />
|
||||
</Grid>
|
||||
</Items>
|
||||
<LayoutTree>
|
||||
<LayoutGroup Orientation="Vertical">
|
||||
<LayoutItem DashboardItem="gridDashboardItem1" />
|
||||
</LayoutGroup>
|
||||
</LayoutTree>
|
||||
</Dashboard>
|
||||
@@ -1,4 +1,103 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Dashboard CurrencyCulture="de-DE" RequestParameters="false">
|
||||
<Title Text="Default Dashboard" />
|
||||
<DataSources>
|
||||
<JsonDataSource Name="JSON Data Source (URL)" RootElement="Customers" ComponentName="jsonDataSource1">
|
||||
<Source SourceType="DevExpress.DataAccess.Json.UriJsonSource" Uri="https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json" />
|
||||
</JsonDataSource>
|
||||
<JsonDataSource Name="Catalogs (API)" ComponentName="catalogsDataSource">
|
||||
<Source SourceType="DevExpress.DataAccess.Json.UriJsonSource" Uri="https://localhost:7204/api/catalogs" />
|
||||
</JsonDataSource>
|
||||
</DataSources>
|
||||
<Items>
|
||||
<Grid ComponentName="gridDashboardItem1" Name="Grid 1" DataSource="jsonDataSource1">
|
||||
<DataItems>
|
||||
<Dimension DataMember="Address" DefaultId="DataItem0" />
|
||||
<Dimension DataMember="City" DefaultId="DataItem1" />
|
||||
<Dimension DataMember="CompanyName" DefaultId="DataItem2" />
|
||||
</DataItems>
|
||||
<GridColumns>
|
||||
<GridDimensionColumn>
|
||||
<Dimension DefaultId="DataItem0" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn>
|
||||
<Dimension DefaultId="DataItem1" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn>
|
||||
<Dimension DefaultId="DataItem2" />
|
||||
</GridDimensionColumn>
|
||||
</GridColumns>
|
||||
<GridOptions />
|
||||
<ColumnFilterOptions />
|
||||
</Grid>
|
||||
<Chart ComponentName="chartDashboardItem1" Name="Chart 1" DataSource="jsonDataSource1">
|
||||
<DataItems>
|
||||
<Measure DataMember="Address" SummaryType="Count" DefaultId="DataItem0" />
|
||||
<Measure DataMember="City" SummaryType="Count" DefaultId="DataItem1" />
|
||||
<Measure DataMember="CompanyName" SummaryType="Count" DefaultId="DataItem2" />
|
||||
</DataItems>
|
||||
<Panes>
|
||||
<Pane Name="Pane 1">
|
||||
<Series>
|
||||
<Simple>
|
||||
<Value DefaultId="DataItem0" />
|
||||
</Simple>
|
||||
<Simple>
|
||||
<Value DefaultId="DataItem1" />
|
||||
</Simple>
|
||||
<Simple>
|
||||
<Value DefaultId="DataItem2" />
|
||||
</Simple>
|
||||
</Series>
|
||||
</Pane>
|
||||
</Panes>
|
||||
</Chart>
|
||||
<Grid ComponentName="gridDashboardItem2" Name="Grid 2" DataSource="catalogsDataSource">
|
||||
<DataItems>
|
||||
<Measure DataMember="guid" DefaultId="DataItem0" />
|
||||
<Dimension DataMember="catTitle" DefaultId="DataItem1" />
|
||||
<Dimension DataMember="catString" DefaultId="DataItem2" />
|
||||
<Dimension DataMember="addedWhen" DefaultId="DataItem3" />
|
||||
<Dimension DataMember="addedWho" DefaultId="DataItem4" />
|
||||
<Dimension DataMember="changedWhen" DefaultId="DataItem5" />
|
||||
<Dimension DataMember="changedWho" DefaultId="DataItem6" />
|
||||
</DataItems>
|
||||
<GridColumns>
|
||||
<GridMeasureColumn>
|
||||
<Measure DefaultId="DataItem0" />
|
||||
</GridMeasureColumn>
|
||||
<GridDimensionColumn>
|
||||
<Dimension DefaultId="DataItem1" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn>
|
||||
<Dimension DefaultId="DataItem2" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn>
|
||||
<Dimension DefaultId="DataItem3" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn>
|
||||
<Dimension DefaultId="DataItem4" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn>
|
||||
<Dimension DefaultId="DataItem5" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn>
|
||||
<Dimension DefaultId="DataItem6" />
|
||||
</GridDimensionColumn>
|
||||
</GridColumns>
|
||||
<GridOptions />
|
||||
<ColumnFilterOptions />
|
||||
</Grid>
|
||||
</Items>
|
||||
<LayoutTree>
|
||||
<LayoutGroup>
|
||||
<LayoutGroup Orientation="Vertical">
|
||||
<LayoutGroup>
|
||||
<LayoutItem DashboardItem="gridDashboardItem1" />
|
||||
<LayoutItem DashboardItem="gridDashboardItem2" />
|
||||
</LayoutGroup>
|
||||
<LayoutItem DashboardItem="chartDashboardItem1" />
|
||||
</LayoutGroup>
|
||||
</LayoutGroup>
|
||||
</LayoutTree>
|
||||
</Dashboard>
|
||||
@@ -2,6 +2,7 @@ using DbFirst.API.Middleware;
|
||||
using DbFirst.Application;
|
||||
using DbFirst.Application.Repositories;
|
||||
using DbFirst.Domain;
|
||||
using DbFirst.Domain.Entities;
|
||||
using DbFirst.Infrastructure;
|
||||
using DbFirst.Infrastructure.Repositories;
|
||||
using DevExpress.AspNetCore;
|
||||
@@ -62,16 +63,68 @@ builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvi
|
||||
defaultDashboard.SaveToXml(defaultDashboardPath);
|
||||
}
|
||||
|
||||
var dashboardBaseUrl = builder.Configuration["Dashboard:BaseUrl"]
|
||||
?? builder.Configuration["ApiBaseUrl"]
|
||||
?? builder.Configuration["ASPNETCORE_URLS"]?.Split(';', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()
|
||||
?? "https://localhost:7204";
|
||||
|
||||
dashboardBaseUrl = dashboardBaseUrl.TrimEnd('/');
|
||||
|
||||
var catalogsGridDashboardPath = Path.Combine(dashboardsPath, "CatalogsGrid.xml");
|
||||
if (!File.Exists(catalogsGridDashboardPath))
|
||||
{
|
||||
var dashboard = new Dashboard();
|
||||
dashboard.Title.Text = "Catalogs (Dashboard Grid)";
|
||||
|
||||
var catalogDataSource = new DashboardJsonDataSource("Catalogs (API)")
|
||||
{
|
||||
ComponentName = "catalogsDataSource",
|
||||
JsonSource = new UriJsonSource(new Uri($"{dashboardBaseUrl}/api/catalogs"))
|
||||
};
|
||||
|
||||
dashboard.DataSources.Add(catalogDataSource);
|
||||
|
||||
var grid = new GridDashboardItem
|
||||
{
|
||||
DataSource = catalogDataSource,
|
||||
Name = "Catalogs"
|
||||
};
|
||||
|
||||
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.Guid))) { Name = "Id" });
|
||||
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.CatTitle))) { Name = "Titel" });
|
||||
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.CatString))) { Name = "String" });
|
||||
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.AddedWho))) { Name = "Angelegt von" });
|
||||
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.AddedWhen))) { Name = "Angelegt am" });
|
||||
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.ChangedWho))) { Name = "Geändert von" });
|
||||
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.ChangedWhen))) { Name = "Geändert am" });
|
||||
|
||||
dashboard.Items.Add(grid);
|
||||
|
||||
var layoutGroup = new DashboardLayoutGroup { Orientation = DashboardLayoutGroupOrientation.Vertical };
|
||||
layoutGroup.ChildNodes.Add(new DashboardLayoutItem(grid));
|
||||
dashboard.LayoutRoot = layoutGroup;
|
||||
|
||||
dashboard.SaveToXml(catalogsGridDashboardPath);
|
||||
}
|
||||
|
||||
DashboardConfigurator configurator = new DashboardConfigurator();
|
||||
// Register Dashboard Storage
|
||||
configurator.SetDashboardStorage(new DashboardFileStorage(dashboardsPath));
|
||||
// Create a sample JSON data source
|
||||
|
||||
DataSourceInMemoryStorage dataSourceStorage = new DataSourceInMemoryStorage();
|
||||
DashboardJsonDataSource jsonDataSourceUrl = new DashboardJsonDataSource("JSON Data Source (URL)");
|
||||
jsonDataSourceUrl.JsonSource = new UriJsonSource(
|
||||
new Uri("https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json"));
|
||||
jsonDataSourceUrl.RootElement = "Customers";
|
||||
dataSourceStorage.RegisterDataSource("jsonDataSourceUrl", jsonDataSourceUrl.SaveToXml());
|
||||
|
||||
var catalogsJsonDataSource = new DashboardJsonDataSource("Catalogs (API)")
|
||||
{
|
||||
ComponentName = "catalogsDataSource",
|
||||
JsonSource = new UriJsonSource(new Uri($"{dashboardBaseUrl}/api/catalogs"))
|
||||
};
|
||||
dataSourceStorage.RegisterDataSource(catalogsJsonDataSource.ComponentName, catalogsJsonDataSource.SaveToXml());
|
||||
dataSourceStorage.RegisterDataSource(catalogsJsonDataSource.Name, catalogsJsonDataSource.SaveToXml());
|
||||
|
||||
configurator.SetDataSourceStorage(dataSourceStorage);
|
||||
return configurator;
|
||||
});
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;"
|
||||
},
|
||||
"Dashboard": {
|
||||
"BaseUrl": "https://localhost:7204"
|
||||
},
|
||||
"Cors": {
|
||||
"AllowedOrigins": [
|
||||
"https://localhost:7276",
|
||||
|
||||
286
DbFirst.BlazorWasm/Components/CatalogsGrid.razor
Normal file
286
DbFirst.BlazorWasm/Components/CatalogsGrid.razor
Normal file
@@ -0,0 +1,286 @@
|
||||
@inject CatalogApiClient Api
|
||||
|
||||
<style>
|
||||
.action-panel { margin-bottom: 16px; }
|
||||
.grid-section { margin-top: 12px; }
|
||||
.catalog-grid th.dxbl-grid-header-sortable {
|
||||
position: relative;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
.catalog-grid th.dxbl-grid-header-sortable:not(:has(.dxbl-grid-sort-asc)):not(:has(.dxbl-grid-sort-desc))::before,
|
||||
.catalog-grid th.dxbl-grid-header-sortable:not(:has(.dxbl-grid-sort-asc)):not(:has(.dxbl-grid-sort-desc))::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0.45rem;
|
||||
width: 0.7rem;
|
||||
height: 0.7rem;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 0.7rem 0.7rem;
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
}
|
||||
.catalog-grid th.dxbl-grid-header-sortable:not(:has(.dxbl-grid-sort-asc)):not(:has(.dxbl-grid-sort-desc))::before {
|
||||
top: 38%;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.957 10.999a1 1 0 0 1-.821-1.571l2.633-3.785a1.5 1.5 0 0 1 2.462 0l2.633 3.785a1 1 0 0 1-.821 1.57H4.957Z' fill='%23888888'/%3E%3C/svg%3E");
|
||||
}
|
||||
.catalog-grid th.dxbl-grid-header-sortable:not(:has(.dxbl-grid-sort-asc)):not(:has(.dxbl-grid-sort-desc))::after {
|
||||
top: 58%;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.957 5a1 1 0 0 0-.821 1.571l2.633 3.784a1.5 1.5 0 0 0 2.462 0l2.633-3.784A1 1 0 0 0 11.043 5H4.957Z' fill='%23888888'/%3E%3C/svg%3E");
|
||||
}
|
||||
.catalog-grid .filter-search-input input {
|
||||
padding-right: 1.75rem;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M9.309 10.016a4.5 4.5 0 1 1 .707-.707l3.838 3.837a.5.5 0 0 1-.708.708L9.31 10.016ZM10 6.5a3.5 3.5 0 1 0-7 0 3.5 3.5 0 0 0 7 0Z' fill='%23666666'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.5rem center;
|
||||
background-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">@errorMessage</div>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(infoMessage))
|
||||
{
|
||||
<div class="alert alert-success" role="alert">@infoMessage</div>
|
||||
}
|
||||
|
||||
<div class="mb-3">
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Primary" Click="@StartCreate">Neuen Eintrag anlegen</DxButton>
|
||||
</div>
|
||||
|
||||
@if (showForm)
|
||||
{
|
||||
<div class="action-panel">
|
||||
<EditForm Model="formModel" OnValidSubmit="HandleSubmit" Context="editCtx">
|
||||
<DxFormLayout ColCount="2">
|
||||
<DxFormLayoutItem Caption="Titel" Context="itemCtx">
|
||||
<DxTextBox @bind-Text="formModel.CatTitle" Enabled="@(isEditing ? formModel.UpdateProcedure != 0 : true)" />
|
||||
</DxFormLayoutItem>
|
||||
<DxFormLayoutItem Caption="Kennung" Context="itemCtx">
|
||||
<DxTextBox @bind-Text="formModel.CatString" />
|
||||
</DxFormLayoutItem>
|
||||
@if (isEditing)
|
||||
{
|
||||
<DxFormLayoutItem Caption="Update-Prozedur" Context="itemCtx">
|
||||
<DxComboBox Data="@procedureOptions"
|
||||
TextFieldName="Text"
|
||||
ValueFieldName="Value"
|
||||
@bind-Value="formModel.UpdateProcedure" />
|
||||
</DxFormLayoutItem>
|
||||
}
|
||||
<DxFormLayoutItem Caption=" " Context="itemCtx">
|
||||
<DxStack Orientation="Orientation.Horizontal" Spacing="8">
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Success" ButtonType="ButtonType.Submit" SubmitFormOnClick="true" Context="btnCtx">@((isEditing ? "Speichern" : "Anlegen"))</DxButton>
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Click="@CancelEdit" Context="btnCtx">Abbrechen</DxButton>
|
||||
</DxStack>
|
||||
</DxFormLayoutItem>
|
||||
</DxFormLayout>
|
||||
</EditForm>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isLoading)
|
||||
{
|
||||
<p><em>Lade Daten...</em></p>
|
||||
}
|
||||
else if (items.Count == 0)
|
||||
{
|
||||
<p>Keine Einträge vorhanden.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid-section">
|
||||
<DxGrid Data="@items" TItem="CatalogReadDto" KeyFieldName="@nameof(CatalogReadDto.Guid)" ShowFilterRow="true" PageSize="10" CssClass="mb-4 catalog-grid">
|
||||
<Columns>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.Guid)" Caption="Id" Width="140px" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatTitle)" Caption="Titel">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatString)" Caption="String">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" />
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" />
|
||||
<DxGridDataColumn Caption="" Width="220px" AllowSort="false">
|
||||
<CellDisplayTemplate Context="cell">
|
||||
@{ var item = (CatalogReadDto)cell.DataItem; }
|
||||
<div style="white-space: nowrap;">
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Size="ButtonSize.Small" Click="@(() => StartEdit(item))">Bearbeiten</DxButton>
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Danger" Size="ButtonSize.Small" Click="@(() => DeleteCatalog(item.Guid))">Löschen</DxButton>
|
||||
</div>
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
</Columns>
|
||||
</DxGrid>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<CatalogReadDto> items = new();
|
||||
private CatalogWriteDto formModel = new();
|
||||
private int editingId;
|
||||
private bool isLoading;
|
||||
private bool isEditing;
|
||||
private bool showForm;
|
||||
private string? errorMessage;
|
||||
private string? infoMessage;
|
||||
|
||||
private readonly List<ProcedureOption> procedureOptions = new()
|
||||
{
|
||||
new() { Value = 0, Text = "PRTBMY_CATALOG_UPDATE" },
|
||||
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCatalogs();
|
||||
}
|
||||
|
||||
private async Task LoadCatalogs()
|
||||
{
|
||||
isLoading = true;
|
||||
errorMessage = null;
|
||||
try
|
||||
{
|
||||
items = await Api.GetAllAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Kataloge konnten nicht geladen werden: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartCreate()
|
||||
{
|
||||
formModel = new CatalogWriteDto();
|
||||
editingId = 0;
|
||||
isEditing = false;
|
||||
showForm = true;
|
||||
infoMessage = null;
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
private void StartEdit(CatalogReadDto item)
|
||||
{
|
||||
formModel = new CatalogWriteDto
|
||||
{
|
||||
CatTitle = item.CatTitle,
|
||||
CatString = item.CatString,
|
||||
UpdateProcedure = 0
|
||||
};
|
||||
editingId = item.Guid;
|
||||
isEditing = true;
|
||||
showForm = true;
|
||||
infoMessage = null;
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
errorMessage = null;
|
||||
infoMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (isEditing)
|
||||
{
|
||||
var updated = await Api.UpdateAsync(editingId, formModel);
|
||||
if (!updated.Success)
|
||||
{
|
||||
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog aktualisiert.";
|
||||
}
|
||||
else
|
||||
{
|
||||
var created = await Api.CreateAsync(formModel);
|
||||
if (!created.Success || created.Value == null)
|
||||
{
|
||||
errorMessage = created.Error ?? "Anlegen fehlgeschlagen.";
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog angelegt.";
|
||||
}
|
||||
|
||||
showForm = false;
|
||||
await LoadCatalogs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Speichern: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelEdit()
|
||||
{
|
||||
showForm = false;
|
||||
infoMessage = null;
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
private async Task DeleteCatalog(int id)
|
||||
{
|
||||
errorMessage = null;
|
||||
infoMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
var deleted = await Api.DeleteAsync(id);
|
||||
if (!deleted.Success)
|
||||
{
|
||||
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog gelöscht.";
|
||||
await LoadCatalogs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Löschen: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ProcedureOption
|
||||
{
|
||||
public int Value { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="dashboard">
|
||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Web Dashboard
|
||||
<NavLink class="nav-link" href="dashboards/default">
|
||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Dashboards
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,303 +1,7 @@
|
||||
@* Stellt die Catalog-Verwaltung bereit.
|
||||
Nutzt CatalogApiClient für API-Interaktionen und DevExpress-Komponenten für die Benutzeroberfläche. *@
|
||||
|
||||
@page "/catalogs"
|
||||
@inject CatalogApiClient Api
|
||||
|
||||
<style>
|
||||
.action-panel { margin-bottom: 16px; }
|
||||
.grid-section { margin-top: 12px; }
|
||||
.catalog-grid th.dxbl-grid-header-sortable {
|
||||
position: relative;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
.catalog-grid th.dxbl-grid-header-sortable:not(:has(.dxbl-grid-sort-asc)):not(:has(.dxbl-grid-sort-desc))::before,
|
||||
.catalog-grid th.dxbl-grid-header-sortable:not(:has(.dxbl-grid-sort-asc)):not(:has(.dxbl-grid-sort-desc))::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0.45rem;
|
||||
width: 0.7rem;
|
||||
height: 0.7rem;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 0.7rem 0.7rem;
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
}
|
||||
.catalog-grid th.dxbl-grid-header-sortable:not(:has(.dxbl-grid-sort-asc)):not(:has(.dxbl-grid-sort-desc))::before {
|
||||
top: 38%;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.957 10.999a1 1 0 0 1-.821-1.571l2.633-3.785a1.5 1.5 0 0 1 2.462 0l2.633 3.785a1 1 0 0 1-.821 1.57H4.957Z' fill='%23888888'/%3E%3C/svg%3E");
|
||||
}
|
||||
.catalog-grid th.dxbl-grid-header-sortable:not(:has(.dxbl-grid-sort-asc)):not(:has(.dxbl-grid-sort-desc))::after {
|
||||
top: 58%;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.957 5a1 1 0 0 0-.821 1.571l2.633 3.784a1.5 1.5 0 0 0 2.462 0l2.633-3.784A1 1 0 0 0 11.043 5H4.957Z' fill='%23888888'/%3E%3C/svg%3E");
|
||||
}
|
||||
.catalog-grid .filter-search-input input {
|
||||
padding-right: 1.75rem;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M9.309 10.016a4.5 4.5 0 1 1 .707-.707l3.838 3.837a.5.5 0 0 1-.708.708L9.31 10.016ZM10 6.5a3.5 3.5 0 1 0-7 0 3.5 3.5 0 0 0 7 0Z' fill='%23666666'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.5rem center;
|
||||
background-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<PageTitle>Catalogs</PageTitle>
|
||||
|
||||
<h1>Catalogs</h1>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">@errorMessage</div>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(infoMessage))
|
||||
{
|
||||
<div class="alert alert-success" role="alert">@infoMessage</div>
|
||||
}
|
||||
|
||||
<div class="mb-3">
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Primary" Click="@StartCreate">Neuen Eintrag anlegen</DxButton>
|
||||
</div>
|
||||
|
||||
@if (showForm)
|
||||
{
|
||||
<div class="action-panel">
|
||||
<EditForm Model="formModel" OnValidSubmit="HandleSubmit" Context="editCtx">
|
||||
<DxFormLayout ColCount="2">
|
||||
<DxFormLayoutItem Caption="Titel" Context="itemCtx">
|
||||
<DxTextBox @bind-Text="formModel.CatTitle" Enabled="@(isEditing ? formModel.UpdateProcedure != 0 : true)" />
|
||||
</DxFormLayoutItem>
|
||||
<DxFormLayoutItem Caption="Kennung" Context="itemCtx">
|
||||
<DxTextBox @bind-Text="formModel.CatString" />
|
||||
</DxFormLayoutItem>
|
||||
@if (isEditing)
|
||||
{
|
||||
<DxFormLayoutItem Caption="Update-Prozedur" Context="itemCtx">
|
||||
<DxComboBox Data="@procedureOptions"
|
||||
TextFieldName="Text"
|
||||
ValueFieldName="Value"
|
||||
@bind-Value="formModel.UpdateProcedure" />
|
||||
</DxFormLayoutItem>
|
||||
}
|
||||
<DxFormLayoutItem Caption=" " Context="itemCtx">
|
||||
<DxStack Orientation="Orientation.Horizontal" Spacing="8">
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Success" ButtonType="ButtonType.Submit" SubmitFormOnClick="true" Context="btnCtx">@((isEditing ? "Speichern" : "Anlegen"))</DxButton>
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Click="@CancelEdit" Context="btnCtx">Abbrechen</DxButton>
|
||||
</DxStack>
|
||||
</DxFormLayoutItem>
|
||||
</DxFormLayout>
|
||||
</EditForm>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isLoading)
|
||||
{
|
||||
<p><em>Lade Daten...</em></p>
|
||||
}
|
||||
else if (items.Count == 0)
|
||||
{
|
||||
<p>Keine Einträge vorhanden.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid-section">
|
||||
<DxGrid Data="@items" TItem="CatalogReadDto" KeyFieldName="@nameof(CatalogReadDto.Guid)" ShowFilterRow="true" PageSize="10" CssClass="mb-4 catalog-grid">
|
||||
<Columns>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.Guid)" Caption="Id" Width="140px" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatTitle)" Caption="Titel">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatString)" Caption="String">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" />
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" />
|
||||
<DxGridDataColumn Caption="" Width="220px" AllowSort="false">
|
||||
<CellDisplayTemplate Context="cell">
|
||||
@{ var item = (CatalogReadDto)cell.DataItem; }
|
||||
<div style="white-space: nowrap;">
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Size="ButtonSize.Small" Click="@(() => StartEdit(item))">Bearbeiten</DxButton>
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Danger" Size="ButtonSize.Small" Click="@(() => DeleteCatalog(item.Guid))">Löschen</DxButton>
|
||||
</div>
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
</Columns>
|
||||
</DxGrid>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<CatalogReadDto> items = new();
|
||||
private CatalogWriteDto formModel = new();
|
||||
private int editingId;
|
||||
private bool isLoading;
|
||||
private bool isEditing;
|
||||
private bool showForm;
|
||||
private string? errorMessage;
|
||||
private string? infoMessage;
|
||||
|
||||
private readonly List<ProcedureOption> procedureOptions = new()
|
||||
{
|
||||
new() { Value = 0, Text = "PRTBMY_CATALOG_UPDATE" },
|
||||
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCatalogs();
|
||||
}
|
||||
|
||||
private async Task LoadCatalogs()
|
||||
{
|
||||
// Lädt die Liste der Kataloge aus der API.
|
||||
// Setzt Ladezustand und behandelt Fehler.
|
||||
|
||||
isLoading = true;
|
||||
errorMessage = null;
|
||||
try
|
||||
{
|
||||
items = await Api.GetAllAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Kataloge konnten nicht geladen werden: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartCreate()
|
||||
{
|
||||
formModel = new CatalogWriteDto();
|
||||
editingId = 0;
|
||||
isEditing = false;
|
||||
showForm = true;
|
||||
infoMessage = null;
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
private void StartEdit(CatalogReadDto item)
|
||||
{
|
||||
formModel = new CatalogWriteDto
|
||||
{
|
||||
CatTitle = item.CatTitle,
|
||||
CatString = item.CatString,
|
||||
UpdateProcedure = 0
|
||||
};
|
||||
editingId = item.Guid;
|
||||
isEditing = true;
|
||||
showForm = true;
|
||||
infoMessage = null;
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
// Behandelt das Absenden des Formulars.
|
||||
// Führt entweder eine Aktualisierung oder das Anlegen eines neuen Eintrags durch.
|
||||
|
||||
errorMessage = null;
|
||||
infoMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (isEditing)
|
||||
{
|
||||
var updated = await Api.UpdateAsync(editingId, formModel);
|
||||
if (!updated.Success)
|
||||
{
|
||||
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog aktualisiert.";
|
||||
}
|
||||
else
|
||||
{
|
||||
var created = await Api.CreateAsync(formModel);
|
||||
if (!created.Success || created.Value == null)
|
||||
{
|
||||
errorMessage = created.Error ?? "Anlegen fehlgeschlagen.";
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog angelegt.";
|
||||
}
|
||||
|
||||
showForm = false;
|
||||
await LoadCatalogs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Speichern: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelEdit()
|
||||
{
|
||||
showForm = false;
|
||||
infoMessage = null;
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
private async Task DeleteCatalog(int id)
|
||||
{
|
||||
// Löscht einen Katalogeintrag basierend auf der ID.
|
||||
// Aktualisiert die Liste nach erfolgreichem Löschen.
|
||||
|
||||
errorMessage = null;
|
||||
infoMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
var deleted = await Api.DeleteAsync(id);
|
||||
if (!deleted.Success)
|
||||
{
|
||||
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog gelöscht.";
|
||||
await LoadCatalogs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Löschen: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ProcedureOption
|
||||
{
|
||||
public int Value { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
<CatalogsGrid />
|
||||
|
||||
@@ -1,12 +1,87 @@
|
||||
@page "/dashboard"
|
||||
@page "/dashboards/{DashboardId?}"
|
||||
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<DxDashboard Endpoint="@DashboardEndpoint" style="width: 100%; height: 800px;">
|
||||
</DxDashboard>
|
||||
<style>
|
||||
.dashboard-shell {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
min-height: 800px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
.dashboard-nav {
|
||||
width: 220px;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
background: #fafafa;
|
||||
}
|
||||
.dashboard-nav-title {
|
||||
padding: 0.75rem 1rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dashboard-nav-link {
|
||||
display: block;
|
||||
padding: 0.55rem 1rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.dashboard-nav-link.active {
|
||||
background: #e9ecef;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dashboard-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<PageTitle>Dashboards</PageTitle>
|
||||
|
||||
<div class="dashboard-shell">
|
||||
<aside class="dashboard-nav">
|
||||
<div class="dashboard-nav-title">Dashboards</div>
|
||||
<NavLink class="dashboard-nav-link" href="dashboards/default">Default Dashboard (Designer)</NavLink>
|
||||
<NavLink class="dashboard-nav-link" href="dashboards/catalog-grid">Catalogs (Dashboard Grid)</NavLink>
|
||||
<NavLink class="dashboard-nav-link" href="dashboards/custom-grid">Catalogs (Custom Grid)</NavLink>
|
||||
</aside>
|
||||
<section class="dashboard-content">
|
||||
@if (SelectedDashboardId == "default")
|
||||
{
|
||||
<DxDashboard Endpoint="@DashboardEndpoint" InitialDashboardId="DefaultDashboard" WorkingMode="WorkingMode.Designer" style="width: 100%; height: 800px;">
|
||||
</DxDashboard>
|
||||
}
|
||||
else if (SelectedDashboardId == "catalog-grid")
|
||||
{
|
||||
<DxDashboard Endpoint="@DashboardEndpoint" InitialDashboardId="CatalogsGrid" WorkingMode="WorkingMode.ViewerOnly" style="width: 100%; height: 800px;">
|
||||
</DxDashboard>
|
||||
}
|
||||
else if (SelectedDashboardId == "custom-grid")
|
||||
{
|
||||
<h3>Catalogs (Custom Grid)</h3>
|
||||
<CatalogsGrid />
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
|
||||
}
|
||||
[Parameter] public string? DashboardId { get; set; }
|
||||
|
||||
@* <DxDashboard Endpoint="api/dashboard" WorkingMode="WorkingMode.ViewerOnly" style="width: 100%; height: 800px;">
|
||||
</DxDashboard> *@
|
||||
private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
|
||||
private string SelectedDashboardId => string.IsNullOrWhiteSpace(DashboardId) ? "default" : DashboardId;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(DashboardId))
|
||||
{
|
||||
Navigation.NavigateTo("dashboards/default", replace: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
@using DbFirst.BlazorWasm.Layout
|
||||
@using DbFirst.BlazorWasm.Models
|
||||
@using DbFirst.BlazorWasm.Services
|
||||
@using DbFirst.BlazorWasm.Components
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.DashboardBlazor
|
||||
@using DevExpress.DashboardWeb
|
||||
286
DbFirst.BlazorWebApp/Components/CatalogsGrid.razor
Normal file
286
DbFirst.BlazorWebApp/Components/CatalogsGrid.razor
Normal file
@@ -0,0 +1,286 @@
|
||||
@inject CatalogApiClient Api
|
||||
|
||||
<style>
|
||||
.action-panel { margin-bottom: 16px; }
|
||||
.grid-section { margin-top: 12px; }
|
||||
.catalog-grid th.dxbl-grid-header-sortable {
|
||||
position: relative;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
.catalog-grid th.dxbl-grid-header-sortable:not(:has(.dxbl-grid-sort-asc)):not(:has(.dxbl-grid-sort-desc))::before,
|
||||
.catalog-grid th.dxbl-grid-header-sortable:not(:has(.dxbl-grid-sort-asc)):not(:has(.dxbl-grid-sort-desc))::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0.45rem;
|
||||
width: 0.7rem;
|
||||
height: 0.7rem;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 0.7rem 0.7rem;
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
}
|
||||
.catalog-grid th.dxbl-grid-header-sortable:not(:has(.dxbl-grid-sort-asc)):not(:has(.dxbl-grid-sort-desc))::before {
|
||||
top: 38%;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.957 10.999a1 1 0 0 1-.821-1.571l2.633-3.785a1.5 1.5 0 0 1 2.462 0l2.633 3.785a1 1 0 0 1-.821 1.57H4.957Z' fill='%23888888'/%3E%3C/svg%3E");
|
||||
}
|
||||
.catalog-grid th.dxbl-grid-header-sortable:not(:has(.dxbl-grid-sort-asc)):not(:has(.dxbl-grid-sort-desc))::after {
|
||||
top: 58%;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.957 5a1 1 0 0 0-.821 1.571l2.633 3.784a1.5 1.5 0 0 0 2.462 0l2.633-3.784A1 1 0 0 0 11.043 5H4.957Z' fill='%23888888'/%3E%3C/svg%3E");
|
||||
}
|
||||
.catalog-grid .filter-search-input input {
|
||||
padding-right: 1.75rem;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M9.309 10.016a4.5 4.5 0 1 1 .707-.707l3.838 3.837a.5.5 0 0 1-.708.708L9.31 10.016ZM10 6.5a3.5 3.5 0 1 0-7 0 3.5 3.5 0 0 0 7 0Z' fill='%23666666'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.5rem center;
|
||||
background-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">@errorMessage</div>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(infoMessage))
|
||||
{
|
||||
<div class="alert alert-success" role="alert">@infoMessage</div>
|
||||
}
|
||||
|
||||
<div class="mb-3">
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Primary" Click="@StartCreate">Neuen Eintrag anlegen</DxButton>
|
||||
</div>
|
||||
|
||||
@if (showForm)
|
||||
{
|
||||
<div class="action-panel">
|
||||
<EditForm Model="formModel" OnValidSubmit="HandleSubmit" Context="editCtx">
|
||||
<DxFormLayout ColCount="2">
|
||||
<DxFormLayoutItem Caption="Titel" Context="itemCtx">
|
||||
<DxTextBox @bind-Text="formModel.CatTitle" Enabled="@(isEditing ? formModel.UpdateProcedure != 0 : true)" />
|
||||
</DxFormLayoutItem>
|
||||
<DxFormLayoutItem Caption="Kennung" Context="itemCtx">
|
||||
<DxTextBox @bind-Text="formModel.CatString" />
|
||||
</DxFormLayoutItem>
|
||||
@if (isEditing)
|
||||
{
|
||||
<DxFormLayoutItem Caption="Update-Prozedur" Context="itemCtx">
|
||||
<DxComboBox Data="@procedureOptions"
|
||||
TextFieldName="Text"
|
||||
ValueFieldName="Value"
|
||||
@bind-Value="formModel.UpdateProcedure" />
|
||||
</DxFormLayoutItem>
|
||||
}
|
||||
<DxFormLayoutItem Caption=" " Context="itemCtx">
|
||||
<DxStack Orientation="Orientation.Horizontal" Spacing="8">
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Success" ButtonType="ButtonType.Submit" SubmitFormOnClick="true" Context="btnCtx">@((isEditing ? "Speichern" : "Anlegen"))</DxButton>
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Click="@CancelEdit" Context="btnCtx">Abbrechen</DxButton>
|
||||
</DxStack>
|
||||
</DxFormLayoutItem>
|
||||
</DxFormLayout>
|
||||
</EditForm>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isLoading)
|
||||
{
|
||||
<p><em>Lade Daten...</em></p>
|
||||
}
|
||||
else if (items.Count == 0)
|
||||
{
|
||||
<p>Keine Einträge vorhanden.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid-section">
|
||||
<DxGrid Data="@items" TItem="CatalogReadDto" KeyFieldName="@nameof(CatalogReadDto.Guid)" ShowFilterRow="true" PageSize="10" CssClass="mb-4 catalog-grid">
|
||||
<Columns>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.Guid)" Caption="Id" Width="140px" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatTitle)" Caption="Titel">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatString)" Caption="String">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" />
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" />
|
||||
<DxGridDataColumn Caption="" Width="220px" AllowSort="false">
|
||||
<CellDisplayTemplate Context="cell">
|
||||
@{ var item = (CatalogReadDto)cell.DataItem; }
|
||||
<div style="white-space: nowrap;">
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Size="ButtonSize.Small" Click="@(() => StartEdit(item))">Bearbeiten</DxButton>
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Danger" Size="ButtonSize.Small" Click="@(() => DeleteCatalog(item.Guid))">Löschen</DxButton>
|
||||
</div>
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
</Columns>
|
||||
</DxGrid>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<CatalogReadDto> items = new();
|
||||
private CatalogWriteDto formModel = new();
|
||||
private int editingId;
|
||||
private bool isLoading;
|
||||
private bool isEditing;
|
||||
private bool showForm;
|
||||
private string? errorMessage;
|
||||
private string? infoMessage;
|
||||
|
||||
private readonly List<ProcedureOption> procedureOptions = new()
|
||||
{
|
||||
new() { Value = 0, Text = "PRTBMY_CATALOG_UPDATE" },
|
||||
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCatalogs();
|
||||
}
|
||||
|
||||
private async Task LoadCatalogs()
|
||||
{
|
||||
isLoading = true;
|
||||
errorMessage = null;
|
||||
try
|
||||
{
|
||||
items = await Api.GetAllAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Kataloge konnten nicht geladen werden: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartCreate()
|
||||
{
|
||||
formModel = new CatalogWriteDto();
|
||||
editingId = 0;
|
||||
isEditing = false;
|
||||
showForm = true;
|
||||
infoMessage = null;
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
private void StartEdit(CatalogReadDto item)
|
||||
{
|
||||
formModel = new CatalogWriteDto
|
||||
{
|
||||
CatTitle = item.CatTitle,
|
||||
CatString = item.CatString,
|
||||
UpdateProcedure = 0
|
||||
};
|
||||
editingId = item.Guid;
|
||||
isEditing = true;
|
||||
showForm = true;
|
||||
infoMessage = null;
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
errorMessage = null;
|
||||
infoMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (isEditing)
|
||||
{
|
||||
var updated = await Api.UpdateAsync(editingId, formModel);
|
||||
if (!updated.Success)
|
||||
{
|
||||
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog aktualisiert.";
|
||||
}
|
||||
else
|
||||
{
|
||||
var created = await Api.CreateAsync(formModel);
|
||||
if (!created.Success || created.Value == null)
|
||||
{
|
||||
errorMessage = created.Error ?? "Anlegen fehlgeschlagen.";
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog angelegt.";
|
||||
}
|
||||
|
||||
showForm = false;
|
||||
await LoadCatalogs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Speichern: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelEdit()
|
||||
{
|
||||
showForm = false;
|
||||
infoMessage = null;
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
private async Task DeleteCatalog(int id)
|
||||
{
|
||||
errorMessage = null;
|
||||
infoMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
var deleted = await Api.DeleteAsync(id);
|
||||
if (!deleted.Success)
|
||||
{
|
||||
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog gelöscht.";
|
||||
await LoadCatalogs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Löschen: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ProcedureOption
|
||||
{
|
||||
public int Value { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,8 @@
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="dashboard">
|
||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Web Dashboard
|
||||
<NavLink class="nav-link" href="dashboards/default">
|
||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Dashboards
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,12 +1,87 @@
|
||||
@page "/dashboard"
|
||||
@page "/dashboards/{DashboardId?}"
|
||||
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<DxDashboard Endpoint="@DashboardEndpoint" style="width: 100%; height: 800px;">
|
||||
</DxDashboard>
|
||||
<style>
|
||||
.dashboard-shell {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
min-height: 800px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
.dashboard-nav {
|
||||
width: 220px;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
background: #fafafa;
|
||||
}
|
||||
.dashboard-nav-title {
|
||||
padding: 0.75rem 1rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dashboard-nav-link {
|
||||
display: block;
|
||||
padding: 0.55rem 1rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.dashboard-nav-link.active {
|
||||
background: #e9ecef;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dashboard-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<PageTitle>Dashboards</PageTitle>
|
||||
|
||||
<div class="dashboard-shell">
|
||||
<aside class="dashboard-nav">
|
||||
<div class="dashboard-nav-title">Dashboards</div>
|
||||
<NavLink class="dashboard-nav-link" href="dashboards/default">Default Dashboard (Designer)</NavLink>
|
||||
<NavLink class="dashboard-nav-link" href="dashboards/catalog-grid">Catalogs (Dashboard Grid)</NavLink>
|
||||
<NavLink class="dashboard-nav-link" href="dashboards/custom-grid">Catalogs (Custom Grid)</NavLink>
|
||||
</aside>
|
||||
<section class="dashboard-content">
|
||||
@if (SelectedDashboardId == "default")
|
||||
{
|
||||
<DxDashboard Endpoint="@DashboardEndpoint" InitialDashboardId="DefaultDashboard" WorkingMode="WorkingMode.Designer" style="width: 100%; height: 800px;">
|
||||
</DxDashboard>
|
||||
}
|
||||
else if (SelectedDashboardId == "catalog-grid")
|
||||
{
|
||||
<DxDashboard Endpoint="@DashboardEndpoint" InitialDashboardId="CatalogsGrid" WorkingMode="WorkingMode.ViewerOnly" style="width: 100%; height: 800px;">
|
||||
</DxDashboard>
|
||||
}
|
||||
else if (SelectedDashboardId == "custom-grid")
|
||||
{
|
||||
<h3>Catalogs (Custom Grid)</h3>
|
||||
<CatalogsGrid />
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
|
||||
}
|
||||
[Parameter] public string? DashboardId { get; set; }
|
||||
|
||||
@* <DxDashboard Endpoint="api/dashboard" WorkingMode="WorkingMode.ViewerOnly" style="width: 100%; height: 800px;">
|
||||
</DxDashboard> *@
|
||||
private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
|
||||
private string SelectedDashboardId => string.IsNullOrWhiteSpace(DashboardId) ? "default" : DashboardId;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(DashboardId))
|
||||
{
|
||||
Navigation.NavigateTo("dashboards/default", replace: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
@using Microsoft.JSInterop
|
||||
@using DbFirst.BlazorWebApp
|
||||
@using DbFirst.BlazorWebApp.Components
|
||||
@using DbFirst.BlazorWebApp.Models
|
||||
@using DbFirst.BlazorWebApp.Services
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.DashboardBlazor
|
||||
@using DevExpress.DashboardWeb
|
||||
@using DevExpress.DashboardWeb
|
||||
@using DbFirst.BlazorWebApp
|
||||
12
DbFirst.BlazorWebApp/Models/CatalogReadDto.cs
Normal file
12
DbFirst.BlazorWebApp/Models/CatalogReadDto.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace DbFirst.BlazorWebApp.Models;
|
||||
|
||||
public class CatalogReadDto
|
||||
{
|
||||
public int Guid { get; set; }
|
||||
public string CatTitle { get; set; } = null!;
|
||||
public string CatString { get; set; } = null!;
|
||||
public string AddedWho { get; set; } = null!;
|
||||
public DateTime AddedWhen { get; set; }
|
||||
public string? ChangedWho { get; set; }
|
||||
public DateTime? ChangedWhen { get; set; }
|
||||
}
|
||||
8
DbFirst.BlazorWebApp/Models/CatalogWriteDto.cs
Normal file
8
DbFirst.BlazorWebApp/Models/CatalogWriteDto.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace DbFirst.BlazorWebApp.Models;
|
||||
|
||||
public class CatalogWriteDto
|
||||
{
|
||||
public string CatTitle { get; set; } = string.Empty;
|
||||
public string CatString { get; set; } = string.Empty;
|
||||
public int UpdateProcedure { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using DbFirst.BlazorWebApp.Components;
|
||||
using DbFirst.BlazorWebApp.Services;
|
||||
using DevExpress.Blazor;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -9,6 +10,19 @@ builder.Services.AddRazorComponents()
|
||||
|
||||
builder.Services.AddDevExpressBlazor();
|
||||
|
||||
var apiBaseUrl = builder.Configuration["ApiBaseUrl"];
|
||||
if (!string.IsNullOrWhiteSpace(apiBaseUrl))
|
||||
{
|
||||
builder.Services.AddHttpClient<CatalogApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Services.AddHttpClient<CatalogApiClient>();
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
|
||||
136
DbFirst.BlazorWebApp/Services/CatalogApiClient.cs
Normal file
136
DbFirst.BlazorWebApp/Services/CatalogApiClient.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using DbFirst.BlazorWebApp.Models;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.Services;
|
||||
|
||||
public class CatalogApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string Endpoint = "api/catalogs";
|
||||
|
||||
public CatalogApiClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<List<CatalogReadDto>> GetAllAsync()
|
||||
{
|
||||
var result = await _httpClient.GetFromJsonAsync<List<CatalogReadDto>>(Endpoint);
|
||||
return result ?? new List<CatalogReadDto>();
|
||||
}
|
||||
|
||||
public async Task<CatalogReadDto?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _httpClient.GetFromJsonAsync<CatalogReadDto>($"{Endpoint}/{id}");
|
||||
}
|
||||
|
||||
public async Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto)
|
||||
{
|
||||
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 ReadErrorAsync(response);
|
||||
return ApiResult<CatalogReadDto?>.Fail(error);
|
||||
}
|
||||
|
||||
public async Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto)
|
||||
{
|
||||
var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return ApiResult<bool>.Ok(true);
|
||||
}
|
||||
|
||||
var error = await ReadErrorAsync(response);
|
||||
return ApiResult<bool>.Fail(error);
|
||||
}
|
||||
|
||||
public async Task<ApiResult<bool>> DeleteAsync(int id)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}");
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return ApiResult<bool>.Ok(true);
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
Reference in New Issue
Block a user