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"?>
|
<?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>
|
||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
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>
|
</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>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
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>
|
</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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
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.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.
|
||||||
|
|||||||
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