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