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:
OlgunR
2026-02-02 17:03:23 +01:00
parent 1667788558
commit 70e5cbc19f
17 changed files with 1123 additions and 316 deletions

View 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>

View File

@@ -1,4 +1,103 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Dashboard CurrencyCulture="de-DE" RequestParameters="false"> <Dashboard CurrencyCulture="de-DE" RequestParameters="false">
<Title Text="Default Dashboard" /> <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> </Dashboard>

View File

@@ -2,6 +2,7 @@ using DbFirst.API.Middleware;
using DbFirst.Application; using DbFirst.Application;
using DbFirst.Application.Repositories; using DbFirst.Application.Repositories;
using DbFirst.Domain; using DbFirst.Domain;
using DbFirst.Domain.Entities;
using DbFirst.Infrastructure; using DbFirst.Infrastructure;
using DbFirst.Infrastructure.Repositories; using DbFirst.Infrastructure.Repositories;
using DevExpress.AspNetCore; using DevExpress.AspNetCore;
@@ -62,16 +63,68 @@ builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvi
defaultDashboard.SaveToXml(defaultDashboardPath); 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(); DashboardConfigurator configurator = new DashboardConfigurator();
// Register Dashboard Storage
configurator.SetDashboardStorage(new DashboardFileStorage(dashboardsPath)); configurator.SetDashboardStorage(new DashboardFileStorage(dashboardsPath));
// Create a sample JSON data source
DataSourceInMemoryStorage dataSourceStorage = new DataSourceInMemoryStorage(); DataSourceInMemoryStorage dataSourceStorage = new DataSourceInMemoryStorage();
DashboardJsonDataSource jsonDataSourceUrl = new DashboardJsonDataSource("JSON Data Source (URL)"); DashboardJsonDataSource jsonDataSourceUrl = new DashboardJsonDataSource("JSON Data Source (URL)");
jsonDataSourceUrl.JsonSource = new UriJsonSource( jsonDataSourceUrl.JsonSource = new UriJsonSource(
new Uri("https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json")); new Uri("https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json"));
jsonDataSourceUrl.RootElement = "Customers"; jsonDataSourceUrl.RootElement = "Customers";
dataSourceStorage.RegisterDataSource("jsonDataSourceUrl", jsonDataSourceUrl.SaveToXml()); 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); configurator.SetDataSourceStorage(dataSourceStorage);
return configurator; return configurator;
}); });

View File

@@ -2,6 +2,9 @@
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;" "DefaultConnection": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;"
}, },
"Dashboard": {
"BaseUrl": "https://localhost:7204"
},
"Cors": { "Cors": {
"AllowedOrigins": [ "AllowedOrigins": [
"https://localhost:7276", "https://localhost:7276",

View 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;
}
}

View File

@@ -22,8 +22,8 @@
</NavLink> </NavLink>
</div> </div>
<div class="nav-item px-3"> <div class="nav-item px-3">
<NavLink class="nav-link" href="dashboard"> <NavLink class="nav-link" href="dashboards/default">
<span class="oi oi-list-rich" aria-hidden="true"></span> Web Dashboard <span class="oi oi-list-rich" aria-hidden="true"></span> Dashboards
</NavLink> </NavLink>
</div> </div>
</nav> </nav>

View File

@@ -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" @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> <PageTitle>Catalogs</PageTitle>
<h1>Catalogs</h1> <h1>Catalogs</h1>
@if (!string.IsNullOrWhiteSpace(errorMessage)) <CatalogsGrid />
{
<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;
}
}

View File

@@ -1,12 +1,87 @@
@page "/dashboard" @page "/dashboard"
@page "/dashboards/{DashboardId?}"
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration @inject Microsoft.Extensions.Configuration.IConfiguration Configuration
@inject NavigationManager Navigation
<DxDashboard Endpoint="@DashboardEndpoint" style="width: 100%; height: 800px;"> <style>
</DxDashboard> .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 { @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;"> private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
</DxDashboard> *@ private string SelectedDashboardId => string.IsNullOrWhiteSpace(DashboardId) ? "default" : DashboardId;
protected override void OnParametersSet()
{
if (string.IsNullOrWhiteSpace(DashboardId))
{
Navigation.NavigateTo("dashboards/default", replace: true);
}
}
}

View File

@@ -10,6 +10,7 @@
@using DbFirst.BlazorWasm.Layout @using DbFirst.BlazorWasm.Layout
@using DbFirst.BlazorWasm.Models @using DbFirst.BlazorWasm.Models
@using DbFirst.BlazorWasm.Services @using DbFirst.BlazorWasm.Services
@using DbFirst.BlazorWasm.Components
@using DevExpress.Blazor @using DevExpress.Blazor
@using DevExpress.DashboardBlazor @using DevExpress.DashboardBlazor
@using DevExpress.DashboardWeb @using DevExpress.DashboardWeb

View 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;
}
}

View File

@@ -26,8 +26,8 @@
</NavLink> </NavLink>
</div> </div>
<div class="nav-item px-3"> <div class="nav-item px-3">
<NavLink class="nav-link" href="dashboard"> <NavLink class="nav-link" href="dashboards/default">
<span class="oi oi-list-rich" aria-hidden="true"></span> Web Dashboard <span class="oi oi-list-rich" aria-hidden="true"></span> Dashboards
</NavLink> </NavLink>
</div> </div>
</nav> </nav>

View File

@@ -1,12 +1,87 @@
@page "/dashboard" @page "/dashboard"
@page "/dashboards/{DashboardId?}"
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration @inject Microsoft.Extensions.Configuration.IConfiguration Configuration
@inject NavigationManager Navigation
<DxDashboard Endpoint="@DashboardEndpoint" style="width: 100%; height: 800px;"> <style>
</DxDashboard> .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 { @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;"> private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
</DxDashboard> *@ private string SelectedDashboardId => string.IsNullOrWhiteSpace(DashboardId) ? "default" : DashboardId;
protected override void OnParametersSet()
{
if (string.IsNullOrWhiteSpace(DashboardId))
{
Navigation.NavigateTo("dashboards/default", replace: true);
}
}
}

View File

@@ -8,6 +8,9 @@
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using DbFirst.BlazorWebApp @using DbFirst.BlazorWebApp
@using DbFirst.BlazorWebApp.Components @using DbFirst.BlazorWebApp.Components
@using DbFirst.BlazorWebApp.Models
@using DbFirst.BlazorWebApp.Services
@using DevExpress.Blazor @using DevExpress.Blazor
@using DevExpress.DashboardBlazor @using DevExpress.DashboardBlazor
@using DevExpress.DashboardWeb @using DevExpress.DashboardWeb
@using DbFirst.BlazorWebApp

View 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; }
}

View 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; }
}

View File

@@ -1,4 +1,5 @@
using DbFirst.BlazorWebApp.Components; using DbFirst.BlazorWebApp.Components;
using DbFirst.BlazorWebApp.Services;
using DevExpress.Blazor; using DevExpress.Blazor;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -9,6 +10,19 @@ builder.Services.AddRazorComponents()
builder.Services.AddDevExpressBlazor(); 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(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.

View 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; }
}