Compare commits
28 Commits
2a730ddfcc
...
feat/timer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86feec930b | ||
|
|
f5224e20f2 | ||
|
|
36ade1b26b | ||
|
|
d422d841ff | ||
|
|
ea1b2ea6e4 | ||
|
|
6101561e72 | ||
|
|
64fb76b9e6 | ||
|
|
4ac8e94334 | ||
|
|
dc74d21426 | ||
|
|
566c3b3276 | ||
|
|
ac84866abe | ||
|
|
d9ce4a5dca | ||
|
|
13617dde87 | ||
|
|
789066a214 | ||
|
|
964d508630 | ||
|
|
c5ca9f0048 | ||
|
|
e7aa41aa4d | ||
|
|
9a4f189e4e | ||
|
|
797dd44f25 | ||
|
|
0e313b2e07 | ||
|
|
db232914a9 | ||
|
|
f49db97147 | ||
|
|
0876ebe999 | ||
|
|
74504c583e | ||
|
|
8387b71676 | ||
|
|
8824492057 | ||
|
|
59f22be405 | ||
|
|
23865aefb6 |
28
DbFirst.API/Controllers/TimeController.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using DbFirst.Application.Time.Commands;
|
||||||
|
using DbFirst.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace DbFirst.API.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class TimeController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public TimeController(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<TimeRecord>> InsertAndGetLast(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new InsertTimeCommand(), cancellationToken);
|
||||||
|
if (result == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ builder.Services.AddApplication();
|
|||||||
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
|
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
|
||||||
builder.Services.AddScoped<IMassDataRepository, MassDataRepository>();
|
builder.Services.AddScoped<IMassDataRepository, MassDataRepository>();
|
||||||
builder.Services.AddScoped<ILayoutRepository, LayoutRepository>();
|
builder.Services.AddScoped<ILayoutRepository, LayoutRepository>();
|
||||||
|
builder.Services.AddScoped<ITimeRepository, TimeRepository>();
|
||||||
|
|
||||||
builder.Services.AddDevExpressControls();
|
builder.Services.AddDevExpressControls();
|
||||||
builder.Services.AddSignalR();
|
builder.Services.AddSignalR();
|
||||||
|
|||||||
9
DbFirst.Application/Repositories/ITimeRepository.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using DbFirst.Domain.Entities;
|
||||||
|
|
||||||
|
namespace DbFirst.Application.Repositories;
|
||||||
|
|
||||||
|
public interface ITimeRepository
|
||||||
|
{
|
||||||
|
Task InsertAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<TimeRecord?> GetLastAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
6
DbFirst.Application/Time/Commands/InsertTimeCommand.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
using DbFirst.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace DbFirst.Application.Time.Commands;
|
||||||
|
|
||||||
|
public record InsertTimeCommand : IRequest<TimeRecord?>;
|
||||||
21
DbFirst.Application/Time/Commands/InsertTimeHandler.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using DbFirst.Application.Repositories;
|
||||||
|
using DbFirst.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace DbFirst.Application.Time.Commands;
|
||||||
|
|
||||||
|
public class InsertTimeHandler : IRequestHandler<InsertTimeCommand, TimeRecord?>
|
||||||
|
{
|
||||||
|
private readonly ITimeRepository _repository;
|
||||||
|
|
||||||
|
public InsertTimeHandler(ITimeRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TimeRecord?> Handle(InsertTimeCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _repository.InsertAsync(cancellationToken);
|
||||||
|
return await _repository.GetLastAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
Ablauf und die Rolle jeder Datei in der Blazor WebAssembly-Anwendung:
|
|
||||||
|
|
||||||
1.index.html
|
|
||||||
• Reihenfolge: Wird als erstes geladen, wenn die Anwendung im Browser geöffnet wird.
|
|
||||||
• Purpose:
|
|
||||||
• Lädt die Blazor WebAssembly-Umgebung (blazor.webassembly.js).
|
|
||||||
• Definiert den Platzhalter <div id="app">, in dem die Blazor-Komponenten gerendert werden.
|
|
||||||
• Bindet Stylesheets und Skripte ein, die für das Styling und die Funktionalität der Anwendung benötigt werden.
|
|
||||||
|
|
||||||
2. Program.cs
|
|
||||||
• Reihenfolge: Wird nach index.html ausgeführt, sobald die Blazor-Umgebung initialisiert ist.
|
|
||||||
• Purpose:
|
|
||||||
• Initialisiert die Blazor WebAssembly-Anwendung.
|
|
||||||
• Registriert Root-Komponenten (App) und Abhängigkeiten (z. B. HttpClient, CatalogApiClient).
|
|
||||||
• Konfiguriert die Basis-URL für API-Aufrufe.
|
|
||||||
|
|
||||||
3. App.razor
|
|
||||||
• Reihenfolge: Wird als nächstes geladen, nachdem die Anwendung initialisiert wurde.
|
|
||||||
• Purpose:
|
|
||||||
• Definiert die Routing-Logik der Anwendung.
|
|
||||||
• Entscheidet, welche Komponente basierend auf der URL gerendert wird.
|
|
||||||
• Stellt sicher, dass ein Standardlayout (MainLayout) verwendet wird.
|
|
||||||
|
|
||||||
4.MainLayout.razor
|
|
||||||
• Reihenfolge: Wird geladen, wenn eine Seite gerendert wird, da es das Standardlayout ist.
|
|
||||||
• Purpose:
|
|
||||||
• Definiert das Hauptlayout der Anwendung.
|
|
||||||
• Enthält die Navigationsleiste (NavMenu) und den Platzhalter für den Seiteninhalt (@Body).
|
|
||||||
|
|
||||||
5. NavMenu.razor
|
|
||||||
• Reihenfolge: Wird als Teil des Layouts (MainLayout) geladen.
|
|
||||||
• Purpose:
|
|
||||||
• Stellt die Navigationsleiste bereit.
|
|
||||||
• Enthält Links zu verschiedenen Seiten der Anwendung (z. B. Home, Catalogs).
|
|
||||||
• Ermöglicht das Ein- und Ausklappen des Menüs.
|
|
||||||
|
|
||||||
6. Catalogs.razor
|
|
||||||
• Reihenfolge: Wird geladen, wenn der Benutzer die URL /catalogs aufruft.
|
|
||||||
• Purpose:
|
|
||||||
• Stellt die Benutzeroberfläche für die Verwaltung von Katalogen bereit.
|
|
||||||
• Nutzt CatalogApiClient, um Daten von der API zu laden, zu erstellen, zu aktualisieren oder zu löschen.
|
|
||||||
• Verwendet DevExpress-Komponenten für ein modernes UI.
|
|
||||||
|
|
||||||
7. CatalogApiClient.cs
|
|
||||||
• Reihenfolge: Wird verwendet, wenn Catalogs.razor API-Aufrufe ausführt.
|
|
||||||
• Purpose:
|
|
||||||
• Kapselt die Kommunikation mit der API.
|
|
||||||
• Bietet Methoden für CRUD-Operationen (Create, Read, Update, Delete) auf Katalog-Daten.
|
|
||||||
• Behandelt Fehler und gibt benutzerfreundliche Fehlermeldungen zurück.
|
|
||||||
|
|
||||||
Zusammenfassung des Ablaufs:
|
|
||||||
1.index.html: Lädt die Blazor-Umgebung und startet die Anwendung.
|
|
||||||
2. Program.cs: Initialisiert die Anwendung und registriert Abhängigkeiten.
|
|
||||||
3. App.razor: Definiert die Routing-Logik und lädt das Standardlayout.
|
|
||||||
4. MainLayout.razor: Stellt das Hauptlayout bereit.
|
|
||||||
5. NavMenu.razor: Lädt die Navigationsleiste.
|
|
||||||
6. Seiten wie Catalogs.razor: Werden basierend auf der URL gerendert.
|
|
||||||
7. CatalogApiClient.cs: Führt API-Aufrufe aus, wenn die Seite Daten benötigt.
|
|
||||||
*/
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
@*
|
|
||||||
• Ist der logische Einstiegspunkt der Blazor-Anwendung.
|
|
||||||
• Sie definiert die Routing-Logik und das Standardlayout der Anwendung.
|
|
||||||
• Der Router-Komponent in App.razor entscheidet, welche Blazor-Komponente basierend auf der URL geladen wird.
|
|
||||||
kurz: Steuert die Navigation und das Rendering der Blazor-Komponenten.
|
|
||||||
*@
|
|
||||||
@DxResourceManager.RegisterTheme(Themes.Fluent)
|
|
||||||
@DxResourceManager.RegisterScripts()
|
|
||||||
<Router AppAssembly="@typeof(App).Assembly">
|
|
||||||
<Found Context="routeData">
|
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
|
||||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
|
||||||
</Found>
|
|
||||||
<NotFound>
|
|
||||||
<PageTitle>Not found</PageTitle>
|
|
||||||
<LayoutView Layout="@typeof(MainLayout)">
|
|
||||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
|
||||||
</LayoutView>
|
|
||||||
</NotFound>
|
|
||||||
</Router>
|
|
||||||
@@ -1,830 +0,0 @@
|
|||||||
@using System.Text.Json
|
|
||||||
@using Microsoft.AspNetCore.Components
|
|
||||||
@using Microsoft.AspNetCore.Components.Rendering
|
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
|
||||||
@using DevExpress.Blazor
|
|
||||||
@inject CatalogApiClient Api
|
|
||||||
@inject LayoutApiClient LayoutApi
|
|
||||||
@inject IJSRuntime JsRuntime
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.action-panel { margin-bottom: 16px; }
|
|
||||||
.grid-section { margin-top: 12px; }
|
|
||||||
.catalog-grid .dxbl-grid-sort-asc,
|
|
||||||
.catalog-grid .dxbl-grid-sort-desc {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.catalog-grid th.dxbl-grid-header-sortable {
|
|
||||||
position: relative;
|
|
||||||
padding-right: 1.5rem;
|
|
||||||
}
|
|
||||||
.catalog-grid th.dxbl-grid-header-sortable::before,
|
|
||||||
.catalog-grid th.dxbl-grid-header-sortable::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::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::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 th.dxbl-grid-header-sortable[aria-sort="ascending"]::after {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.catalog-grid th.dxbl-grid-header-sortable[aria-sort="descending"]::before {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
.catalog-edit-popup {
|
|
||||||
min-width: 720px;
|
|
||||||
}
|
|
||||||
.band-editor {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.band-controls {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.band-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.band-columns {
|
|
||||||
max-width: 720px;
|
|
||||||
}
|
|
||||||
</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>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (isLoading)
|
|
||||||
{
|
|
||||||
<p><em>Lade Daten...</em></p>
|
|
||||||
}
|
|
||||||
else if (items.Count == 0)
|
|
||||||
{
|
|
||||||
<p>Keine Einträge vorhanden.</p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="band-editor">
|
|
||||||
<div class="band-controls">
|
|
||||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
|
||||||
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
|
||||||
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
|
||||||
</div>
|
|
||||||
@foreach (var band in bandLayout.Bands)
|
|
||||||
{
|
|
||||||
<div class="band-row">
|
|
||||||
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
|
|
||||||
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<DxFormLayout CssClass="band-columns" ColCount="2">
|
|
||||||
@foreach (var column in columnDefinitions)
|
|
||||||
{
|
|
||||||
<DxFormLayoutItem Caption="@column.Caption">
|
|
||||||
<DxComboBox Data="@bandOptions"
|
|
||||||
TData="BandOption"
|
|
||||||
TValue="string"
|
|
||||||
TextFieldName="Caption"
|
|
||||||
ValueFieldName="Id"
|
|
||||||
Value="@GetColumnBand(column.FieldName)"
|
|
||||||
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
|
|
||||||
Width="100%" />
|
|
||||||
</DxFormLayoutItem>
|
|
||||||
}
|
|
||||||
</DxFormLayout>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid-section">
|
|
||||||
<DxGrid Data="@items"
|
|
||||||
TItem="CatalogReadDto"
|
|
||||||
KeyFieldName="@nameof(CatalogReadDto.Guid)"
|
|
||||||
ShowFilterRow="true"
|
|
||||||
AllowColumnResize="true"
|
|
||||||
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
|
||||||
AllowColumnReorder="true"
|
|
||||||
PageSize="10"
|
|
||||||
CssClass="mb-4 catalog-grid"
|
|
||||||
EditMode="GridEditMode.PopupEditForm"
|
|
||||||
PopupEditFormCssClass="catalog-edit-popup"
|
|
||||||
PopupEditFormHeaderText="@popupHeaderText"
|
|
||||||
CustomizeEditModel="OnCustomizeEditModel"
|
|
||||||
EditModelSaving="OnEditModelSaving"
|
|
||||||
DataItemDeleting="OnDataItemDeleting"
|
|
||||||
@ref="gridRef">
|
|
||||||
<Columns>
|
|
||||||
@RenderColumns()
|
|
||||||
</Columns>
|
|
||||||
<EditFormTemplate Context="editFormContext">
|
|
||||||
@{ SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
|
|
||||||
<DxFormLayout ColCount="2">
|
|
||||||
<DxFormLayoutItem Caption="Titel">
|
|
||||||
<DxTextBox @bind-Text="editModel.CatTitle" Width="100%" />
|
|
||||||
</DxFormLayoutItem>
|
|
||||||
<DxFormLayoutItem Caption="Kennung">
|
|
||||||
<DxTextBox @bind-Text="editModel.CatString" Width="100%" />
|
|
||||||
</DxFormLayoutItem>
|
|
||||||
@if (!editModel.IsNew)
|
|
||||||
{
|
|
||||||
<DxFormLayoutItem Caption="Update-Prozedur">
|
|
||||||
<DxComboBox Data="@procedureOptions"
|
|
||||||
TData="ProcedureOption"
|
|
||||||
TValue="int"
|
|
||||||
TextFieldName="Text"
|
|
||||||
ValueFieldName="Value"
|
|
||||||
@bind-Value="editModel.UpdateProcedure"
|
|
||||||
Width="100%" />
|
|
||||||
</DxFormLayoutItem>
|
|
||||||
}
|
|
||||||
<DxFormLayoutItem ColSpanMd="12">
|
|
||||||
<ValidationSummary />
|
|
||||||
</DxFormLayoutItem>
|
|
||||||
</DxFormLayout>
|
|
||||||
</EditFormTemplate>
|
|
||||||
</DxGrid>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private List<CatalogReadDto> items = new();
|
|
||||||
private bool isLoading;
|
|
||||||
private string? errorMessage;
|
|
||||||
private string? infoMessage;
|
|
||||||
private EditContext? editContext;
|
|
||||||
private ValidationMessageStore? validationMessageStore;
|
|
||||||
private IGrid? gridRef;
|
|
||||||
private string popupHeaderText = "Edit";
|
|
||||||
private const string LayoutType = "GRID_BANDS";
|
|
||||||
private const string LayoutKey = "CatalogsGrid";
|
|
||||||
private const string LayoutUserStorageKey = "layoutUser";
|
|
||||||
private string? layoutUser;
|
|
||||||
private BandLayout bandLayout = new();
|
|
||||||
private Dictionary<string, string> columnBandAssignments = new();
|
|
||||||
private List<BandOption> bandOptions = new();
|
|
||||||
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
|
||||||
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
|
|
||||||
private List<ColumnDefinition> columnDefinitions = new()
|
|
||||||
{
|
|
||||||
new() { FieldName = nameof(CatalogReadDto.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text },
|
|
||||||
new() { FieldName = nameof(CatalogReadDto.CatTitle), Caption = "Titel", FilterType = ColumnFilterType.Text },
|
|
||||||
new() { FieldName = nameof(CatalogReadDto.CatString), Caption = "String", FilterType = ColumnFilterType.Text },
|
|
||||||
new() { FieldName = nameof(CatalogReadDto.AddedWho), Caption = "Angelegt von", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
|
||||||
new() { FieldName = nameof(CatalogReadDto.AddedWhen), Caption = "Angelegt am", ReadOnly = true, FilterType = ColumnFilterType.Date },
|
|
||||||
new() { FieldName = nameof(CatalogReadDto.ChangedWho), Caption = "Geändert von", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
|
||||||
new() { FieldName = nameof(CatalogReadDto.ChangedWhen), Caption = "Geändert am", ReadOnly = true, FilterType = ColumnFilterType.Date }
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly List<ProcedureOption> procedureOptions = new()
|
|
||||||
{
|
|
||||||
new() { Value = 0, Text = "PRTBMY_CATALOG_UPDATE" },
|
|
||||||
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
|
|
||||||
};
|
|
||||||
|
|
||||||
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
|
||||||
|
|
||||||
private bool gridLayoutApplied;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
|
||||||
await EnsureLayoutUserAsync();
|
|
||||||
await LoadBandLayoutAsync();
|
|
||||||
await LoadCatalogs();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
|
||||||
{
|
|
||||||
gridRef.LoadLayout(bandLayout.GridLayout);
|
|
||||||
gridLayoutApplied = true;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetEditContext(EditContext context)
|
|
||||||
{
|
|
||||||
if (editContext == context)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editContext != null)
|
|
||||||
{
|
|
||||||
editContext.OnFieldChanged -= OnEditFieldChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
editContext = context;
|
|
||||||
validationMessageStore = new ValidationMessageStore(editContext);
|
|
||||||
editContext.OnFieldChanged += OnEditFieldChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (validationMessageStore == null || editContext == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.UpdateProcedure))
|
|
||||||
{
|
|
||||||
validationMessageStore.Clear();
|
|
||||||
editContext.NotifyValidationStateChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.CatTitle))
|
|
||||||
{
|
|
||||||
var field = new FieldIdentifier(editContext.Model, nameof(CatalogEditModel.CatTitle));
|
|
||||||
validationMessageStore.Clear(field);
|
|
||||||
editContext.NotifyValidationStateChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetPopupHeaderText(bool isNew)
|
|
||||||
{
|
|
||||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
|
||||||
{
|
|
||||||
popupHeaderText = e.IsNew ? "Neu" : "Edit";
|
|
||||||
if (e.IsNew)
|
|
||||||
{
|
|
||||||
e.EditModel = new CatalogEditModel { IsNew = true };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var item = (CatalogReadDto)e.DataItem;
|
|
||||||
e.EditModel = new CatalogEditModel
|
|
||||||
{
|
|
||||||
Guid = item.Guid,
|
|
||||||
CatTitle = item.CatTitle,
|
|
||||||
CatString = item.CatString,
|
|
||||||
UpdateProcedure = 0,
|
|
||||||
OriginalCatTitle = item.CatTitle,
|
|
||||||
IsNew = false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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 async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
|
|
||||||
{
|
|
||||||
errorMessage = null;
|
|
||||||
infoMessage = null;
|
|
||||||
|
|
||||||
validationMessageStore?.Clear();
|
|
||||||
editContext?.NotifyValidationStateChanged();
|
|
||||||
|
|
||||||
var editModel = (CatalogEditModel)e.EditModel;
|
|
||||||
if (!ValidateEditModel(editModel, e.IsNew))
|
|
||||||
{
|
|
||||||
e.Cancel = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dto = new CatalogWriteDto
|
|
||||||
{
|
|
||||||
CatTitle = editModel.CatTitle,
|
|
||||||
CatString = editModel.CatString,
|
|
||||||
UpdateProcedure = editModel.UpdateProcedure
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (e.IsNew)
|
|
||||||
{
|
|
||||||
var created = await Api.CreateAsync(dto);
|
|
||||||
if (!created.Success || created.Value == null)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(created.Error))
|
|
||||||
{
|
|
||||||
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), created.Error);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
errorMessage = "Anlegen fehlgeschlagen.";
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Cancel = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
infoMessage = "Katalog angelegt.";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var updated = await Api.UpdateAsync(editModel.Guid, dto);
|
|
||||||
if (!updated.Success)
|
|
||||||
{
|
|
||||||
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
|
|
||||||
e.Cancel = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
infoMessage = "Katalog aktualisiert.";
|
|
||||||
}
|
|
||||||
|
|
||||||
await LoadCatalogs();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errorMessage = $"Fehler beim Speichern: {ex.Message}";
|
|
||||||
e.Cancel = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddValidationError(CatalogEditModel editModel, string fieldName, string message)
|
|
||||||
{
|
|
||||||
if (editContext == null || validationMessageStore == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var field = new FieldIdentifier(editModel, fieldName);
|
|
||||||
validationMessageStore.Add(field, message);
|
|
||||||
editContext.NotifyValidationStateChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ValidateEditModel(CatalogEditModel editModel, bool isNew)
|
|
||||||
{
|
|
||||||
if (isNew)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editModel.UpdateProcedure == 0 &&
|
|
||||||
!string.Equals(editModel.CatTitle, editModel.OriginalCatTitle, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), "Titel kann nicht geändert werden.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnDataItemDeleting(GridDataItemDeletingEventArgs e)
|
|
||||||
{
|
|
||||||
errorMessage = null;
|
|
||||||
infoMessage = null;
|
|
||||||
|
|
||||||
var item = (CatalogReadDto)e.DataItem;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var deleted = await Api.DeleteAsync(item.Guid);
|
|
||||||
if (!deleted.Success)
|
|
||||||
{
|
|
||||||
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
|
|
||||||
e.Cancel = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
infoMessage = "Katalog gelöscht.";
|
|
||||||
await LoadCatalogs();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errorMessage = $"Fehler beim Löschen: {ex.Message}";
|
|
||||||
e.Cancel = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task EnsureLayoutUserAsync()
|
|
||||||
{
|
|
||||||
layoutUser = await JsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
||||||
{
|
|
||||||
layoutUser = Guid.NewGuid().ToString("N");
|
|
||||||
await JsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadBandLayoutAsync()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
||||||
{
|
|
||||||
bandLayout = new BandLayout();
|
|
||||||
UpdateBandOptions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var stored = await LayoutApi.GetAsync(LayoutType, LayoutKey, layoutUser);
|
|
||||||
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
|
|
||||||
{
|
|
||||||
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, jsonOptions);
|
|
||||||
bandLayout = NormalizeBandLayout(parsed);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bandLayout = new BandLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
columnBandAssignments = BuildAssignmentsFromLayout(bandLayout);
|
|
||||||
ApplyColumnLayoutFromStorage();
|
|
||||||
UpdateBandOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SaveLayoutAsync()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CaptureColumnLayoutFromGrid();
|
|
||||||
|
|
||||||
var layoutData = JsonSerializer.Serialize(bandLayout, jsonOptions);
|
|
||||||
await LayoutApi.UpsertAsync(new LayoutDto
|
|
||||||
{
|
|
||||||
LayoutType = LayoutType,
|
|
||||||
LayoutKey = LayoutKey,
|
|
||||||
UserName = layoutUser,
|
|
||||||
LayoutData = layoutData
|
|
||||||
});
|
|
||||||
infoMessage = "Layout gespeichert.";
|
|
||||||
errorMessage = null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CaptureColumnLayoutFromGrid()
|
|
||||||
{
|
|
||||||
if (gridRef == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var layout = gridRef.SaveLayout();
|
|
||||||
bandLayout.GridLayout = layout;
|
|
||||||
|
|
||||||
var orderedColumns = layout.Columns
|
|
||||||
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
|
|
||||||
.OrderBy(column => column.VisibleIndex)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
bandLayout.ColumnOrder = orderedColumns
|
|
||||||
.Select(column => column.FieldName)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
bandLayout.ColumnWidths = orderedColumns
|
|
||||||
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
|
|
||||||
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ResetBandLayoutAsync()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
|
|
||||||
bandLayout = new BandLayout();
|
|
||||||
columnBandAssignments.Clear();
|
|
||||||
UpdateBandOptions();
|
|
||||||
infoMessage = "Band-Layout zurückgesetzt.";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyColumnLayoutFromStorage()
|
|
||||||
{
|
|
||||||
foreach (var column in columnDefinitions)
|
|
||||||
{
|
|
||||||
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
|
||||||
{
|
|
||||||
column.Width = width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyBandOrderingFromColumnOrder()
|
|
||||||
{
|
|
||||||
if (bandLayout.ColumnOrder.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bandById = bandLayout.Bands.ToDictionary(band => band.Id, StringComparer.OrdinalIgnoreCase);
|
|
||||||
var orderedBandIds = new List<string>();
|
|
||||||
var orderedColumnsByBand = bandLayout.Bands.ToDictionary(
|
|
||||||
band => band.Id,
|
|
||||||
_ => new List<string>(),
|
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
foreach (var field in bandLayout.ColumnOrder)
|
|
||||||
{
|
|
||||||
if (columnBandAssignments.TryGetValue(field, out var bandId) && bandById.ContainsKey(bandId))
|
|
||||||
{
|
|
||||||
if (!orderedBandIds.Contains(bandId, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
orderedBandIds.Add(bandId);
|
|
||||||
}
|
|
||||||
|
|
||||||
orderedColumnsByBand[bandId].Add(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var band in bandLayout.Bands)
|
|
||||||
{
|
|
||||||
var orderedColumns = orderedColumnsByBand[band.Id];
|
|
||||||
orderedColumns.AddRange(band.Columns.Where(column => !orderedColumns.Contains(column, StringComparer.OrdinalIgnoreCase)));
|
|
||||||
band.Columns = orderedColumns;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orderedBandIds.Count > 0)
|
|
||||||
{
|
|
||||||
bandLayout.Bands = orderedBandIds
|
|
||||||
.Select(id => bandById[id])
|
|
||||||
.Concat(bandLayout.Bands.Where(band => !orderedBandIds.Contains(band.Id, StringComparer.OrdinalIgnoreCase)))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddBand()
|
|
||||||
{
|
|
||||||
bandLayout.Bands.Add(new BandDefinition
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid().ToString("N"),
|
|
||||||
Caption = "Band"
|
|
||||||
});
|
|
||||||
UpdateBandOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveBand(BandDefinition band)
|
|
||||||
{
|
|
||||||
bandLayout.Bands.Remove(band);
|
|
||||||
var removedColumns = columnBandAssignments.Where(pair => pair.Value == band.Id)
|
|
||||||
.Select(pair => pair.Key)
|
|
||||||
.ToList();
|
|
||||||
foreach (var column in removedColumns)
|
|
||||||
{
|
|
||||||
columnBandAssignments.Remove(column);
|
|
||||||
}
|
|
||||||
UpdateBandOptions();
|
|
||||||
SyncBandsFromAssignments();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateBandCaption(BandDefinition band, string value)
|
|
||||||
{
|
|
||||||
band.Caption = value;
|
|
||||||
UpdateBandOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateColumnBand(string fieldName, string? bandId)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(bandId))
|
|
||||||
{
|
|
||||||
columnBandAssignments.Remove(fieldName);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
columnBandAssignments[fieldName] = bandId;
|
|
||||||
}
|
|
||||||
|
|
||||||
SyncBandsFromAssignments();
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetColumnBand(string fieldName)
|
|
||||||
{
|
|
||||||
return columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SyncBandsFromAssignments()
|
|
||||||
{
|
|
||||||
foreach (var band in bandLayout.Bands)
|
|
||||||
{
|
|
||||||
band.Columns = columnDefinitions
|
|
||||||
.Where(column => columnBandAssignments.TryGetValue(column.FieldName, out var bandId) && bandId == band.Id)
|
|
||||||
.Select(column => column.FieldName)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateBandOptions()
|
|
||||||
{
|
|
||||||
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
|
||||||
bandOptions.AddRange(bandLayout.Bands.Select(band => new BandOption { Id = band.Id, Caption = band.Caption }));
|
|
||||||
}
|
|
||||||
|
|
||||||
private BandLayout NormalizeBandLayout(BandLayout? layout)
|
|
||||||
{
|
|
||||||
layout ??= new BandLayout();
|
|
||||||
layout.Bands ??= new List<BandDefinition>();
|
|
||||||
layout.ColumnOrder ??= new List<string>();
|
|
||||||
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var band in layout.Bands)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(band.Id))
|
|
||||||
{
|
|
||||||
band.Id = Guid.NewGuid().ToString("N");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(band.Caption))
|
|
||||||
{
|
|
||||||
band.Caption = "Band";
|
|
||||||
}
|
|
||||||
|
|
||||||
band.Columns = band.Columns?.Where(columnLookup.ContainsKey).ToList() ?? new List<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return layout;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
|
|
||||||
{
|
|
||||||
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var band in layout.Bands)
|
|
||||||
{
|
|
||||||
foreach (var column in band.Columns)
|
|
||||||
{
|
|
||||||
assignments[column] = band.Id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return assignments;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RenderFragment RenderColumns() => builder =>
|
|
||||||
{
|
|
||||||
var seq = 0;
|
|
||||||
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
|
||||||
builder.AddAttribute(seq++, "Width", "120px");
|
|
||||||
builder.CloseComponent();
|
|
||||||
|
|
||||||
var grouped = bandLayout.Bands.SelectMany(band => band.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var column in columnDefinitions.Where(column => !grouped.Contains(column.FieldName)))
|
|
||||||
{
|
|
||||||
BuildDataColumn(builder, ref seq, column);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var band in bandLayout.Bands)
|
|
||||||
{
|
|
||||||
if (band.Columns.Count == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.OpenComponent<DxGridBandColumn>(seq++);
|
|
||||||
builder.AddAttribute(seq++, "Caption", band.Caption);
|
|
||||||
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
|
||||||
{
|
|
||||||
var bandSeq = 0;
|
|
||||||
foreach (var columnName in band.Columns)
|
|
||||||
{
|
|
||||||
if (columnLookup.TryGetValue(columnName, out var column))
|
|
||||||
{
|
|
||||||
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
builder.CloseComponent();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
|
|
||||||
{
|
|
||||||
builder.OpenComponent<DxGridDataColumn>(seq++);
|
|
||||||
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
|
||||||
builder.AddAttribute(seq++, "Caption", column.Caption);
|
|
||||||
if (!string.IsNullOrWhiteSpace(column.Width))
|
|
||||||
{
|
|
||||||
builder.AddAttribute(seq++, "Width", column.Width);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
|
||||||
{
|
|
||||||
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.ReadOnly)
|
|
||||||
{
|
|
||||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.CloseComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private RenderFragment? BuildFilterTemplate(ColumnDefinition column)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RenderFragment? BuildTextFilterTemplate()
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RenderFragment? BuildDateFilterTemplate()
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class BandLayout
|
|
||||||
{
|
|
||||||
public List<BandDefinition> Bands { get; set; } = new();
|
|
||||||
public List<string> ColumnOrder { get; set; } = new();
|
|
||||||
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
public GridPersistentLayout? GridLayout { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class BandDefinition
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
public string Caption { get; set; } = string.Empty;
|
|
||||||
public List<string> Columns { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class BandOption
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
public string Caption { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ColumnDefinition
|
|
||||||
{
|
|
||||||
public string FieldName { get; init; } = string.Empty;
|
|
||||||
public string Caption { get; init; } = string.Empty;
|
|
||||||
public string? Width { get; set; }
|
|
||||||
public string? DisplayFormat { get; init; }
|
|
||||||
public bool ReadOnly { get; init; }
|
|
||||||
public ColumnFilterType FilterType { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum ColumnFilterType
|
|
||||||
{
|
|
||||||
Text,
|
|
||||||
Date
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class CatalogEditModel
|
|
||||||
{
|
|
||||||
public int Guid { get; set; }
|
|
||||||
public string CatTitle { get; set; } = string.Empty;
|
|
||||||
public string CatString { get; set; } = string.Empty;
|
|
||||||
public int UpdateProcedure { get; set; }
|
|
||||||
public string OriginalCatTitle { get; set; } = string.Empty;
|
|
||||||
public bool IsNew { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ProcedureOption
|
|
||||||
{
|
|
||||||
public int Value { get; set; }
|
|
||||||
public string Text { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,819 +0,0 @@
|
|||||||
@using System.Text.Json
|
|
||||||
@using Microsoft.AspNetCore.Components
|
|
||||||
@using Microsoft.AspNetCore.Components.Rendering
|
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
|
||||||
@using DevExpress.Blazor
|
|
||||||
@inject MassDataApiClient Api
|
|
||||||
@inject LayoutApiClient LayoutApi
|
|
||||||
@inject IJSRuntime JsRuntime
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.action-panel { margin-bottom: 16px; }
|
|
||||||
.grid-section { margin-top: 12px; }
|
|
||||||
.pager-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 12px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.page-size-selector {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
}
|
|
||||||
.page-size-label {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.page-size-combo {
|
|
||||||
width: 13ch;
|
|
||||||
min-width: 13ch;
|
|
||||||
max-width: 13ch;
|
|
||||||
}
|
|
||||||
.page-size-combo input {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.massdata-grid .dxbl-grid-sort-asc,
|
|
||||||
.massdata-grid .dxbl-grid-sort-desc {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.massdata-grid th.dxbl-grid-header-sortable {
|
|
||||||
position: relative;
|
|
||||||
padding-right: 1.5rem;
|
|
||||||
}
|
|
||||||
.massdata-grid th.dxbl-grid-header-sortable::before,
|
|
||||||
.massdata-grid th.dxbl-grid-header-sortable::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;
|
|
||||||
}
|
|
||||||
.massdata-grid th.dxbl-grid-header-sortable::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");
|
|
||||||
}
|
|
||||||
.massdata-grid th.dxbl-grid-header-sortable::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");
|
|
||||||
}
|
|
||||||
.massdata-grid th.dxbl-grid-header-sortable[aria-sort="ascending"]::after {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.massdata-grid th.dxbl-grid-header-sortable[aria-sort="descending"]::before {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.massdata-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;
|
|
||||||
}
|
|
||||||
.massdata-edit-popup {
|
|
||||||
min-width: 720px;
|
|
||||||
}
|
|
||||||
.band-editor {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.band-controls {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.band-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.band-columns {
|
|
||||||
max-width: 720px;
|
|
||||||
}
|
|
||||||
</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>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (isLoading)
|
|
||||||
{
|
|
||||||
<p><em>Lade Daten...</em></p>
|
|
||||||
}
|
|
||||||
else if (items.Count == 0)
|
|
||||||
{
|
|
||||||
<p>Keine Einträge vorhanden.</p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="mb-3 page-size-selector">
|
|
||||||
<span class="page-size-label">Datensätze je Seite:</span>
|
|
||||||
<DxComboBox Data="@pageSizeOptions"
|
|
||||||
TData="PageSizeOption"
|
|
||||||
TValue="int?"
|
|
||||||
TextFieldName="Text"
|
|
||||||
ValueFieldName="Value"
|
|
||||||
Value="@pageSize"
|
|
||||||
ValueChanged="OnPageSizeChanged"
|
|
||||||
CssClass="page-size-combo" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="band-editor">
|
|
||||||
<div class="band-controls">
|
|
||||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
|
||||||
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
|
||||||
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
|
||||||
</div>
|
|
||||||
@foreach (var band in bandLayout.Bands)
|
|
||||||
{
|
|
||||||
<div class="band-row">
|
|
||||||
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
|
|
||||||
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<DxFormLayout CssClass="band-columns" ColCount="2">
|
|
||||||
@foreach (var column in columnDefinitions)
|
|
||||||
{
|
|
||||||
<DxFormLayoutItem Caption="@column.Caption">
|
|
||||||
<DxComboBox Data="@bandOptions"
|
|
||||||
TData="BandOption"
|
|
||||||
TValue="string"
|
|
||||||
TextFieldName="Caption"
|
|
||||||
ValueFieldName="Id"
|
|
||||||
Value="@GetColumnBand(column.FieldName)"
|
|
||||||
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
|
|
||||||
Width="100%" />
|
|
||||||
</DxFormLayoutItem>
|
|
||||||
}
|
|
||||||
</DxFormLayout>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid-section">
|
|
||||||
<DxGrid Data="@items"
|
|
||||||
TItem="MassDataReadDto"
|
|
||||||
KeyFieldName="@nameof(MassDataReadDto.Id)"
|
|
||||||
ShowFilterRow="true"
|
|
||||||
AllowColumnResize="true"
|
|
||||||
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
|
||||||
AllowColumnReorder="true"
|
|
||||||
PagerVisible="false"
|
|
||||||
PageSize="@(pageSize ?? 100)"
|
|
||||||
CssClass="mb-3 massdata-grid"
|
|
||||||
EditMode="GridEditMode.PopupEditForm"
|
|
||||||
PopupEditFormHeaderText="@popupHeaderText"
|
|
||||||
CustomizeEditModel="OnCustomizeEditModel"
|
|
||||||
EditModelSaving="OnEditModelSaving"
|
|
||||||
DataItemDeleting="OnDataItemDeleting"
|
|
||||||
@ref="gridRef">
|
|
||||||
<Columns>
|
|
||||||
@RenderColumns()
|
|
||||||
</Columns>
|
|
||||||
<EditFormTemplate Context="editFormContext">
|
|
||||||
@{ SetEditContext(editFormContext.EditContext); var editModel = (MassDataEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
|
|
||||||
<DxFormLayout ColCount="2">
|
|
||||||
<DxFormLayoutItem Caption="CustomerName">
|
|
||||||
<DxTextBox @bind-Text="editModel.CustomerName" Width="100%" />
|
|
||||||
</DxFormLayoutItem>
|
|
||||||
<DxFormLayoutItem Caption="Amount">
|
|
||||||
<DxTextBox @bind-Text="editModel.AmountText" Width="100%" />
|
|
||||||
</DxFormLayoutItem>
|
|
||||||
<DxFormLayoutItem Caption="Category">
|
|
||||||
<DxTextBox @bind-Text="editModel.Category" Width="100%" ReadOnly="@(!editModel.IsNew)" />
|
|
||||||
</DxFormLayoutItem>
|
|
||||||
<DxFormLayoutItem Caption="Status">
|
|
||||||
<DxCheckBox @bind-Checked="editModel.StatusFlag" ReadOnly="@(!editModel.IsNew)" />
|
|
||||||
</DxFormLayoutItem>
|
|
||||||
@if (!editModel.IsNew)
|
|
||||||
{
|
|
||||||
<DxFormLayoutItem Caption="Prozedur">
|
|
||||||
<DxComboBox Data="@procedureOptions"
|
|
||||||
TData="ProcedureOption"
|
|
||||||
TValue="int"
|
|
||||||
TextFieldName="Text"
|
|
||||||
ValueFieldName="Value"
|
|
||||||
@bind-Value="editModel.UpdateProcedure"
|
|
||||||
Width="100%" />
|
|
||||||
</DxFormLayoutItem>
|
|
||||||
}
|
|
||||||
<DxFormLayoutItem ColSpanMd="12">
|
|
||||||
<ValidationSummary />
|
|
||||||
</DxFormLayoutItem>
|
|
||||||
</DxFormLayout>
|
|
||||||
</EditFormTemplate>
|
|
||||||
</DxGrid>
|
|
||||||
|
|
||||||
@if (pageCount > 1)
|
|
||||||
{
|
|
||||||
<div class="pager-container">
|
|
||||||
<DxPager PageCount="@pageCount" ActivePageIndex="@pageIndex" ActivePageIndexChanged="OnPageChanged" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private List<MassDataReadDto> items = new();
|
|
||||||
private bool isLoading;
|
|
||||||
private string? errorMessage;
|
|
||||||
private string? infoMessage;
|
|
||||||
private int pageIndex;
|
|
||||||
private int pageCount = 1;
|
|
||||||
private int? pageSize = 100;
|
|
||||||
private string popupHeaderText = "Edit";
|
|
||||||
private EditContext? editContext;
|
|
||||||
private ValidationMessageStore? validationMessageStore;
|
|
||||||
private IGrid? gridRef;
|
|
||||||
private const string LayoutType = "GRID_BANDS";
|
|
||||||
private const string LayoutKey = "MassDataGrid";
|
|
||||||
private const string LayoutUserStorageKey = "layoutUser";
|
|
||||||
private string? layoutUser;
|
|
||||||
private BandLayout bandLayout = new();
|
|
||||||
private Dictionary<string, string> columnBandAssignments = new();
|
|
||||||
private List<BandOption> bandOptions = new();
|
|
||||||
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
|
||||||
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
|
|
||||||
private List<ColumnDefinition> columnDefinitions = new()
|
|
||||||
{
|
|
||||||
new() { FieldName = nameof(MassDataReadDto.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
|
||||||
new() { FieldName = nameof(MassDataReadDto.CustomerName), Caption = "CustomerName", FilterType = ColumnFilterType.Text },
|
|
||||||
new() { FieldName = nameof(MassDataReadDto.Amount), Caption = "Amount", DisplayFormat = "c2", FilterType = ColumnFilterType.Text },
|
|
||||||
new() { FieldName = nameof(MassDataReadDto.Category), Caption = "Category", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
|
||||||
new() { FieldName = nameof(MassDataReadDto.StatusFlag), Caption = "Status", ReadOnly = true, FilterType = ColumnFilterType.Bool },
|
|
||||||
new() { FieldName = nameof(MassDataReadDto.AddedWhen), Caption = "Angelegt am", ReadOnly = true, FilterType = ColumnFilterType.Date },
|
|
||||||
new() { FieldName = nameof(MassDataReadDto.ChangedWhen), Caption = "Geändert am", ReadOnly = true, FilterType = ColumnFilterType.Date }
|
|
||||||
};
|
|
||||||
|
|
||||||
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
|
||||||
|
|
||||||
private readonly List<PageSizeOption> pageSizeOptions = new()
|
|
||||||
{
|
|
||||||
new() { Value = 100, Text = "100" },
|
|
||||||
new() { Value = 1000, Text = "1.000" },
|
|
||||||
new() { Value = 10000, Text = "10.000" },
|
|
||||||
new() { Value = 100000, Text = "100.000" },
|
|
||||||
new() { Value = null, Text = "Alle" }
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly List<BoolFilterOption> statusFilterOptions = new()
|
|
||||||
{
|
|
||||||
new() { Value = null, Text = "Alle" },
|
|
||||||
new() { Value = true, Text = "True" },
|
|
||||||
new() { Value = false, Text = "False" }
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly List<ProcedureOption> procedureOptions = new()
|
|
||||||
{
|
|
||||||
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
|
|
||||||
};
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
|
||||||
await EnsureLayoutUserAsync();
|
|
||||||
await LoadBandLayoutAsync();
|
|
||||||
await LoadPage(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadPage(int page)
|
|
||||||
{
|
|
||||||
isLoading = true;
|
|
||||||
errorMessage = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var total = await Api.GetCountAsync();
|
|
||||||
var effectivePageSize = pageSize ?? (total == 0 ? 1 : total);
|
|
||||||
pageCount = Math.Max(1, (int)Math.Ceiling(total / (double)effectivePageSize));
|
|
||||||
pageIndex = Math.Clamp(page, 0, pageCount - 1);
|
|
||||||
|
|
||||||
var skip = pageSize.HasValue ? pageIndex * pageSize.Value : 0;
|
|
||||||
items = await Api.GetAllAsync(skip, pageSize);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errorMessage = $"MassData konnten nicht geladen werden: {ex.Message}";
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
isLoading = false;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnPageChanged(int index)
|
|
||||||
{
|
|
||||||
await LoadPage(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnPageSizeChanged(int? size)
|
|
||||||
{
|
|
||||||
pageSize = size;
|
|
||||||
await LoadPage(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task EnsureLayoutUserAsync()
|
|
||||||
{
|
|
||||||
layoutUser = await JsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
||||||
{
|
|
||||||
layoutUser = Guid.NewGuid().ToString("N");
|
|
||||||
await JsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadBandLayoutAsync()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
||||||
{
|
|
||||||
bandLayout = new BandLayout();
|
|
||||||
UpdateBandOptions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var stored = await LayoutApi.GetAsync(LayoutType, LayoutKey, layoutUser);
|
|
||||||
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
|
|
||||||
{
|
|
||||||
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, jsonOptions);
|
|
||||||
bandLayout = NormalizeBandLayout(parsed);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bandLayout = new BandLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
columnBandAssignments = BuildAssignmentsFromLayout(bandLayout);
|
|
||||||
ApplyColumnLayoutFromStorage();
|
|
||||||
UpdateBandOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SaveLayoutAsync()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CaptureColumnLayoutFromGrid();
|
|
||||||
|
|
||||||
var layoutData = JsonSerializer.Serialize(bandLayout, jsonOptions);
|
|
||||||
await LayoutApi.UpsertAsync(new LayoutDto
|
|
||||||
{
|
|
||||||
LayoutType = LayoutType,
|
|
||||||
LayoutKey = LayoutKey,
|
|
||||||
UserName = layoutUser,
|
|
||||||
LayoutData = layoutData
|
|
||||||
});
|
|
||||||
infoMessage = "Layout gespeichert.";
|
|
||||||
errorMessage = null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CaptureColumnLayoutFromGrid()
|
|
||||||
{
|
|
||||||
if (gridRef == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var layout = gridRef.SaveLayout();
|
|
||||||
bandLayout.GridLayout = layout;
|
|
||||||
|
|
||||||
var orderedColumns = layout.Columns
|
|
||||||
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
|
|
||||||
.OrderBy(column => column.VisibleIndex)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
bandLayout.ColumnOrder = orderedColumns
|
|
||||||
.Select(column => column.FieldName)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
bandLayout.ColumnWidths = orderedColumns
|
|
||||||
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
|
|
||||||
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ResetBandLayoutAsync()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
|
|
||||||
bandLayout = new BandLayout();
|
|
||||||
columnBandAssignments.Clear();
|
|
||||||
UpdateBandOptions();
|
|
||||||
infoMessage = "Band-Layout zurückgesetzt.";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyColumnLayoutFromStorage()
|
|
||||||
{
|
|
||||||
foreach (var column in columnDefinitions)
|
|
||||||
{
|
|
||||||
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
|
||||||
{
|
|
||||||
column.Width = width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddBand()
|
|
||||||
{
|
|
||||||
bandLayout.Bands.Add(new BandDefinition
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid().ToString("N"),
|
|
||||||
Caption = "Band"
|
|
||||||
});
|
|
||||||
UpdateBandOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveBand(BandDefinition band)
|
|
||||||
{
|
|
||||||
bandLayout.Bands.Remove(band);
|
|
||||||
var removedColumns = columnBandAssignments.Where(pair => pair.Value == band.Id)
|
|
||||||
.Select(pair => pair.Key)
|
|
||||||
.ToList();
|
|
||||||
foreach (var column in removedColumns)
|
|
||||||
{
|
|
||||||
columnBandAssignments.Remove(column);
|
|
||||||
}
|
|
||||||
UpdateBandOptions();
|
|
||||||
SyncBandsFromAssignments();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateBandCaption(BandDefinition band, string value)
|
|
||||||
{
|
|
||||||
band.Caption = value;
|
|
||||||
UpdateBandOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateColumnBand(string fieldName, string? bandId)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(bandId))
|
|
||||||
{
|
|
||||||
columnBandAssignments.Remove(fieldName);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
columnBandAssignments[fieldName] = bandId;
|
|
||||||
}
|
|
||||||
|
|
||||||
SyncBandsFromAssignments();
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetColumnBand(string fieldName)
|
|
||||||
{
|
|
||||||
return columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SyncBandsFromAssignments()
|
|
||||||
{
|
|
||||||
foreach (var band in bandLayout.Bands)
|
|
||||||
{
|
|
||||||
band.Columns = columnDefinitions
|
|
||||||
.Where(column => columnBandAssignments.TryGetValue(column.FieldName, out var bandId) && bandId == band.Id)
|
|
||||||
.Select(column => column.FieldName)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateBandOptions()
|
|
||||||
{
|
|
||||||
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
|
||||||
bandOptions.AddRange(bandLayout.Bands.Select(band => new BandOption { Id = band.Id, Caption = band.Caption }));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
|
|
||||||
{
|
|
||||||
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var band in layout.Bands)
|
|
||||||
{
|
|
||||||
foreach (var column in band.Columns)
|
|
||||||
{
|
|
||||||
assignments[column] = band.Id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return assignments;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BandLayout NormalizeBandLayout(BandLayout? layout)
|
|
||||||
{
|
|
||||||
layout ??= new BandLayout();
|
|
||||||
layout.Bands ??= new List<BandDefinition>();
|
|
||||||
layout.ColumnOrder ??= new List<string>();
|
|
||||||
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var band in layout.Bands)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(band.Id))
|
|
||||||
{
|
|
||||||
band.Id = Guid.NewGuid().ToString("N");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(band.Caption))
|
|
||||||
{
|
|
||||||
band.Caption = "Band";
|
|
||||||
}
|
|
||||||
|
|
||||||
band.Columns = band.Columns?.Where(columnLookup.ContainsKey).ToList() ?? new List<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return layout;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RenderFragment RenderColumns() => builder =>
|
|
||||||
{
|
|
||||||
var seq = 0;
|
|
||||||
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
|
||||||
builder.AddAttribute(seq++, "Width", "120px");
|
|
||||||
builder.CloseComponent();
|
|
||||||
|
|
||||||
var grouped = bandLayout.Bands.SelectMany(band => band.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var column in columnDefinitions.Where(column => !grouped.Contains(column.FieldName)))
|
|
||||||
{
|
|
||||||
BuildDataColumn(builder, ref seq, column);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var band in bandLayout.Bands)
|
|
||||||
{
|
|
||||||
if (band.Columns.Count == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.OpenComponent<DxGridBandColumn>(seq++);
|
|
||||||
builder.AddAttribute(seq++, "Caption", band.Caption);
|
|
||||||
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
|
||||||
{
|
|
||||||
var bandSeq = 0;
|
|
||||||
foreach (var columnName in band.Columns)
|
|
||||||
{
|
|
||||||
if (columnLookup.TryGetValue(columnName, out var column))
|
|
||||||
{
|
|
||||||
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
builder.CloseComponent();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
|
|
||||||
{
|
|
||||||
builder.OpenComponent<DxGridDataColumn>(seq++);
|
|
||||||
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
|
||||||
builder.AddAttribute(seq++, "Caption", column.Caption);
|
|
||||||
if (!string.IsNullOrWhiteSpace(column.Width))
|
|
||||||
{
|
|
||||||
builder.AddAttribute(seq++, "Width", column.Width);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
|
||||||
{
|
|
||||||
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.ReadOnly)
|
|
||||||
{
|
|
||||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.CloseComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetEditContext(EditContext context)
|
|
||||||
{
|
|
||||||
if (editContext == context)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editContext != null)
|
|
||||||
{
|
|
||||||
editContext.OnFieldChanged -= OnEditFieldChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
editContext = context;
|
|
||||||
validationMessageStore = new ValidationMessageStore(editContext);
|
|
||||||
editContext.OnFieldChanged += OnEditFieldChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (validationMessageStore == null || editContext == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure))
|
|
||||||
{
|
|
||||||
validationMessageStore.Clear();
|
|
||||||
editContext.NotifyValidationStateChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.CustomerName))
|
|
||||||
{
|
|
||||||
var field = new FieldIdentifier(editContext.Model, nameof(MassDataEditModel.CustomerName));
|
|
||||||
validationMessageStore.Clear(field);
|
|
||||||
editContext.NotifyValidationStateChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetPopupHeaderText(bool isNew)
|
|
||||||
{
|
|
||||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.IsNew)
|
|
||||||
{
|
|
||||||
e.EditModel = new MassDataEditModel { IsNew = true, UpdateProcedure = procedureOptions[0].Value };
|
|
||||||
SetPopupHeaderText(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var item = (MassDataReadDto)e.DataItem;
|
|
||||||
e.EditModel = new MassDataEditModel
|
|
||||||
{
|
|
||||||
Id = item.Id,
|
|
||||||
CustomerName = item.CustomerName,
|
|
||||||
AmountText = item.Amount.ToString("0.00"),
|
|
||||||
Category = item.Category,
|
|
||||||
StatusFlag = item.StatusFlag,
|
|
||||||
UpdateProcedure = procedureOptions[0].Value,
|
|
||||||
IsNew = false,
|
|
||||||
OriginalCustomerName = item.CustomerName
|
|
||||||
};
|
|
||||||
SetPopupHeaderText(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
|
|
||||||
{
|
|
||||||
errorMessage = null;
|
|
||||||
infoMessage = null;
|
|
||||||
|
|
||||||
validationMessageStore?.Clear();
|
|
||||||
editContext?.NotifyValidationStateChanged();
|
|
||||||
|
|
||||||
var editModel = (MassDataEditModel)e.EditModel;
|
|
||||||
if (!decimal.TryParse(editModel.AmountText, out var amount))
|
|
||||||
{
|
|
||||||
AddValidationError(editModel, nameof(MassDataEditModel.AmountText), "Amount ist ungültig.");
|
|
||||||
e.Cancel = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editModel.IsNew)
|
|
||||||
{
|
|
||||||
var existing = await Api.GetByCustomerNameAsync(editModel.CustomerName);
|
|
||||||
if (existing != null)
|
|
||||||
{
|
|
||||||
AddValidationError(editModel, nameof(MassDataEditModel.CustomerName), "Kunde existiert bereits.");
|
|
||||||
e.Cancel = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var dto = new MassDataWriteDto
|
|
||||||
{
|
|
||||||
CustomerName = editModel.CustomerName,
|
|
||||||
Amount = amount,
|
|
||||||
Category = editModel.Category,
|
|
||||||
StatusFlag = editModel.StatusFlag
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Api.UpsertAsync(dto);
|
|
||||||
infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert.";
|
|
||||||
await LoadPage(pageIndex);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errorMessage = $"Fehler beim Speichern: {ex.Message}";
|
|
||||||
e.Cancel = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddValidationError(MassDataEditModel editModel, string fieldName, string message)
|
|
||||||
{
|
|
||||||
if (editContext == null || validationMessageStore == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var field = new FieldIdentifier(editModel, fieldName);
|
|
||||||
validationMessageStore.Add(field, message);
|
|
||||||
editContext.NotifyValidationStateChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task OnDataItemDeleting(GridDataItemDeletingEventArgs e)
|
|
||||||
{
|
|
||||||
errorMessage = null;
|
|
||||||
infoMessage = "Löschen ist aktuell noch nicht verfügbar.";
|
|
||||||
e.Cancel = true;
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class BandLayout
|
|
||||||
{
|
|
||||||
public List<BandDefinition> Bands { get; set; } = new();
|
|
||||||
public List<string> ColumnOrder { get; set; } = new();
|
|
||||||
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
public GridPersistentLayout? GridLayout { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class BandDefinition
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
public string Caption { get; set; } = string.Empty;
|
|
||||||
public List<string> Columns { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class BandOption
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
public string Caption { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ColumnDefinition
|
|
||||||
{
|
|
||||||
public string FieldName { get; init; } = string.Empty;
|
|
||||||
public string Caption { get; init; } = string.Empty;
|
|
||||||
public string? Width { get; set; }
|
|
||||||
public string? DisplayFormat { get; init; }
|
|
||||||
public bool ReadOnly { get; init; }
|
|
||||||
public ColumnFilterType FilterType { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum ColumnFilterType
|
|
||||||
{
|
|
||||||
Text,
|
|
||||||
Bool,
|
|
||||||
Date
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class MassDataEditModel
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string CustomerName { get; set; } = string.Empty;
|
|
||||||
public string AmountText { get; set; } = string.Empty;
|
|
||||||
public string Category { get; set; } = string.Empty;
|
|
||||||
public bool StatusFlag { get; set; }
|
|
||||||
public int UpdateProcedure { get; set; }
|
|
||||||
public bool IsNew { get; set; }
|
|
||||||
public string OriginalCustomerName { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ProcedureOption
|
|
||||||
{
|
|
||||||
public int Value { get; set; }
|
|
||||||
public string Text { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class BoolFilterOption
|
|
||||||
{
|
|
||||||
public bool? Value { get; set; }
|
|
||||||
public string Text { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class PageSizeOption
|
|
||||||
{
|
|
||||||
public int? Value { get; set; }
|
|
||||||
public string Text { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool gridLayoutApplied;
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
|
||||||
{
|
|
||||||
gridRef.LoadLayout(bandLayout.GridLayout);
|
|
||||||
gridLayoutApplied = true;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="DevExpress.Blazor" Version="25.2.3" />
|
|
||||||
<PackageReference Include="DevExpress.Blazor.Dashboard" Version="25.2.3" />
|
|
||||||
<PackageReference Include="DevExpress.Blazor.Themes" Version="25.2.3" />
|
|
||||||
<PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.22" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.22" PrivateAssets="all" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.22" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="wwwroot\sample-data\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
@* Definiert das Hauptlayout der Anwendung.
|
|
||||||
Enthält die Navigationsleiste und den Hauptinhalt. *@
|
|
||||||
|
|
||||||
@inherits LayoutComponentBase
|
|
||||||
<div class="page">
|
|
||||||
<div class="sidebar">
|
|
||||||
<NavMenu /> <!-- Einbindung der Navigationsleiste -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<div class="top-row px-4">
|
|
||||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<article class="content px-4">
|
|
||||||
@Body <!-- Platzhalter für den Seiteninhalt -->
|
|
||||||
</article>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
.page {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row {
|
|
||||||
background-color: #f7f7f7;
|
|
||||||
border-bottom: 1px solid #d6d5d5;
|
|
||||||
justify-content: flex-end;
|
|
||||||
height: 3.5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row ::deep a:first-child {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640.98px) {
|
|
||||||
.top-row {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 641px) {
|
|
||||||
.page {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 250px;
|
|
||||||
height: 100vh;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row.auth ::deep a:first-child {
|
|
||||||
flex: 1;
|
|
||||||
text-align: right;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row, article {
|
|
||||||
padding-left: 2rem !important;
|
|
||||||
padding-right: 1.5rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
@* Definiert die Navigationsleiste, die Links zu verschiedenen Seiten der Anwendung enthält. *@
|
|
||||||
|
|
||||||
<div class="top-row ps-3 navbar navbar-dark">
|
|
||||||
<div class="container-fluid">
|
|
||||||
<a class="navbar-brand" href="">DbFirst.BlazorWasm</a>
|
|
||||||
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
|
||||||
<nav class="flex-column">
|
|
||||||
<div class="nav-item px-3">
|
|
||||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
|
||||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item px-3">
|
|
||||||
<NavLink class="nav-link" href="catalogs">
|
|
||||||
<span class="bi bi-collection-nav-menu" aria-hidden="true"></span> Catalogs
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item px-3">
|
|
||||||
<NavLink class="nav-link" href="dashboards">
|
|
||||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Dashboards
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item px-3">
|
|
||||||
<NavLink class="nav-link" href="massdata">
|
|
||||||
<span class="bi bi-table" aria-hidden="true"></span> MassData
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private bool collapseNavMenu = true;
|
|
||||||
|
|
||||||
// CSS-Klasse für die Navigation, die den Zustand (eingeklappt/ausgeklappt) steuert.
|
|
||||||
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
|
||||||
|
|
||||||
// Methode zum Umschalten des Navigationsmenüs.
|
|
||||||
private void ToggleNavMenu()
|
|
||||||
{
|
|
||||||
collapseNavMenu = !collapseNavMenu;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
.navbar-toggler {
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row {
|
|
||||||
height: 3.5rem;
|
|
||||||
background-color: rgba(0,0,0,0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bi {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
top: -1px;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bi-house-door-fill-nav-menu {
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
|
|
||||||
.bi-plus-square-fill-nav-menu {
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
|
|
||||||
.bi-list-nested-nav-menu {
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item:first-of-type {
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item:last-of-type {
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item ::deep a {
|
|
||||||
color: #d7d7d7;
|
|
||||||
border-radius: 4px;
|
|
||||||
height: 3rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
line-height: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item ::deep a.active {
|
|
||||||
background-color: rgba(255,255,255,0.37);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item ::deep a:hover {
|
|
||||||
background-color: rgba(255,255,255,0.1);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 641px) {
|
|
||||||
.navbar-toggler {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapse {
|
|
||||||
/* Never collapse the sidebar for wide screens */
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-scrollable {
|
|
||||||
/* Allow sidebar to scroll for tall menus */
|
|
||||||
height: calc(100vh - 3.5rem);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace DbFirst.BlazorWasm.Models;
|
|
||||||
|
|
||||||
public class CatalogReadDto
|
|
||||||
{
|
|
||||||
public int Guid { get; set; }
|
|
||||||
public string CatTitle { get; set; } = string.Empty;
|
|
||||||
public string CatString { get; set; } = string.Empty;
|
|
||||||
public string AddedWho { get; set; } = string.Empty;
|
|
||||||
public DateTime AddedWhen { get; set; }
|
|
||||||
public string? ChangedWho { get; set; }
|
|
||||||
public DateTime? ChangedWhen { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace DbFirst.BlazorWasm.Models;
|
|
||||||
|
|
||||||
public class CatalogWriteDto
|
|
||||||
{
|
|
||||||
public string CatTitle { get; set; } = string.Empty;
|
|
||||||
public string CatString { get; set; } = string.Empty;
|
|
||||||
public int UpdateProcedure { get; set; } = 0; // 0 = Update, 1 = Save
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace DbFirst.BlazorWasm.Models;
|
|
||||||
|
|
||||||
public class DashboardInfoDto
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace DbFirst.BlazorWasm.Models;
|
|
||||||
|
|
||||||
public class LayoutDto
|
|
||||||
{
|
|
||||||
public string LayoutType { get; set; } = string.Empty;
|
|
||||||
public string LayoutKey { get; set; } = string.Empty;
|
|
||||||
public string UserName { get; set; } = string.Empty;
|
|
||||||
public string LayoutData { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace DbFirst.BlazorWasm.Models;
|
|
||||||
|
|
||||||
public class MassDataReadDto
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string CustomerName { get; set; } = string.Empty;
|
|
||||||
public decimal Amount { get; set; }
|
|
||||||
public string Category { get; set; } = string.Empty;
|
|
||||||
public bool StatusFlag { get; set; }
|
|
||||||
public DateTime AddedWhen { get; set; }
|
|
||||||
public DateTime? ChangedWhen { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace DbFirst.BlazorWasm.Models;
|
|
||||||
|
|
||||||
public class MassDataWriteDto
|
|
||||||
{
|
|
||||||
public string CustomerName { get; set; } = string.Empty;
|
|
||||||
public decimal Amount { get; set; }
|
|
||||||
public string Category { get; set; } = string.Empty;
|
|
||||||
public bool StatusFlag { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
@page "/catalogs"
|
|
||||||
|
|
||||||
<PageTitle>Catalogs</PageTitle>
|
|
||||||
|
|
||||||
<h1>Catalogs</h1>
|
|
||||||
|
|
||||||
<CatalogsGrid />
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
@page "/dashboard"
|
|
||||||
@page "/dashboards/{DashboardId?}"
|
|
||||||
@implements IAsyncDisposable
|
|
||||||
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
@inject DashboardApiClient DashboardApi
|
|
||||||
|
|
||||||
<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>
|
|
||||||
@if (dashboards.Count == 0)
|
|
||||||
{
|
|
||||||
<div class="px-3 py-2 text-muted">Keine Dashboards vorhanden.</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
@foreach (var dashboard in dashboards)
|
|
||||||
{
|
|
||||||
<NavLink class="dashboard-nav-link" href="@($"dashboards/{dashboard.Id}?mode={(IsDesigner ? "designer" : "viewer")}")">@dashboard.Name</NavLink>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</aside>
|
|
||||||
<section class="dashboard-content">
|
|
||||||
<div class="mb-3">
|
|
||||||
<DxButton RenderStyle="ButtonRenderStyle.Primary" Click="@ToggleMode">
|
|
||||||
@(IsDesigner ? "Zum Viewer wechseln" : "Zum Designer wechseln")
|
|
||||||
</DxButton>
|
|
||||||
</div>
|
|
||||||
<DxDashboard @key="DashboardKey" Endpoint="@DashboardEndpoint" InitialDashboardId="@SelectedDashboardId" WorkingMode="@CurrentMode" style="width: 100%; height: 800px;">
|
|
||||||
</DxDashboard>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public string? DashboardId { get; set; }
|
|
||||||
[SupplyParameterFromQuery] public string? Mode { get; set; }
|
|
||||||
|
|
||||||
private readonly List<DashboardInfoDto> dashboards = new();
|
|
||||||
private HubConnection? _hubConnection;
|
|
||||||
|
|
||||||
private bool IsDesigner => !string.Equals(Mode, "viewer", StringComparison.OrdinalIgnoreCase);
|
|
||||||
private WorkingMode CurrentMode => IsDesigner ? WorkingMode.Designer : WorkingMode.ViewerOnly;
|
|
||||||
private string SelectedDashboardId { get; set; } = string.Empty;
|
|
||||||
private string DashboardKey => $"{SelectedDashboardId}-{(IsDesigner ? "designer" : "viewer")}";
|
|
||||||
|
|
||||||
private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
|
|
||||||
private string HubEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/hubs/dashboards";
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
await RefreshDashboards();
|
|
||||||
|
|
||||||
_hubConnection = new HubConnectionBuilder()
|
|
||||||
.WithUrl(HubEndpoint)
|
|
||||||
.WithAutomaticReconnect()
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
_hubConnection.On("DashboardsChanged", async () =>
|
|
||||||
{
|
|
||||||
await RefreshDashboards();
|
|
||||||
});
|
|
||||||
|
|
||||||
await _hubConnection.StartAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
|
||||||
{
|
|
||||||
if (dashboards.Count == 0)
|
|
||||||
{
|
|
||||||
await RefreshDashboards();
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase)
|
|
||||||
? null
|
|
||||||
: DashboardId;
|
|
||||||
|
|
||||||
var resolved = !string.IsNullOrWhiteSpace(requestedId)
|
|
||||||
? dashboards.FirstOrDefault(d => string.Equals(d.Id, requestedId, StringComparison.OrdinalIgnoreCase))
|
|
||||||
: dashboards.FirstOrDefault(d => string.Equals(d.Id, "DefaultDashboard", StringComparison.OrdinalIgnoreCase))
|
|
||||||
?? dashboards.FirstOrDefault();
|
|
||||||
|
|
||||||
if (resolved == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SelectedDashboardId = resolved.Id;
|
|
||||||
|
|
||||||
if (!string.Equals(DashboardId, resolved.Id, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo($"dashboards/{resolved.Id}?mode={(IsDesigner ? "designer" : "viewer")}", replace: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ToggleMode()
|
|
||||||
{
|
|
||||||
var targetMode = IsDesigner ? "viewer" : "designer";
|
|
||||||
Navigation.NavigateTo($"dashboards/{SelectedDashboardId}?mode={targetMode}", replace: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshDashboards()
|
|
||||||
{
|
|
||||||
var latest = await DashboardApi.GetAllAsync();
|
|
||||||
if (latest.Count == dashboards.Count && latest.All(d => dashboards.Any(x => x.Id == d.Id && x.Name == d.Name)))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dashboards.Clear();
|
|
||||||
dashboards.AddRange(latest);
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
if (_hubConnection != null)
|
|
||||||
{
|
|
||||||
await _hubConnection.DisposeAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
@page "/"
|
|
||||||
|
|
||||||
<PageTitle>Home</PageTitle>
|
|
||||||
|
|
||||||
<h1>Db First approach</h1>
|
|
||||||
|
|
||||||
This is a Blazor WebAssembly application demonstrating the Database First approach using DevExpress components.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
@page "/massdata"
|
|
||||||
|
|
||||||
<PageTitle>MassData</PageTitle>
|
|
||||||
|
|
||||||
<h1>MassData</h1>
|
|
||||||
|
|
||||||
<MassDataGrid />
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/* Initialisiert die Blazor WebAssembly-Anwendung.
|
|
||||||
Registriert Root-Komponenten
|
|
||||||
Konfiguriert Abhängigkeiten */
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
|
||||||
using DbFirst.BlazorWasm;
|
|
||||||
using DbFirst.BlazorWasm.Services;
|
|
||||||
using DevExpress.Blazor;
|
|
||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
|
||||||
builder.RootComponents.Add<App>("#app");
|
|
||||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
|
||||||
|
|
||||||
builder.Services.AddDevExpressBlazor();
|
|
||||||
|
|
||||||
var apiBaseUrl = builder.Configuration["ApiBaseUrl"] ?? builder.HostEnvironment.BaseAddress;
|
|
||||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiBaseUrl) });
|
|
||||||
builder.Services.AddScoped<CatalogApiClient>();
|
|
||||||
builder.Services.AddScoped<DashboardApiClient>();
|
|
||||||
builder.Services.AddScoped<MassDataApiClient>();
|
|
||||||
builder.Services.AddScoped<LayoutApiClient>();
|
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
|
||||||
"iisSettings": {
|
|
||||||
"windowsAuthentication": false,
|
|
||||||
"anonymousAuthentication": true,
|
|
||||||
"iisExpress": {
|
|
||||||
"applicationUrl": "http://localhost:12804",
|
|
||||||
"sslPort": 44394
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profiles": {
|
|
||||||
"http": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
|
||||||
"applicationUrl": "http://localhost:5101",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": true,
|
|
||||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
|
||||||
"applicationUrl": "https://localhost:7276;http://localhost:5101",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"IIS Express": {
|
|
||||||
"commandName": "IISExpress",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
/* Kapselt die Kommunikation mit der API für den Catalog-Endpunkt.
|
|
||||||
Bietet Methoden für CRUD-Operationen auf Catalog-Daten */
|
|
||||||
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http.Json;
|
|
||||||
using System.Text.Json;
|
|
||||||
using DbFirst.BlazorWasm.Models;
|
|
||||||
|
|
||||||
namespace DbFirst.BlazorWasm.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)
|
|
||||||
{
|
|
||||||
// Liest und analysiert Fehlerdetails aus der API-Antwort.
|
|
||||||
// Gibt eine benutzerfreundliche Fehlermeldung zurück.
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
// ignore parse errors
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Friendly overrides
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
// Formatiert zusätzliche Informationen für Fehlermeldungen.
|
|
||||||
// Kombiniert Titel, Details und Grund in einer lesbaren Form.
|
|
||||||
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using System.Net.Http.Json;
|
|
||||||
using DbFirst.BlazorWasm.Models;
|
|
||||||
|
|
||||||
namespace DbFirst.BlazorWasm.Services;
|
|
||||||
|
|
||||||
public class DashboardApiClient
|
|
||||||
{
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
private const string Endpoint = "api/dashboard/dashboards";
|
|
||||||
|
|
||||||
public DashboardApiClient(HttpClient httpClient)
|
|
||||||
{
|
|
||||||
_httpClient = httpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<DashboardInfoDto>> GetAllAsync()
|
|
||||||
{
|
|
||||||
var result = await _httpClient.GetFromJsonAsync<List<DashboardInfoDto>>(Endpoint);
|
|
||||||
return result ?? new List<DashboardInfoDto>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
using System.Net.Http.Json;
|
|
||||||
using DbFirst.BlazorWasm.Models;
|
|
||||||
|
|
||||||
namespace DbFirst.BlazorWasm.Services;
|
|
||||||
|
|
||||||
public class LayoutApiClient
|
|
||||||
{
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
private const string Endpoint = "api/layouts";
|
|
||||||
|
|
||||||
public LayoutApiClient(HttpClient httpClient)
|
|
||||||
{
|
|
||||||
_httpClient = httpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<LayoutDto?> GetAsync(string layoutType, string layoutKey, string userName)
|
|
||||||
{
|
|
||||||
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
|
|
||||||
var response = await _httpClient.GetAsync(url);
|
|
||||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
return await response.Content.ReadFromJsonAsync<LayoutDto>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<LayoutDto> UpsertAsync(LayoutDto dto)
|
|
||||||
{
|
|
||||||
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var detail = await ReadErrorAsync(response);
|
|
||||||
throw new InvalidOperationException(detail);
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload = await response.Content.ReadFromJsonAsync<LayoutDto>();
|
|
||||||
return payload ?? dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
|
|
||||||
{
|
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
|
||||||
if (!string.IsNullOrWhiteSpace(body))
|
|
||||||
{
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"{(int)response.StatusCode} {response.ReasonPhrase}".Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteAsync(string layoutType, string layoutKey, string userName)
|
|
||||||
{
|
|
||||||
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
|
|
||||||
var response = await _httpClient.DeleteAsync(url);
|
|
||||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
using System.Net.Http.Json;
|
|
||||||
using DbFirst.BlazorWasm.Models;
|
|
||||||
|
|
||||||
namespace DbFirst.BlazorWasm.Services;
|
|
||||||
|
|
||||||
public class MassDataApiClient
|
|
||||||
{
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
private const string Endpoint = "api/massdata";
|
|
||||||
|
|
||||||
public MassDataApiClient(HttpClient httpClient)
|
|
||||||
{
|
|
||||||
_httpClient = httpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> GetCountAsync()
|
|
||||||
{
|
|
||||||
var result = await _httpClient.GetFromJsonAsync<int?>("api/massdata/count");
|
|
||||||
return result ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<MassDataReadDto>> GetAllAsync(int? skip, int? take)
|
|
||||||
{
|
|
||||||
var query = new List<string>();
|
|
||||||
if (skip.HasValue)
|
|
||||||
{
|
|
||||||
query.Add($"skip={skip.Value}");
|
|
||||||
}
|
|
||||||
if (take.HasValue)
|
|
||||||
{
|
|
||||||
query.Add($"take={take.Value}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = query.Count == 0 ? Endpoint : $"{Endpoint}?{string.Join("&", query)}";
|
|
||||||
var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>(url);
|
|
||||||
return result ?? new List<MassDataReadDto>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MassDataReadDto> UpsertAsync(MassDataWriteDto dto)
|
|
||||||
{
|
|
||||||
var response = await _httpClient.PostAsJsonAsync($"{Endpoint}/upsert", dto);
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>();
|
|
||||||
return payload ?? new MassDataReadDto();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(customerName))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = await _httpClient.GetAsync($"{Endpoint}/{Uri.EscapeDataString(customerName)}");
|
|
||||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
return await response.Content.ReadFromJsonAsync<MassDataReadDto>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
@using System.Net.Http
|
|
||||||
@using System.Net.Http.Json
|
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
|
||||||
@using Microsoft.AspNetCore.Components.Routing
|
|
||||||
@using Microsoft.AspNetCore.Components.Web
|
|
||||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
|
||||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
|
||||||
@using Microsoft.JSInterop
|
|
||||||
@using Microsoft.AspNetCore.SignalR.Client
|
|
||||||
@using DbFirst.BlazorWasm
|
|
||||||
@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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"ApiBaseUrl": "https://localhost:7204/"
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
html, body {
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a, .btn-link {
|
|
||||||
color: #0071c1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #1b6ec2;
|
|
||||||
border-color: #1861ac;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
|
||||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding-top: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.valid.modified:not([type=checkbox]) {
|
|
||||||
outline: 1px solid #26b050;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invalid {
|
|
||||||
outline: 1px solid red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.validation-message {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
#blazor-error-ui {
|
|
||||||
background: lightyellow;
|
|
||||||
bottom: 0;
|
|
||||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
|
||||||
display: none;
|
|
||||||
left: 0;
|
|
||||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#blazor-error-ui .dismiss {
|
|
||||||
cursor: pointer;
|
|
||||||
position: absolute;
|
|
||||||
right: 0.75rem;
|
|
||||||
top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blazor-error-boundary {
|
|
||||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
|
||||||
padding: 1rem 1rem 1rem 3.7rem;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blazor-error-boundary::after {
|
|
||||||
content: "An error has occurred."
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-progress {
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
width: 8rem;
|
|
||||||
height: 8rem;
|
|
||||||
margin: 20vh auto 1rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-progress circle {
|
|
||||||
fill: none;
|
|
||||||
stroke: #e0e0e0;
|
|
||||||
stroke-width: 0.6rem;
|
|
||||||
transform-origin: 50% 50%;
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-progress circle:last-child {
|
|
||||||
stroke: #1b6ec2;
|
|
||||||
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
|
||||||
transition: stroke-dasharray 0.05s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-progress-text {
|
|
||||||
position: absolute;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-progress-text:after {
|
|
||||||
content: var(--blazor-load-percentage-text, "Loading");
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
color: #c02d76;
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,58 +0,0 @@
|
|||||||
<!--
|
|
||||||
• Ist der technische Einstiegspunkt der Blazor WebAssembly-Anwendung.
|
|
||||||
• Sie lädt die notwendigen Ressourcen (z. B. das Blazor-Skript blazor.webassembly.js)
|
|
||||||
und definiert den Platzhalter <div id="app">, in dem die Blazor-Komponenten gerendert werden.
|
|
||||||
• Ohne diese Datei könnte die Blazor-Anwendung nicht starten, da sie die Verbindung
|
|
||||||
zwischen der statischen HTML-Welt und der Blazor-Welt herstellt.
|
|
||||||
kurz: Startet die Anwendung und lädt die Blazor-Umgebung.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>DbFirst.BlazorWasm</title>
|
|
||||||
<base href="/" />
|
|
||||||
|
|
||||||
<!-- Stylesheets für DevExpress und Bootstrap -->
|
|
||||||
|
|
||||||
<link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" />
|
|
||||||
<link href="_content/DevExpress.Blazor.Dashboard/ace-theme-dreamweaver.css" rel="stylesheet" />
|
|
||||||
<link href="_content/DevExpress.Blazor.Dashboard/ace-theme-ambiance.css" rel="stylesheet" />
|
|
||||||
<link href="_content/DevExpress.Blazor.Dashboard/dx.light.css" rel="stylesheet" />
|
|
||||||
<link href="_content/DevExpress.Blazor.Dashboard/dx-analytics.common.css" rel="stylesheet" />
|
|
||||||
<link href="_content/DevExpress.Blazor.Dashboard/dx-analytics.light.css" rel="stylesheet" />
|
|
||||||
<link href="_content/DevExpress.Blazor.Dashboard/dx-querybuilder.css" rel="stylesheet" />
|
|
||||||
<link href="_content/DevExpress.Blazor.Dashboard/dx-dashboard.light.min.css" rel="stylesheet" />
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
|
|
||||||
<link rel="stylesheet" href="css/app.css" />
|
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="favicon.png" />
|
|
||||||
<link href="DbFirst.BlazorWasm.styles.css" rel="stylesheet" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<!-- Einstiegspunkt der Blazor-Anwendung -->
|
|
||||||
<div id="app">
|
|
||||||
<svg class="loading-progress">
|
|
||||||
<circle r="40%" cx="50%" cy="50%" />
|
|
||||||
<circle r="40%" cx="50%" cy="50%" />
|
|
||||||
</svg>
|
|
||||||
<div class="loading-progress-text"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Fehler-UI für unvorhergesehene Fehler -->
|
|
||||||
<div id="blazor-error-ui">
|
|
||||||
An unhandled error has occurred.
|
|
||||||
<a href="" class="reload">Reload</a>
|
|
||||||
<a class="dismiss">🗙</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Blazor WebAssembly-Skript -->
|
|
||||||
<script src="_framework/blazor.webassembly.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -5,7 +5,10 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
@DxResourceManager.RegisterTheme(Themes.Fluent)
|
@DxResourceManager.RegisterTheme(Themes.Fluent.Clone(properties =>
|
||||||
|
{
|
||||||
|
properties.ApplyToPageElements = true;
|
||||||
|
}))
|
||||||
@DxResourceManager.RegisterScripts()
|
@DxResourceManager.RegisterScripts()
|
||||||
|
|
||||||
<link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" />
|
<link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" />
|
||||||
@@ -17,17 +20,12 @@
|
|||||||
<link href="_content/DevExpress.Blazor.Dashboard/dx-querybuilder.css" rel="stylesheet" />
|
<link href="_content/DevExpress.Blazor.Dashboard/dx-querybuilder.css" rel="stylesheet" />
|
||||||
<link href="_content/DevExpress.Blazor.Dashboard/dx-dashboard.light.min.css" rel="stylesheet" />
|
<link href="_content/DevExpress.Blazor.Dashboard/dx-dashboard.light.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes/bootstrap-external.bs5.min.css" />
|
|
||||||
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes.Fluent/core.min.css" />
|
|
||||||
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes.Fluent/global.min.css" />
|
|
||||||
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes.Fluent/modes/light.min.css" />
|
|
||||||
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes.Fluent/accents/blue.min.css" />
|
|
||||||
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes.Fluent/bootstrap/fluent-light.bs5.min.css" />
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
|
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="app.css" />
|
<link rel="stylesheet" href="app.css" />
|
||||||
<link rel="stylesheet" href="DbFirst.BlazorWebApp.styles.css" />
|
<link rel="stylesheet" href="DbFirst.BlazorWebApp.styles.css" />
|
||||||
<link rel="icon" type="image/png" href="favicon.png" />
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
|
<script src="js/size-manager.js"></script>
|
||||||
<HeadOutlet />
|
<HeadOutlet />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,5 @@
|
|||||||
@using System.Text.Json
|
|
||||||
@using Microsoft.AspNetCore.Components
|
|
||||||
@using Microsoft.AspNetCore.Components.Rendering
|
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
|
||||||
@using DevExpress.Blazor
|
|
||||||
@inject CatalogApiClient Api
|
@inject CatalogApiClient Api
|
||||||
@inject LayoutApiClient LayoutApi
|
@inject BandLayoutService BandLayoutService
|
||||||
@inject IJSRuntime JsRuntime
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.action-panel { margin-bottom: 16px; }
|
|
||||||
.grid-section { margin-top: 12px; }
|
|
||||||
.catalog-edit-popup {
|
|
||||||
min-width: 720px;
|
|
||||||
}
|
|
||||||
.band-editor {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.band-controls {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.band-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.band-columns {
|
|
||||||
max-width: 720px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||||
{
|
{
|
||||||
@@ -43,9 +10,13 @@ else if (!string.IsNullOrWhiteSpace(infoMessage))
|
|||||||
<div class="alert alert-success" role="alert">@infoMessage</div>
|
<div class="alert alert-success" role="alert">@infoMessage</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (isLoading)
|
@if (!hasLoaded || isLoading)
|
||||||
{
|
{
|
||||||
<p><em>Lade Daten...</em></p>
|
<div class="loading-container">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Lade...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
else if (items.Count == 0)
|
else if (items.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -54,40 +25,53 @@ else if (items.Count == 0)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="band-editor">
|
<div class="band-editor">
|
||||||
<div class="band-controls">
|
<button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded">
|
||||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
<span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">►</span>
|
||||||
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
<span>Layout</span>
|
||||||
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
</button>
|
||||||
</div>
|
@if (bandEditorExpanded)
|
||||||
@foreach (var band in bandLayout.Bands)
|
|
||||||
{
|
{
|
||||||
<div class="band-row">
|
<div class="band-editor-body">
|
||||||
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
|
<div class="band-controls">
|
||||||
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
|
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||||
|
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||||
|
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
|
||||||
|
</div>
|
||||||
|
@foreach (var band in bandLayout.Bands)
|
||||||
|
{
|
||||||
|
<div class="band-row">
|
||||||
|
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
|
||||||
|
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<DxFormLayout CssClass="band-columns" ColCount="2">
|
||||||
|
@foreach (var column in columnDefinitions)
|
||||||
|
{
|
||||||
|
<DxFormLayoutItem Caption="@column.Caption">
|
||||||
|
<DxComboBox Data="@bandOptions"
|
||||||
|
TData="BandOption"
|
||||||
|
TValue="string"
|
||||||
|
TextFieldName="Caption"
|
||||||
|
ValueFieldName="Id"
|
||||||
|
Value="@GetColumnBand(column.FieldName)"
|
||||||
|
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
|
||||||
|
Width="100%" />
|
||||||
|
</DxFormLayoutItem>
|
||||||
|
}
|
||||||
|
</DxFormLayout>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<DxFormLayout CssClass="band-columns" ColCount="2">
|
|
||||||
@foreach (var column in columnDefinitions)
|
|
||||||
{
|
|
||||||
<DxFormLayoutItem Caption="@column.Caption">
|
|
||||||
<DxComboBox Data="@bandOptions"
|
|
||||||
TData="BandOption"
|
|
||||||
TValue="string"
|
|
||||||
TextFieldName="Caption"
|
|
||||||
ValueFieldName="Id"
|
|
||||||
Value="@GetColumnBand(column.FieldName)"
|
|
||||||
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
|
|
||||||
Width="100%" />
|
|
||||||
</DxFormLayoutItem>
|
|
||||||
}
|
|
||||||
</DxFormLayout>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid-section">
|
<div class="grid-section">
|
||||||
<DxGrid Data="@items"
|
<DxGrid Data="@items"
|
||||||
TItem="CatalogReadDto"
|
TItem="CatalogReadDto"
|
||||||
KeyFieldName="@nameof(CatalogReadDto.Guid)"
|
KeyFieldName="@nameof(CatalogReadDto.Guid)"
|
||||||
ShowFilterRow="true"
|
SizeMode="@_sizeMode"
|
||||||
|
ShowGroupPanel="true"
|
||||||
|
ShowGroupedColumns="true"
|
||||||
|
AllowGroup="true"
|
||||||
|
FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always"
|
||||||
AllowColumnResize="true"
|
AllowColumnResize="true"
|
||||||
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||||
AllowColumnReorder="true"
|
AllowColumnReorder="true"
|
||||||
@@ -99,12 +83,35 @@ else
|
|||||||
CustomizeEditModel="OnCustomizeEditModel"
|
CustomizeEditModel="OnCustomizeEditModel"
|
||||||
EditModelSaving="OnEditModelSaving"
|
EditModelSaving="OnEditModelSaving"
|
||||||
DataItemDeleting="OnDataItemDeleting"
|
DataItemDeleting="OnDataItemDeleting"
|
||||||
|
FocusedRowEnabled="true"
|
||||||
|
@bind-FocusedRowKey="focusedRowKey"
|
||||||
@ref="gridRef">
|
@ref="gridRef">
|
||||||
|
<ToolbarTemplate>
|
||||||
|
<DxToolbar>
|
||||||
|
<DxToolbarItem Alignment="ToolbarItemAlignment.Right">
|
||||||
|
<Template Context="_">
|
||||||
|
<DxDropDownButton Text="@FormatSizeText(_sizeMode)"
|
||||||
|
RenderStyle="ButtonRenderStyle.Secondary"
|
||||||
|
RenderStyleMode="ButtonRenderStyleMode.Text"
|
||||||
|
ItemClick="OnSizeChange">
|
||||||
|
<Items>
|
||||||
|
@foreach (var size in _sizeModes)
|
||||||
|
{
|
||||||
|
<DxDropDownButtonItem Text="@FormatSizeText(size)" Id="@size.ToString()" />
|
||||||
|
}
|
||||||
|
</Items>
|
||||||
|
</DxDropDownButton>
|
||||||
|
</Template>
|
||||||
|
</DxToolbarItem>
|
||||||
|
</DxToolbar>
|
||||||
|
</ToolbarTemplate>
|
||||||
<Columns>
|
<Columns>
|
||||||
@RenderColumns()
|
@RenderColumns()
|
||||||
</Columns>
|
</Columns>
|
||||||
<EditFormTemplate Context="editFormContext">
|
<EditFormTemplate Context="editFormContext">
|
||||||
@{ SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
|
@{
|
||||||
|
SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew);
|
||||||
|
}
|
||||||
<DxFormLayout ColCount="2">
|
<DxFormLayout ColCount="2">
|
||||||
<DxFormLayoutItem Caption="Titel">
|
<DxFormLayoutItem Caption="Titel">
|
||||||
<DxTextBox @bind-Text="editModel.CatTitle" Width="100%" />
|
<DxTextBox @bind-Text="editModel.CatTitle" Width="100%" />
|
||||||
@@ -136,21 +143,24 @@ else
|
|||||||
@code {
|
@code {
|
||||||
private List<CatalogReadDto> items = new();
|
private List<CatalogReadDto> items = new();
|
||||||
private bool isLoading;
|
private bool isLoading;
|
||||||
|
private bool hasLoaded;
|
||||||
private string? errorMessage;
|
private string? errorMessage;
|
||||||
private string? infoMessage;
|
private string? infoMessage;
|
||||||
private EditContext? editContext;
|
private EditContext? editContext;
|
||||||
private ValidationMessageStore? validationMessageStore;
|
private ValidationMessageStore? validationMessageStore;
|
||||||
private IGrid? gridRef;
|
private IGrid? gridRef;
|
||||||
|
private int? focusedRowKey;
|
||||||
private string popupHeaderText = "Edit";
|
private string popupHeaderText = "Edit";
|
||||||
private const string LayoutType = "GRID_BANDS";
|
private const string LayoutType = "GRID_BANDS";
|
||||||
private const string LayoutKey = "CatalogsGrid";
|
private const string LayoutKey = "CatalogsGrid";
|
||||||
private const string LayoutUserStorageKey = "layoutUser";
|
|
||||||
private string? layoutUser;
|
private string? layoutUser;
|
||||||
private BandLayout bandLayout = new();
|
private BandLayout bandLayout = new();
|
||||||
private Dictionary<string, string> columnBandAssignments = new();
|
private Dictionary<string, string> columnBandAssignments = new();
|
||||||
private List<BandOption> bandOptions = new();
|
private List<BandOption> bandOptions = new();
|
||||||
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
||||||
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
|
private bool gridLayoutApplied;
|
||||||
|
private bool bandEditorExpanded;
|
||||||
|
|
||||||
private List<ColumnDefinition> columnDefinitions = new()
|
private List<ColumnDefinition> columnDefinitions = new()
|
||||||
{
|
{
|
||||||
new() { FieldName = nameof(CatalogReadDto.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text },
|
new() { FieldName = nameof(CatalogReadDto.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text },
|
||||||
@@ -170,26 +180,237 @@ else
|
|||||||
|
|
||||||
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||||
|
|
||||||
|
private SizeMode _sizeMode = SizeMode.Medium;
|
||||||
|
private static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
|
||||||
|
|
||||||
|
private string FormatSizeText(SizeMode size) => size switch
|
||||||
|
{
|
||||||
|
SizeMode.Small => "Klein",
|
||||||
|
SizeMode.Medium => "Mittel",
|
||||||
|
SizeMode.Large => "Groß",
|
||||||
|
_ => size.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
private void OnSizeChange(DropDownButtonItemClickEventArgs args)
|
||||||
|
{
|
||||||
|
_sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
await EnsureLayoutUserAsync();
|
layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
|
||||||
await LoadBandLayoutAsync();
|
bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
|
||||||
|
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
|
||||||
|
ApplyColumnLayoutFromStorage();
|
||||||
|
_sizeMode = bandLayout.SizeMode;
|
||||||
|
UpdateBandOptions();
|
||||||
await LoadCatalogs();
|
await LoadCatalogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
||||||
|
{
|
||||||
|
gridRef.LoadLayout(bandLayout.GridLayout);
|
||||||
|
gridLayoutApplied = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
hasLoaded = true;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveLayoutAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CaptureColumnLayoutFromGrid();
|
||||||
|
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
|
||||||
|
infoMessage = "Layout gespeichert.";
|
||||||
|
errorMessage = null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ResetLayoutAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
|
||||||
|
|
||||||
|
bandLayout = new BandLayout();
|
||||||
|
columnBandAssignments.Clear();
|
||||||
|
UpdateBandOptions();
|
||||||
|
|
||||||
|
foreach (var column in columnDefinitions)
|
||||||
|
column.Width = null;
|
||||||
|
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
_sizeMode = SizeMode.Medium;
|
||||||
|
|
||||||
|
if (gridRef != null)
|
||||||
|
gridRef.LoadLayout(new GridPersistentLayout());
|
||||||
|
gridLayoutApplied = false;
|
||||||
|
|
||||||
|
infoMessage = "Layout zurückgesetzt.";
|
||||||
|
errorMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CaptureColumnLayoutFromGrid()
|
||||||
|
{
|
||||||
|
if (gridRef == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var layout = gridRef.SaveLayout();
|
||||||
|
bandLayout.GridLayout = layout;
|
||||||
|
bandLayout.SizeMode = _sizeMode;
|
||||||
|
|
||||||
|
var orderedColumns = layout.Columns
|
||||||
|
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
|
||||||
|
.OrderBy(c => c.VisibleIndex)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
|
||||||
|
bandLayout.ColumnWidths = orderedColumns
|
||||||
|
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
|
||||||
|
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyColumnLayoutFromStorage()
|
||||||
|
{
|
||||||
|
foreach (var column in columnDefinitions)
|
||||||
|
{
|
||||||
|
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||||||
|
column.Width = width;
|
||||||
|
}
|
||||||
|
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddBand()
|
||||||
|
{
|
||||||
|
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
|
||||||
|
UpdateBandOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveBand(BandDefinition band)
|
||||||
|
{
|
||||||
|
bandLayout.Bands.Remove(band);
|
||||||
|
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
|
||||||
|
columnBandAssignments.Remove(key);
|
||||||
|
UpdateBandOptions();
|
||||||
|
SyncBandsFromAssignments();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateBandCaption(BandDefinition band, string value)
|
||||||
|
{
|
||||||
|
band.Caption = value;
|
||||||
|
UpdateBandOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateColumnBand(string fieldName, string? bandId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(bandId))
|
||||||
|
columnBandAssignments.Remove(fieldName);
|
||||||
|
else
|
||||||
|
columnBandAssignments[fieldName] = bandId;
|
||||||
|
SyncBandsFromAssignments();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetColumnBand(string fieldName)
|
||||||
|
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
||||||
|
|
||||||
|
private void SyncBandsFromAssignments()
|
||||||
|
{
|
||||||
|
foreach (var band in bandLayout.Bands)
|
||||||
|
{
|
||||||
|
band.Columns = columnDefinitions
|
||||||
|
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
|
||||||
|
.Select(c => c.FieldName)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateBandOptions()
|
||||||
|
{
|
||||||
|
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
||||||
|
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RenderFragment RenderColumns() => builder =>
|
||||||
|
{
|
||||||
|
var seq = 0;
|
||||||
|
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
||||||
|
builder.AddAttribute(seq++, "Width", "120px");
|
||||||
|
builder.CloseComponent();
|
||||||
|
|
||||||
|
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
|
||||||
|
BuildDataColumn(builder, ref seq, column);
|
||||||
|
|
||||||
|
foreach (var band in bandLayout.Bands)
|
||||||
|
{
|
||||||
|
if (band.Columns.Count == 0) continue;
|
||||||
|
|
||||||
|
builder.OpenComponent<DxGridBandColumn>(seq++);
|
||||||
|
builder.AddAttribute(seq++, "Caption", band.Caption);
|
||||||
|
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
||||||
|
{
|
||||||
|
var bandSeq = 0;
|
||||||
|
foreach (var columnName in band.Columns)
|
||||||
|
{
|
||||||
|
if (columnLookup.TryGetValue(columnName, out var column))
|
||||||
|
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
builder.CloseComponent();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
|
||||||
|
{
|
||||||
|
builder.OpenComponent<DxGridDataColumn>(seq++);
|
||||||
|
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
||||||
|
builder.AddAttribute(seq++, "Caption", column.Caption);
|
||||||
|
if (!string.IsNullOrWhiteSpace(column.Width))
|
||||||
|
builder.AddAttribute(seq++, "Width", column.Width);
|
||||||
|
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
||||||
|
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
||||||
|
if (column.ReadOnly)
|
||||||
|
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||||
|
builder.CloseComponent();
|
||||||
|
}
|
||||||
|
|
||||||
private void SetEditContext(EditContext context)
|
private void SetEditContext(EditContext context)
|
||||||
{
|
{
|
||||||
if (editContext == context)
|
if (editContext == context) return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editContext != null)
|
if (editContext != null)
|
||||||
{
|
|
||||||
editContext.OnFieldChanged -= OnEditFieldChanged;
|
editContext.OnFieldChanged -= OnEditFieldChanged;
|
||||||
}
|
|
||||||
|
|
||||||
editContext = context;
|
editContext = context;
|
||||||
validationMessageStore = new ValidationMessageStore(editContext);
|
validationMessageStore = new ValidationMessageStore(editContext);
|
||||||
editContext.OnFieldChanged += OnEditFieldChanged;
|
editContext.OnFieldChanged += OnEditFieldChanged;
|
||||||
@@ -197,10 +418,7 @@ else
|
|||||||
|
|
||||||
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (validationMessageStore == null || editContext == null)
|
if (validationMessageStore == null || editContext == null) return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.UpdateProcedure))
|
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.UpdateProcedure))
|
||||||
{
|
{
|
||||||
@@ -211,16 +429,12 @@ else
|
|||||||
|
|
||||||
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.CatTitle))
|
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.CatTitle))
|
||||||
{
|
{
|
||||||
var field = new FieldIdentifier(editContext.Model, nameof(CatalogEditModel.CatTitle));
|
validationMessageStore.Clear(new FieldIdentifier(editContext.Model, nameof(CatalogEditModel.CatTitle)));
|
||||||
validationMessageStore.Clear(field);
|
|
||||||
editContext.NotifyValidationStateChanged();
|
editContext.NotifyValidationStateChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetPopupHeaderText(bool isNew)
|
private void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
|
||||||
{
|
|
||||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -243,30 +457,10 @@ else
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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 async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
|
private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
|
||||||
{
|
{
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
infoMessage = null;
|
infoMessage = null;
|
||||||
|
|
||||||
validationMessageStore?.Clear();
|
validationMessageStore?.Clear();
|
||||||
editContext?.NotifyValidationStateChanged();
|
editContext?.NotifyValidationStateChanged();
|
||||||
|
|
||||||
@@ -292,19 +486,14 @@ else
|
|||||||
if (!created.Success || created.Value == null)
|
if (!created.Success || created.Value == null)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(created.Error))
|
if (!string.IsNullOrWhiteSpace(created.Error))
|
||||||
{
|
|
||||||
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), created.Error);
|
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), created.Error);
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
errorMessage = "Anlegen fehlgeschlagen.";
|
errorMessage = "Anlegen fehlgeschlagen.";
|
||||||
}
|
|
||||||
|
|
||||||
e.Cancel = true;
|
e.Cancel = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
infoMessage = "Katalog angelegt.";
|
infoMessage = "Katalog angelegt.";
|
||||||
|
focusedRowKey = created.Value.Guid;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -315,8 +504,8 @@ else
|
|||||||
e.Cancel = true;
|
e.Cancel = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
infoMessage = "Katalog aktualisiert.";
|
infoMessage = "Katalog aktualisiert.";
|
||||||
|
focusedRowKey = editModel.Guid;
|
||||||
}
|
}
|
||||||
|
|
||||||
await LoadCatalogs();
|
await LoadCatalogs();
|
||||||
@@ -330,30 +519,20 @@ else
|
|||||||
|
|
||||||
private void AddValidationError(CatalogEditModel editModel, string fieldName, string message)
|
private void AddValidationError(CatalogEditModel editModel, string fieldName, string message)
|
||||||
{
|
{
|
||||||
if (editContext == null || validationMessageStore == null)
|
if (editContext == null || validationMessageStore == null) return;
|
||||||
{
|
validationMessageStore.Add(new FieldIdentifier(editModel, fieldName), message);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var field = new FieldIdentifier(editModel, fieldName);
|
|
||||||
validationMessageStore.Add(field, message);
|
|
||||||
editContext.NotifyValidationStateChanged();
|
editContext.NotifyValidationStateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ValidateEditModel(CatalogEditModel editModel, bool isNew)
|
private bool ValidateEditModel(CatalogEditModel editModel, bool isNew)
|
||||||
{
|
{
|
||||||
if (isNew)
|
if (isNew) return true;
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editModel.UpdateProcedure == 0 &&
|
if (editModel.UpdateProcedure == 0 &&
|
||||||
!string.Equals(editModel.CatTitle, editModel.OriginalCatTitle, StringComparison.OrdinalIgnoreCase))
|
!string.Equals(editModel.CatTitle, editModel.OriginalCatTitle, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), "Titel kann nicht geändert werden.");
|
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), "Titel kann nicht geändert werden.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,9 +540,7 @@ else
|
|||||||
{
|
{
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
infoMessage = null;
|
infoMessage = null;
|
||||||
|
|
||||||
var item = (CatalogReadDto)e.DataItem;
|
var item = (CatalogReadDto)e.DataItem;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var deleted = await Api.DeleteAsync(item.Guid);
|
var deleted = await Api.DeleteAsync(item.Guid);
|
||||||
@@ -373,7 +550,6 @@ else
|
|||||||
e.Cancel = true;
|
e.Cancel = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
infoMessage = "Katalog gelöscht.";
|
infoMessage = "Katalog gelöscht.";
|
||||||
await LoadCatalogs();
|
await LoadCatalogs();
|
||||||
}
|
}
|
||||||
@@ -384,296 +560,6 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureLayoutUserAsync()
|
|
||||||
{
|
|
||||||
layoutUser = await JsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
||||||
{
|
|
||||||
layoutUser = Guid.NewGuid().ToString("N");
|
|
||||||
await JsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SaveLayoutAsync()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CaptureColumnLayoutFromGrid();
|
|
||||||
|
|
||||||
var layoutData = JsonSerializer.Serialize(bandLayout, jsonOptions);
|
|
||||||
await LayoutApi.UpsertAsync(new LayoutDto
|
|
||||||
{
|
|
||||||
LayoutType = LayoutType,
|
|
||||||
LayoutKey = LayoutKey,
|
|
||||||
UserName = layoutUser,
|
|
||||||
LayoutData = layoutData
|
|
||||||
});
|
|
||||||
infoMessage = "Layout gespeichert.";
|
|
||||||
errorMessage = null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CaptureColumnLayoutFromGrid()
|
|
||||||
{
|
|
||||||
if (gridRef == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var layout = gridRef.SaveLayout();
|
|
||||||
bandLayout.GridLayout = layout;
|
|
||||||
|
|
||||||
var orderedColumns = layout.Columns
|
|
||||||
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
|
|
||||||
.OrderBy(column => column.VisibleIndex)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
bandLayout.ColumnOrder = orderedColumns
|
|
||||||
.Select(column => column.FieldName)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
bandLayout.ColumnWidths = orderedColumns
|
|
||||||
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
|
|
||||||
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadBandLayoutAsync()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
||||||
{
|
|
||||||
bandLayout = new BandLayout();
|
|
||||||
UpdateBandOptions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var stored = await LayoutApi.GetAsync(LayoutType, LayoutKey, layoutUser);
|
|
||||||
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
|
|
||||||
{
|
|
||||||
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, jsonOptions);
|
|
||||||
bandLayout = NormalizeBandLayout(parsed);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bandLayout = new BandLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
columnBandAssignments = BuildAssignmentsFromLayout(bandLayout);
|
|
||||||
ApplyColumnLayoutFromStorage();
|
|
||||||
UpdateBandOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddBand()
|
|
||||||
{
|
|
||||||
bandLayout.Bands.Add(new BandDefinition
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid().ToString("N"),
|
|
||||||
Caption = "Band"
|
|
||||||
});
|
|
||||||
UpdateBandOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveBand(BandDefinition band)
|
|
||||||
{
|
|
||||||
bandLayout.Bands.Remove(band);
|
|
||||||
var removedColumns = columnBandAssignments.Where(pair => pair.Value == band.Id)
|
|
||||||
.Select(pair => pair.Key)
|
|
||||||
.ToList();
|
|
||||||
foreach (var column in removedColumns)
|
|
||||||
{
|
|
||||||
columnBandAssignments.Remove(column);
|
|
||||||
}
|
|
||||||
UpdateBandOptions();
|
|
||||||
SyncBandsFromAssignments();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateBandCaption(BandDefinition band, string value)
|
|
||||||
{
|
|
||||||
band.Caption = value;
|
|
||||||
UpdateBandOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateColumnBand(string fieldName, string? bandId)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(bandId))
|
|
||||||
{
|
|
||||||
columnBandAssignments.Remove(fieldName);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
columnBandAssignments[fieldName] = bandId;
|
|
||||||
}
|
|
||||||
|
|
||||||
SyncBandsFromAssignments();
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetColumnBand(string fieldName)
|
|
||||||
{
|
|
||||||
return columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SyncBandsFromAssignments()
|
|
||||||
{
|
|
||||||
foreach (var band in bandLayout.Bands)
|
|
||||||
{
|
|
||||||
band.Columns = columnDefinitions
|
|
||||||
.Where(column => columnBandAssignments.TryGetValue(column.FieldName, out var bandId) && bandId == band.Id)
|
|
||||||
.Select(column => column.FieldName)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateBandOptions()
|
|
||||||
{
|
|
||||||
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
|
||||||
bandOptions.AddRange(bandLayout.Bands.Select(band => new BandOption { Id = band.Id, Caption = band.Caption }));
|
|
||||||
}
|
|
||||||
|
|
||||||
private BandLayout NormalizeBandLayout(BandLayout? layout)
|
|
||||||
{
|
|
||||||
layout ??= new BandLayout();
|
|
||||||
layout.Bands ??= new List<BandDefinition>();
|
|
||||||
layout.ColumnOrder ??= new List<string>();
|
|
||||||
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var band in layout.Bands)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(band.Id))
|
|
||||||
{
|
|
||||||
band.Id = Guid.NewGuid().ToString("N");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(band.Caption))
|
|
||||||
{
|
|
||||||
band.Caption = "Band";
|
|
||||||
}
|
|
||||||
|
|
||||||
band.Columns = band.Columns?.Where(columnLookup.ContainsKey).ToList() ?? new List<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return layout;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
|
|
||||||
{
|
|
||||||
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var band in layout.Bands)
|
|
||||||
{
|
|
||||||
foreach (var column in band.Columns)
|
|
||||||
{
|
|
||||||
assignments[column] = band.Id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return assignments;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RenderFragment RenderColumns() => builder =>
|
|
||||||
{
|
|
||||||
var seq = 0;
|
|
||||||
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
|
||||||
builder.AddAttribute(seq++, "Width", "120px");
|
|
||||||
builder.CloseComponent();
|
|
||||||
|
|
||||||
var grouped = bandLayout.Bands.SelectMany(band => band.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var column in columnDefinitions.Where(column => !grouped.Contains(column.FieldName)))
|
|
||||||
{
|
|
||||||
BuildDataColumn(builder, ref seq, column);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var band in bandLayout.Bands)
|
|
||||||
{
|
|
||||||
if (band.Columns.Count == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.OpenComponent<DxGridBandColumn>(seq++);
|
|
||||||
builder.AddAttribute(seq++, "Caption", band.Caption);
|
|
||||||
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
|
||||||
{
|
|
||||||
var bandSeq = 0;
|
|
||||||
foreach (var columnName in band.Columns)
|
|
||||||
{
|
|
||||||
if (columnLookup.TryGetValue(columnName, out var column))
|
|
||||||
{
|
|
||||||
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
builder.CloseComponent();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
|
|
||||||
{
|
|
||||||
builder.OpenComponent<DxGridDataColumn>(seq++);
|
|
||||||
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
|
||||||
builder.AddAttribute(seq++, "Caption", column.Caption);
|
|
||||||
if (!string.IsNullOrWhiteSpace(column.Width))
|
|
||||||
{
|
|
||||||
builder.AddAttribute(seq++, "Width", column.Width);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
|
||||||
{
|
|
||||||
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.ReadOnly)
|
|
||||||
{
|
|
||||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.CloseComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class BandLayout
|
|
||||||
{
|
|
||||||
public List<BandDefinition> Bands { get; set; } = new();
|
|
||||||
public List<string> ColumnOrder { get; set; } = new();
|
|
||||||
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
public GridPersistentLayout? GridLayout { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class BandDefinition
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
public string Caption { get; set; } = string.Empty;
|
|
||||||
public List<string> Columns { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class BandOption
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
public string Caption { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ColumnDefinition
|
|
||||||
{
|
|
||||||
public string FieldName { get; init; } = string.Empty;
|
|
||||||
public string Caption { get; init; } = string.Empty;
|
|
||||||
public string? Width { get; set; }
|
|
||||||
public string? DisplayFormat { get; init; }
|
|
||||||
public bool ReadOnly { get; init; }
|
|
||||||
public ColumnFilterType FilterType { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum ColumnFilterType
|
|
||||||
{
|
|
||||||
Text,
|
|
||||||
Date
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class CatalogEditModel
|
private sealed class CatalogEditModel
|
||||||
{
|
{
|
||||||
public int Guid { get; set; }
|
public int Guid { get; set; }
|
||||||
@@ -689,43 +575,4 @@ else
|
|||||||
public int Value { get; set; }
|
public int Value { get; set; }
|
||||||
public string Text { get; set; } = string.Empty;
|
public string Text { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private async Task ResetBandLayoutAsync()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
|
|
||||||
bandLayout = new BandLayout();
|
|
||||||
columnBandAssignments.Clear();
|
|
||||||
UpdateBandOptions();
|
|
||||||
infoMessage = "Band-Layout zurückgesetzt.";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyColumnLayoutFromStorage()
|
|
||||||
{
|
|
||||||
foreach (var column in columnDefinitions)
|
|
||||||
{
|
|
||||||
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
|
||||||
{
|
|
||||||
column.Width = width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool gridLayoutApplied;
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
|
||||||
{
|
|
||||||
gridRef.LoadLayout(bandLayout.GridLayout);
|
|
||||||
gridLayoutApplied = true;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
DbFirst.BlazorWebApp/Components/CatalogsGrid.razor.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.catalog-edit-popup {
|
||||||
|
min-width: 720px;
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
@implements IDisposable
|
||||||
|
@inject ThemeState ThemeState
|
||||||
|
|
||||||
<div class="page">
|
<div class="page @(ThemeState.IsDarkMode ? "app-dark" : "app-light")">
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<NavMenu />
|
<NavMenu />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="top-row px-4">
|
<div class="top-row px-4">
|
||||||
|
<DxButton Text="@(ThemeState.IsDarkMode ? "Dark Mode aus" : "Dark Mode an")" Click="ToggleTheme" />
|
||||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -21,3 +24,20 @@
|
|||||||
<a href="" class="reload">Reload</a>
|
<a href="" class="reload">Reload</a>
|
||||||
<a class="dismiss">🗙</a>
|
<a class="dismiss">🗙</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
ThemeState.OnChange += StateHasChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleTheme()
|
||||||
|
{
|
||||||
|
ThemeState.SetDarkMode(!ThemeState.IsDarkMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
ThemeState.OnChange -= StateHasChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,14 +4,27 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page.app-dark {
|
||||||
|
background-color: #1b1b1b;
|
||||||
|
color: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page.app-dark main {
|
||||||
|
background-color: #1b1b1b;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page.app-dark .sidebar {
|
||||||
|
background-image: linear-gradient(180deg, #171717 0%, #0f2a46 70%);
|
||||||
|
}
|
||||||
|
|
||||||
.top-row {
|
.top-row {
|
||||||
background-color: #f7f7f7;
|
background-color: #f7f7f7;
|
||||||
border-bottom: 1px solid #d6d5d5;
|
border-bottom: 1px solid #d6d5d5;
|
||||||
@@ -21,6 +34,11 @@ main {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page.app-dark .top-row {
|
||||||
|
background-color: #222;
|
||||||
|
border-bottom-color: #3d3d3d;
|
||||||
|
}
|
||||||
|
|
||||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin-left: 1.5rem;
|
margin-left: 1.5rem;
|
||||||
|
|||||||
@@ -14,17 +14,6 @@
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-item px-3">
|
|
||||||
<NavLink class="nav-link" href="counter">
|
|
||||||
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav-item px-3">
|
|
||||||
<NavLink class="nav-link" href="weather">
|
|
||||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="catalogs">
|
<NavLink class="nav-link" href="catalogs">
|
||||||
<span class="bi bi-collection-nav-menu" aria-hidden="true"></span> Catalogs
|
<span class="bi bi-collection-nav-menu" aria-hidden="true"></span> Catalogs
|
||||||
@@ -33,13 +22,19 @@
|
|||||||
|
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="dashboards">
|
<NavLink class="nav-link" href="dashboards">
|
||||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Dashboards
|
<span class="bi bi-speedometer-nav-menu" aria-hidden="true"></span> Dashboards
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="massdata">
|
<NavLink class="nav-link" href="massdata">
|
||||||
<span class="bi bi-table" aria-hidden="true"></span> MassData
|
<span class="bi bi-table-nav-menu" aria-hidden="true"></span> MassData
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="clock">
|
||||||
|
<span class="bi bi-clock-nav-menu" aria-hidden="true"></span> Clock
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -46,6 +46,18 @@
|
|||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bi-collection-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M2 3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 .5.5v1H2V3z'/%3E%3Cpath d='M2 5h12v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-speedometer-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M1 11a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v4zm5 0a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v8zm5 0a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v2z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-table-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M1 2a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2zm1 1v2h12V3H2zm12 3H2v2h12V6zm0 3H2v2h12V9zm0 3H2v1h12v-1z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
|
|||||||
@@ -1,102 +1,5 @@
|
|||||||
@using System.Text.Json
|
|
||||||
@using Microsoft.AspNetCore.Components
|
|
||||||
@using Microsoft.AspNetCore.Components.Rendering
|
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
|
||||||
@using DevExpress.Blazor
|
|
||||||
@inject MassDataApiClient Api
|
@inject MassDataApiClient Api
|
||||||
@inject LayoutApiClient LayoutApi
|
@inject BandLayoutService BandLayoutService
|
||||||
@inject IJSRuntime JsRuntime
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.action-panel { margin-bottom: 16px; }
|
|
||||||
.grid-section { margin-top: 12px; }
|
|
||||||
.pager-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 12px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.page-size-selector {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
}
|
|
||||||
.page-size-label {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.page-size-combo {
|
|
||||||
width: 13ch;
|
|
||||||
min-width: 13ch;
|
|
||||||
max-width: 13ch;
|
|
||||||
}
|
|
||||||
.page-size-combo input {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.massdata-grid .dxbl-grid-sort-asc,
|
|
||||||
.massdata-grid .dxbl-grid-sort-desc {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.massdata-grid th.dxbl-grid-header-sortable {
|
|
||||||
position: relative;
|
|
||||||
padding-right: 1.5rem;
|
|
||||||
}
|
|
||||||
.massdata-grid th.dxbl-grid-header-sortable::before,
|
|
||||||
.massdata-grid th.dxbl-grid-header-sortable::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;
|
|
||||||
}
|
|
||||||
.massdata-grid th.dxbl-grid-header-sortable::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");
|
|
||||||
}
|
|
||||||
.massdata-grid th.dxbl-grid-header-sortable::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");
|
|
||||||
}
|
|
||||||
.massdata-grid th.dxbl-grid-header-sortable[aria-sort="ascending"]::after {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.massdata-grid th.dxbl-grid-header-sortable[aria-sort="descending"]::before {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.massdata-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;
|
|
||||||
}
|
|
||||||
.massdata-edit-popup {
|
|
||||||
min-width: 720px;
|
|
||||||
}
|
|
||||||
.band-editor {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.band-controls {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.band-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.band-columns {
|
|
||||||
max-width: 720px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||||
{
|
{
|
||||||
@@ -107,9 +10,13 @@ else if (!string.IsNullOrWhiteSpace(infoMessage))
|
|||||||
<div class="alert alert-success" role="alert">@infoMessage</div>
|
<div class="alert alert-success" role="alert">@infoMessage</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (isLoading)
|
@if (!hasLoaded || isLoading)
|
||||||
{
|
{
|
||||||
<p><em>Lade Daten...</em></p>
|
<div class="loading-container">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Lade...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
else if (items.Count == 0)
|
else if (items.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -117,6 +24,46 @@ else if (items.Count == 0)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
||||||
|
<div class="band-editor">
|
||||||
|
<button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded">
|
||||||
|
<span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">►</span>
|
||||||
|
<span>Layout</span>
|
||||||
|
</button>
|
||||||
|
@if (bandEditorExpanded)
|
||||||
|
{
|
||||||
|
<div class="band-editor-body">
|
||||||
|
<div class="band-controls">
|
||||||
|
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||||
|
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||||
|
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
|
||||||
|
</div>
|
||||||
|
@foreach (var band in bandLayout.Bands)
|
||||||
|
{
|
||||||
|
<div class="band-row">
|
||||||
|
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
|
||||||
|
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<DxFormLayout CssClass="band-columns" ColCount="2">
|
||||||
|
@foreach (var column in columnDefinitions)
|
||||||
|
{
|
||||||
|
<DxFormLayoutItem Caption="@column.Caption">
|
||||||
|
<DxComboBox Data="@bandOptions"
|
||||||
|
TData="BandOption"
|
||||||
|
TValue="string"
|
||||||
|
TextFieldName="Caption"
|
||||||
|
ValueFieldName="Id"
|
||||||
|
Value="@GetColumnBand(column.FieldName)"
|
||||||
|
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
|
||||||
|
Width="100%" />
|
||||||
|
</DxFormLayoutItem>
|
||||||
|
}
|
||||||
|
</DxFormLayout>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3 page-size-selector">
|
<div class="mb-3 page-size-selector">
|
||||||
<span class="page-size-label">Datensätze je Seite:</span>
|
<span class="page-size-label">Datensätze je Seite:</span>
|
||||||
<DxComboBox Data="@pageSizeOptions"
|
<DxComboBox Data="@pageSizeOptions"
|
||||||
@@ -129,41 +76,15 @@ else
|
|||||||
CssClass="page-size-combo" />
|
CssClass="page-size-combo" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="band-editor">
|
|
||||||
<div class="band-controls">
|
|
||||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
|
||||||
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
|
||||||
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
|
||||||
</div>
|
|
||||||
@foreach (var band in bandLayout.Bands)
|
|
||||||
{
|
|
||||||
<div class="band-row">
|
|
||||||
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
|
|
||||||
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<DxFormLayout CssClass="band-columns" ColCount="2">
|
|
||||||
@foreach (var column in columnDefinitions)
|
|
||||||
{
|
|
||||||
<DxFormLayoutItem Caption="@column.Caption">
|
|
||||||
<DxComboBox Data="@bandOptions"
|
|
||||||
TData="BandOption"
|
|
||||||
TValue="string"
|
|
||||||
TextFieldName="Caption"
|
|
||||||
ValueFieldName="Id"
|
|
||||||
Value="@GetColumnBand(column.FieldName)"
|
|
||||||
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
|
|
||||||
Width="100%" />
|
|
||||||
</DxFormLayoutItem>
|
|
||||||
}
|
|
||||||
</DxFormLayout>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid-section">
|
<div class="grid-section">
|
||||||
<DxGrid Data="@items"
|
<DxGrid Data="@items"
|
||||||
TItem="MassDataReadDto"
|
TItem="MassDataReadDto"
|
||||||
KeyFieldName="@nameof(MassDataReadDto.Id)"
|
KeyFieldName="@nameof(MassDataReadDto.Id)"
|
||||||
ShowFilterRow="true"
|
SizeMode="@_sizeMode"
|
||||||
|
ShowGroupPanel="true"
|
||||||
|
ShowGroupedColumns="true"
|
||||||
|
AllowGroup="true"
|
||||||
|
FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always"
|
||||||
AllowColumnResize="true"
|
AllowColumnResize="true"
|
||||||
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||||
AllowColumnReorder="true"
|
AllowColumnReorder="true"
|
||||||
@@ -175,12 +96,35 @@ else
|
|||||||
CustomizeEditModel="OnCustomizeEditModel"
|
CustomizeEditModel="OnCustomizeEditModel"
|
||||||
EditModelSaving="OnEditModelSaving"
|
EditModelSaving="OnEditModelSaving"
|
||||||
DataItemDeleting="OnDataItemDeleting"
|
DataItemDeleting="OnDataItemDeleting"
|
||||||
|
FocusedRowEnabled="true"
|
||||||
|
@bind-FocusedRowKey="focusedRowKey"
|
||||||
@ref="gridRef">
|
@ref="gridRef">
|
||||||
|
<ToolbarTemplate>
|
||||||
|
<DxToolbar>
|
||||||
|
<DxToolbarItem Alignment="ToolbarItemAlignment.Right">
|
||||||
|
<Template Context="_">
|
||||||
|
<DxDropDownButton Text="@FormatSizeText(_sizeMode)"
|
||||||
|
RenderStyle="ButtonRenderStyle.Secondary"
|
||||||
|
RenderStyleMode="ButtonRenderStyleMode.Text"
|
||||||
|
ItemClick="OnSizeChange">
|
||||||
|
<Items>
|
||||||
|
@foreach (var size in _sizeModes)
|
||||||
|
{
|
||||||
|
<DxDropDownButtonItem Text="@FormatSizeText(size)" Id="@size.ToString()" />
|
||||||
|
}
|
||||||
|
</Items>
|
||||||
|
</DxDropDownButton>
|
||||||
|
</Template>
|
||||||
|
</DxToolbarItem>
|
||||||
|
</DxToolbar>
|
||||||
|
</ToolbarTemplate>
|
||||||
<Columns>
|
<Columns>
|
||||||
@RenderColumns()
|
@RenderColumns()
|
||||||
</Columns>
|
</Columns>
|
||||||
<EditFormTemplate Context="editFormContext">
|
<EditFormTemplate Context="editFormContext">
|
||||||
@{ SetEditContext(editFormContext.EditContext); var editModel = (MassDataEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
|
@{
|
||||||
|
SetEditContext(editFormContext.EditContext); var editModel = (MassDataEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew);
|
||||||
|
}
|
||||||
<DxFormLayout ColCount="2">
|
<DxFormLayout ColCount="2">
|
||||||
<DxFormLayoutItem Caption="CustomerName">
|
<DxFormLayoutItem Caption="CustomerName">
|
||||||
<DxTextBox @bind-Text="editModel.CustomerName" Width="100%" />
|
<DxTextBox @bind-Text="editModel.CustomerName" Width="100%" />
|
||||||
@@ -225,6 +169,7 @@ else
|
|||||||
@code {
|
@code {
|
||||||
private List<MassDataReadDto> items = new();
|
private List<MassDataReadDto> items = new();
|
||||||
private bool isLoading;
|
private bool isLoading;
|
||||||
|
private bool hasLoaded;
|
||||||
private string? errorMessage;
|
private string? errorMessage;
|
||||||
private string? infoMessage;
|
private string? infoMessage;
|
||||||
private int pageIndex;
|
private int pageIndex;
|
||||||
@@ -234,15 +179,17 @@ else
|
|||||||
private EditContext? editContext;
|
private EditContext? editContext;
|
||||||
private ValidationMessageStore? validationMessageStore;
|
private ValidationMessageStore? validationMessageStore;
|
||||||
private IGrid? gridRef;
|
private IGrid? gridRef;
|
||||||
|
private int? focusedRowKey;
|
||||||
private const string LayoutType = "GRID_BANDS";
|
private const string LayoutType = "GRID_BANDS";
|
||||||
private const string LayoutKey = "MassDataGrid";
|
private const string LayoutKey = "MassDataGrid";
|
||||||
private const string LayoutUserStorageKey = "layoutUser";
|
|
||||||
private string? layoutUser;
|
private string? layoutUser;
|
||||||
private BandLayout bandLayout = new();
|
private BandLayout bandLayout = new();
|
||||||
private Dictionary<string, string> columnBandAssignments = new();
|
private Dictionary<string, string> columnBandAssignments = new();
|
||||||
private List<BandOption> bandOptions = new();
|
private List<BandOption> bandOptions = new();
|
||||||
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
||||||
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
|
private bool gridLayoutApplied;
|
||||||
|
private bool bandEditorExpanded;
|
||||||
|
|
||||||
private List<ColumnDefinition> columnDefinitions = new()
|
private List<ColumnDefinition> columnDefinitions = new()
|
||||||
{
|
{
|
||||||
new() { FieldName = nameof(MassDataReadDto.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
new() { FieldName = nameof(MassDataReadDto.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||||
@@ -254,22 +201,13 @@ else
|
|||||||
new() { FieldName = nameof(MassDataReadDto.ChangedWhen), Caption = "Changed", ReadOnly = true, FilterType = ColumnFilterType.Date }
|
new() { FieldName = nameof(MassDataReadDto.ChangedWhen), Caption = "Changed", ReadOnly = true, FilterType = ColumnFilterType.Date }
|
||||||
};
|
};
|
||||||
|
|
||||||
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
|
||||||
|
|
||||||
private readonly List<PageSizeOption> pageSizeOptions = new()
|
private readonly List<PageSizeOption> pageSizeOptions = new()
|
||||||
{
|
{
|
||||||
new() { Value = 100, Text = "100" },
|
new() { Value = 100, Text = "100" },
|
||||||
new() { Value = 1000, Text = "1.000" },
|
new() { Value = 1000, Text = "1.000" },
|
||||||
new() { Value = 10000, Text = "10.000" },
|
new() { Value = 10000, Text = "10.000" },
|
||||||
new() { Value = 100000, Text = "100.000" },
|
new() { Value = 100000, Text = "100.000" },
|
||||||
new() { Value = null, Text = "Alle" }
|
new() { Value = null, Text = "Alle" }
|
||||||
};
|
|
||||||
|
|
||||||
private readonly List<BoolFilterOption> statusFilterOptions = new()
|
|
||||||
{
|
|
||||||
new() { Value = null, Text = "Alle" },
|
|
||||||
new() { Value = true, Text = "True" },
|
|
||||||
new() { Value = false, Text = "False" }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly List<ProcedureOption> procedureOptions = new()
|
private readonly List<ProcedureOption> procedureOptions = new()
|
||||||
@@ -277,14 +215,46 @@ else
|
|||||||
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
|
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||||
|
|
||||||
|
private SizeMode _sizeMode = SizeMode.Medium;
|
||||||
|
private static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
|
||||||
|
|
||||||
|
private string FormatSizeText(SizeMode size) => size switch
|
||||||
|
{
|
||||||
|
SizeMode.Small => "Klein",
|
||||||
|
SizeMode.Medium => "Mittel",
|
||||||
|
SizeMode.Large => "Groß",
|
||||||
|
_ => size.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
private void OnSizeChange(DropDownButtonItemClickEventArgs args)
|
||||||
|
{
|
||||||
|
_sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
await EnsureLayoutUserAsync();
|
layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
|
||||||
await LoadBandLayoutAsync();
|
bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
|
||||||
|
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
|
||||||
|
ApplyColumnLayoutFromStorage();
|
||||||
|
_sizeMode = bandLayout.SizeMode;
|
||||||
|
UpdateBandOptions();
|
||||||
await LoadPage(0);
|
await LoadPage(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
||||||
|
{
|
||||||
|
gridRef.LoadLayout(bandLayout.GridLayout);
|
||||||
|
gridLayoutApplied = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LoadPage(int page)
|
private async Task LoadPage(int page)
|
||||||
{
|
{
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
@@ -295,7 +265,6 @@ else
|
|||||||
var effectivePageSize = pageSize ?? (total == 0 ? 1 : total);
|
var effectivePageSize = pageSize ?? (total == 0 ? 1 : total);
|
||||||
pageCount = Math.Max(1, (int)Math.Ceiling(total / (double)effectivePageSize));
|
pageCount = Math.Max(1, (int)Math.Ceiling(total / (double)effectivePageSize));
|
||||||
pageIndex = Math.Clamp(page, 0, pageCount - 1);
|
pageIndex = Math.Clamp(page, 0, pageCount - 1);
|
||||||
|
|
||||||
var skip = pageSize.HasValue ? pageIndex * pageSize.Value : 0;
|
var skip = pageSize.HasValue ? pageIndex * pageSize.Value : 0;
|
||||||
items = await Api.GetAllAsync(skip, pageSize);
|
items = await Api.GetAllAsync(skip, pageSize);
|
||||||
}
|
}
|
||||||
@@ -306,14 +275,12 @@ else
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
hasLoaded = true;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnPageChanged(int index)
|
private async Task OnPageChanged(int index) => await LoadPage(index);
|
||||||
{
|
|
||||||
await LoadPage(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnPageSizeChanged(int? size)
|
private async Task OnPageSizeChanged(int? size)
|
||||||
{
|
{
|
||||||
@@ -321,61 +288,14 @@ else
|
|||||||
await LoadPage(0);
|
await LoadPage(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureLayoutUserAsync()
|
|
||||||
{
|
|
||||||
layoutUser = await JsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
||||||
{
|
|
||||||
layoutUser = Guid.NewGuid().ToString("N");
|
|
||||||
await JsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadBandLayoutAsync()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
|
||||||
{
|
|
||||||
bandLayout = new BandLayout();
|
|
||||||
UpdateBandOptions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var stored = await LayoutApi.GetAsync(LayoutType, LayoutKey, layoutUser);
|
|
||||||
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
|
|
||||||
{
|
|
||||||
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, jsonOptions);
|
|
||||||
bandLayout = NormalizeBandLayout(parsed);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bandLayout = new BandLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
columnBandAssignments = BuildAssignmentsFromLayout(bandLayout);
|
|
||||||
ApplyColumnLayoutFromStorage();
|
|
||||||
//ApplyBandOrderingFromColumnOrder();
|
|
||||||
UpdateBandOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SaveLayoutAsync()
|
private async Task SaveLayoutAsync()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CaptureColumnLayoutFromGrid();
|
CaptureColumnLayoutFromGrid();
|
||||||
|
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
|
||||||
var layoutData = JsonSerializer.Serialize(bandLayout, jsonOptions);
|
|
||||||
await LayoutApi.UpsertAsync(new LayoutDto
|
|
||||||
{
|
|
||||||
LayoutType = LayoutType,
|
|
||||||
LayoutKey = LayoutKey,
|
|
||||||
UserName = layoutUser,
|
|
||||||
LayoutData = layoutData
|
|
||||||
});
|
|
||||||
infoMessage = "Layout gespeichert.";
|
infoMessage = "Layout gespeichert.";
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
}
|
}
|
||||||
@@ -385,85 +305,39 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CaptureColumnLayoutFromGrid()
|
private async Task ResetLayoutAsync()
|
||||||
{
|
|
||||||
if (gridRef == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var layout = gridRef.SaveLayout();
|
|
||||||
bandLayout.GridLayout = layout;
|
|
||||||
|
|
||||||
var orderedColumns = layout.Columns
|
|
||||||
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
|
|
||||||
.OrderBy(column => column.VisibleIndex)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
bandLayout.ColumnOrder = orderedColumns
|
|
||||||
.Select(column => column.FieldName)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
bandLayout.ColumnWidths = orderedColumns
|
|
||||||
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
|
|
||||||
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ResetBandLayoutAsync()
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
|
||||||
|
|
||||||
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
|
|
||||||
bandLayout = new BandLayout();
|
bandLayout = new BandLayout();
|
||||||
columnBandAssignments.Clear();
|
columnBandAssignments.Clear();
|
||||||
UpdateBandOptions();
|
UpdateBandOptions();
|
||||||
infoMessage = "Band-Layout zurückgesetzt.";
|
foreach (var column in columnDefinitions)
|
||||||
|
column.Width = null;
|
||||||
|
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
_sizeMode = SizeMode.Medium;
|
||||||
|
if (gridRef != null)
|
||||||
|
gridRef.LoadLayout(new GridPersistentLayout());
|
||||||
|
gridLayoutApplied = false;
|
||||||
|
infoMessage = "Layout zurückgesetzt.";
|
||||||
|
errorMessage = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyBandOrderingFromColumnOrder()
|
private void CaptureColumnLayoutFromGrid()
|
||||||
{
|
{
|
||||||
if (bandLayout.ColumnOrder.Count == 0)
|
if (gridRef == null) return;
|
||||||
{
|
var layout = gridRef.SaveLayout();
|
||||||
return;
|
bandLayout.GridLayout = layout;
|
||||||
}
|
bandLayout.SizeMode = _sizeMode;
|
||||||
|
var orderedColumns = layout.Columns
|
||||||
var bandById = bandLayout.Bands.ToDictionary(band => band.Id, StringComparer.OrdinalIgnoreCase);
|
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
|
||||||
var orderedBandIds = new List<string>();
|
.OrderBy(c => c.VisibleIndex)
|
||||||
var orderedColumnsByBand = bandLayout.Bands.ToDictionary(
|
.ToList();
|
||||||
band => band.Id,
|
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
|
||||||
_ => new List<string>(),
|
bandLayout.ColumnWidths = orderedColumns
|
||||||
StringComparer.OrdinalIgnoreCase);
|
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
|
||||||
|
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var field in bandLayout.ColumnOrder)
|
|
||||||
{
|
|
||||||
if (columnBandAssignments.TryGetValue(field, out var bandId) && bandById.ContainsKey(bandId))
|
|
||||||
{
|
|
||||||
if (!orderedBandIds.Contains(bandId, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
orderedBandIds.Add(bandId);
|
|
||||||
}
|
|
||||||
|
|
||||||
orderedColumnsByBand[bandId].Add(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var band in bandLayout.Bands)
|
|
||||||
{
|
|
||||||
var orderedColumns = orderedColumnsByBand[band.Id];
|
|
||||||
orderedColumns.AddRange(band.Columns.Where(column => !orderedColumns.Contains(column, StringComparer.OrdinalIgnoreCase)));
|
|
||||||
band.Columns = orderedColumns;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orderedBandIds.Count > 0)
|
|
||||||
{
|
|
||||||
bandLayout.Bands = orderedBandIds
|
|
||||||
.Select(id => bandById[id])
|
|
||||||
.Concat(bandLayout.Bands.Where(band => !orderedBandIds.Contains(band.Id, StringComparer.OrdinalIgnoreCase)))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyColumnLayoutFromStorage()
|
private void ApplyColumnLayoutFromStorage()
|
||||||
@@ -471,34 +345,22 @@ else
|
|||||||
foreach (var column in columnDefinitions)
|
foreach (var column in columnDefinitions)
|
||||||
{
|
{
|
||||||
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||||||
{
|
|
||||||
column.Width = width;
|
column.Width = width;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddBand()
|
private void AddBand()
|
||||||
{
|
{
|
||||||
bandLayout.Bands.Add(new BandDefinition
|
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
|
||||||
{
|
|
||||||
Id = Guid.NewGuid().ToString("N"),
|
|
||||||
Caption = "Band"
|
|
||||||
});
|
|
||||||
UpdateBandOptions();
|
UpdateBandOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveBand(BandDefinition band)
|
private void RemoveBand(BandDefinition band)
|
||||||
{
|
{
|
||||||
bandLayout.Bands.Remove(band);
|
bandLayout.Bands.Remove(band);
|
||||||
var removedColumns = columnBandAssignments.Where(pair => pair.Value == band.Id)
|
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
|
||||||
.Select(pair => pair.Key)
|
columnBandAssignments.Remove(key);
|
||||||
.ToList();
|
|
||||||
foreach (var column in removedColumns)
|
|
||||||
{
|
|
||||||
columnBandAssignments.Remove(column);
|
|
||||||
}
|
|
||||||
UpdateBandOptions();
|
UpdateBandOptions();
|
||||||
SyncBandsFromAssignments();
|
SyncBandsFromAssignments();
|
||||||
}
|
}
|
||||||
@@ -512,77 +374,31 @@ else
|
|||||||
private void UpdateColumnBand(string fieldName, string? bandId)
|
private void UpdateColumnBand(string fieldName, string? bandId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(bandId))
|
if (string.IsNullOrWhiteSpace(bandId))
|
||||||
{
|
|
||||||
columnBandAssignments.Remove(fieldName);
|
columnBandAssignments.Remove(fieldName);
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
columnBandAssignments[fieldName] = bandId;
|
columnBandAssignments[fieldName] = bandId;
|
||||||
}
|
|
||||||
|
|
||||||
SyncBandsFromAssignments();
|
SyncBandsFromAssignments();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetColumnBand(string fieldName)
|
private string GetColumnBand(string fieldName)
|
||||||
{
|
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
||||||
return columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SyncBandsFromAssignments()
|
private void SyncBandsFromAssignments()
|
||||||
{
|
{
|
||||||
foreach (var band in bandLayout.Bands)
|
foreach (var band in bandLayout.Bands)
|
||||||
{
|
{
|
||||||
band.Columns = columnDefinitions
|
band.Columns = columnDefinitions
|
||||||
.Where(column => columnBandAssignments.TryGetValue(column.FieldName, out var bandId) && bandId == band.Id)
|
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
|
||||||
.Select(column => column.FieldName)
|
.Select(c => c.FieldName)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateBandOptions()
|
private void UpdateBandOptions()
|
||||||
{
|
{
|
||||||
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
||||||
bandOptions.AddRange(bandLayout.Bands.Select(band => new BandOption { Id = band.Id, Caption = band.Caption }));
|
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
|
|
||||||
{
|
|
||||||
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var band in layout.Bands)
|
|
||||||
{
|
|
||||||
foreach (var column in band.Columns)
|
|
||||||
{
|
|
||||||
assignments[column] = band.Id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return assignments;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BandLayout NormalizeBandLayout(BandLayout? layout)
|
|
||||||
{
|
|
||||||
layout ??= new BandLayout();
|
|
||||||
layout.Bands ??= new List<BandDefinition>();
|
|
||||||
layout.ColumnOrder ??= new List<string>();
|
|
||||||
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var band in layout.Bands)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(band.Id))
|
|
||||||
{
|
|
||||||
band.Id = Guid.NewGuid().ToString("N");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(band.Caption))
|
|
||||||
{
|
|
||||||
band.Caption = "Band";
|
|
||||||
}
|
|
||||||
|
|
||||||
band.Columns = band.Columns?.Where(columnLookup.ContainsKey).ToList() ?? new List<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return layout;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private RenderFragment RenderColumns() => builder =>
|
private RenderFragment RenderColumns() => builder =>
|
||||||
@@ -591,20 +407,12 @@ else
|
|||||||
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
||||||
builder.AddAttribute(seq++, "Width", "120px");
|
builder.AddAttribute(seq++, "Width", "120px");
|
||||||
builder.CloseComponent();
|
builder.CloseComponent();
|
||||||
|
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
var grouped = bandLayout.Bands.SelectMany(band => band.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
|
||||||
foreach (var column in columnDefinitions.Where(column => !grouped.Contains(column.FieldName)))
|
|
||||||
{
|
|
||||||
BuildDataColumn(builder, ref seq, column);
|
BuildDataColumn(builder, ref seq, column);
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var band in bandLayout.Bands)
|
foreach (var band in bandLayout.Bands)
|
||||||
{
|
{
|
||||||
if (band.Columns.Count == 0)
|
if (band.Columns.Count == 0) continue;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.OpenComponent<DxGridBandColumn>(seq++);
|
builder.OpenComponent<DxGridBandColumn>(seq++);
|
||||||
builder.AddAttribute(seq++, "Caption", band.Caption);
|
builder.AddAttribute(seq++, "Caption", band.Caption);
|
||||||
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
||||||
@@ -613,9 +421,7 @@ else
|
|||||||
foreach (var columnName in band.Columns)
|
foreach (var columnName in band.Columns)
|
||||||
{
|
{
|
||||||
if (columnLookup.TryGetValue(columnName, out var column))
|
if (columnLookup.TryGetValue(columnName, out var column))
|
||||||
{
|
|
||||||
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
builder.CloseComponent();
|
builder.CloseComponent();
|
||||||
@@ -628,35 +434,19 @@ else
|
|||||||
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
||||||
builder.AddAttribute(seq++, "Caption", column.Caption);
|
builder.AddAttribute(seq++, "Caption", column.Caption);
|
||||||
if (!string.IsNullOrWhiteSpace(column.Width))
|
if (!string.IsNullOrWhiteSpace(column.Width))
|
||||||
{
|
|
||||||
builder.AddAttribute(seq++, "Width", column.Width);
|
builder.AddAttribute(seq++, "Width", column.Width);
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
||||||
{
|
|
||||||
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
||||||
}
|
|
||||||
|
|
||||||
if (column.ReadOnly)
|
if (column.ReadOnly)
|
||||||
{
|
|
||||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||||
}
|
|
||||||
|
|
||||||
builder.CloseComponent();
|
builder.CloseComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetEditContext(EditContext context)
|
private void SetEditContext(EditContext context)
|
||||||
{
|
{
|
||||||
if (editContext == context)
|
if (editContext == context) return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editContext != null)
|
if (editContext != null)
|
||||||
{
|
|
||||||
editContext.OnFieldChanged -= OnEditFieldChanged;
|
editContext.OnFieldChanged -= OnEditFieldChanged;
|
||||||
}
|
|
||||||
|
|
||||||
editContext = context;
|
editContext = context;
|
||||||
validationMessageStore = new ValidationMessageStore(editContext);
|
validationMessageStore = new ValidationMessageStore(editContext);
|
||||||
editContext.OnFieldChanged += OnEditFieldChanged;
|
editContext.OnFieldChanged += OnEditFieldChanged;
|
||||||
@@ -664,30 +454,21 @@ else
|
|||||||
|
|
||||||
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (validationMessageStore == null || editContext == null)
|
if (validationMessageStore == null || editContext == null) return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure))
|
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure))
|
||||||
{
|
{
|
||||||
validationMessageStore.Clear();
|
validationMessageStore.Clear();
|
||||||
editContext.NotifyValidationStateChanged();
|
editContext.NotifyValidationStateChanged();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.CustomerName))
|
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.CustomerName))
|
||||||
{
|
{
|
||||||
var field = new FieldIdentifier(editContext.Model, nameof(MassDataEditModel.CustomerName));
|
validationMessageStore.Clear(new FieldIdentifier(editContext.Model, nameof(MassDataEditModel.CustomerName)));
|
||||||
validationMessageStore.Clear(field);
|
|
||||||
editContext.NotifyValidationStateChanged();
|
editContext.NotifyValidationStateChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetPopupHeaderText(bool isNew)
|
private void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
|
||||||
{
|
|
||||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
private async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -697,7 +478,6 @@ else
|
|||||||
SetPopupHeaderText(true);
|
SetPopupHeaderText(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var item = (MassDataReadDto)e.DataItem;
|
var item = (MassDataReadDto)e.DataItem;
|
||||||
e.EditModel = new MassDataEditModel
|
e.EditModel = new MassDataEditModel
|
||||||
{
|
{
|
||||||
@@ -717,10 +497,8 @@ else
|
|||||||
{
|
{
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
infoMessage = null;
|
infoMessage = null;
|
||||||
|
|
||||||
validationMessageStore?.Clear();
|
validationMessageStore?.Clear();
|
||||||
editContext?.NotifyValidationStateChanged();
|
editContext?.NotifyValidationStateChanged();
|
||||||
|
|
||||||
var editModel = (MassDataEditModel)e.EditModel;
|
var editModel = (MassDataEditModel)e.EditModel;
|
||||||
if (!decimal.TryParse(editModel.AmountText, out var amount))
|
if (!decimal.TryParse(editModel.AmountText, out var amount))
|
||||||
{
|
{
|
||||||
@@ -728,7 +506,6 @@ else
|
|||||||
e.Cancel = true;
|
e.Cancel = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editModel.IsNew)
|
if (editModel.IsNew)
|
||||||
{
|
{
|
||||||
var existing = await Api.GetByCustomerNameAsync(editModel.CustomerName);
|
var existing = await Api.GetByCustomerNameAsync(editModel.CustomerName);
|
||||||
@@ -739,7 +516,6 @@ else
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var dto = new MassDataWriteDto
|
var dto = new MassDataWriteDto
|
||||||
{
|
{
|
||||||
CustomerName = editModel.CustomerName,
|
CustomerName = editModel.CustomerName,
|
||||||
@@ -747,11 +523,11 @@ else
|
|||||||
Category = editModel.Category,
|
Category = editModel.Category,
|
||||||
StatusFlag = editModel.StatusFlag
|
StatusFlag = editModel.StatusFlag
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Api.UpsertAsync(dto);
|
var saved = await Api.UpsertAsync(dto);
|
||||||
infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert.";
|
infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert.";
|
||||||
|
focusedRowKey = saved.Id;
|
||||||
await LoadPage(pageIndex);
|
await LoadPage(pageIndex);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -763,13 +539,8 @@ else
|
|||||||
|
|
||||||
private void AddValidationError(MassDataEditModel editModel, string fieldName, string message)
|
private void AddValidationError(MassDataEditModel editModel, string fieldName, string message)
|
||||||
{
|
{
|
||||||
if (editContext == null || validationMessageStore == null)
|
if (editContext == null || validationMessageStore == null) return;
|
||||||
{
|
validationMessageStore.Add(new FieldIdentifier(editModel, fieldName), message);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var field = new FieldIdentifier(editModel, fieldName);
|
|
||||||
validationMessageStore.Add(field, message);
|
|
||||||
editContext.NotifyValidationStateChanged();
|
editContext.NotifyValidationStateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,44 +552,6 @@ else
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class BandLayout
|
|
||||||
{
|
|
||||||
public List<BandDefinition> Bands { get; set; } = new();
|
|
||||||
public List<string> ColumnOrder { get; set; } = new();
|
|
||||||
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
public GridPersistentLayout? GridLayout { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class BandDefinition
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
public string Caption { get; set; } = string.Empty;
|
|
||||||
public List<string> Columns { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class BandOption
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
public string Caption { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ColumnDefinition
|
|
||||||
{
|
|
||||||
public string FieldName { get; init; } = string.Empty;
|
|
||||||
public string Caption { get; init; } = string.Empty;
|
|
||||||
public string? Width { get; set; }
|
|
||||||
public string? DisplayFormat { get; init; }
|
|
||||||
public bool ReadOnly { get; init; }
|
|
||||||
public ColumnFilterType FilterType { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum ColumnFilterType
|
|
||||||
{
|
|
||||||
Text,
|
|
||||||
Bool,
|
|
||||||
Date
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class MassDataEditModel
|
private sealed class MassDataEditModel
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
@@ -837,27 +570,9 @@ else
|
|||||||
public string Text { get; set; } = string.Empty;
|
public string Text { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class BoolFilterOption
|
|
||||||
{
|
|
||||||
public bool? Value { get; set; }
|
|
||||||
public string Text { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class PageSizeOption
|
private sealed class PageSizeOption
|
||||||
{
|
{
|
||||||
public int? Value { get; set; }
|
public int? Value { get; set; }
|
||||||
public string Text { get; set; } = string.Empty;
|
public string Text { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private bool gridLayoutApplied;
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
|
||||||
{
|
|
||||||
gridRef.LoadLayout(bandLayout.GridLayout);
|
|
||||||
gridLayoutApplied = true;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
DbFirst.BlazorWebApp/Components/MassDataGrid.razor.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.massdata-edit-popup {
|
||||||
|
min-width: 720px;
|
||||||
|
}
|
||||||
100
DbFirst.BlazorWebApp/Components/Pages/Clock.razor
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
@rendermode InteractiveServer
|
||||||
|
@page "/clock"
|
||||||
|
@inject TimeApiClient TimeApi
|
||||||
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
|
<PageTitle>Clock</PageTitle>
|
||||||
|
|
||||||
|
<h3>DB Server Clock</h3>
|
||||||
|
|
||||||
|
<div class="clock-wrapper">
|
||||||
|
<div class="clock-display @(_error != null ? "clock-error" : "")">
|
||||||
|
@if (_dbTime.HasValue)
|
||||||
|
{
|
||||||
|
<span class="clock-time">@_dbTime.Value.ToString("HH:mm:ss")</span>
|
||||||
|
<span class="clock-date">@_dbTime.Value.ToString("dd.MM.yyyy")</span>
|
||||||
|
}
|
||||||
|
else if (_error != null)
|
||||||
|
{
|
||||||
|
<span class="clock-time">--:--:--</span>
|
||||||
|
<span class="clock-date text-danger">@_error</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="clock-time">...</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.clock-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 40vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-display {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bs-body-bg, #1e1e2e);
|
||||||
|
border: 2px solid var(--bs-border-color, #444);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2rem 4rem;
|
||||||
|
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-time {
|
||||||
|
font-size: 5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--bs-primary, #0d6efd);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-date {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-error .clock-time {
|
||||||
|
color: var(--bs-danger, #dc3545);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private DateTime? _dbTime;
|
||||||
|
private string? _error;
|
||||||
|
private Timer? _timer;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await TickAsync();
|
||||||
|
_timer = new Timer(async _ =>
|
||||||
|
{
|
||||||
|
await TickAsync();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TickAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_dbTime = await TimeApi.InsertAndGetLastAsync();
|
||||||
|
_error = null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_error = ex.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_timer != null)
|
||||||
|
await _timer.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
@page "/counter"
|
|
||||||
@rendermode InteractiveServer
|
|
||||||
|
|
||||||
<PageTitle>Counter</PageTitle>
|
|
||||||
|
|
||||||
<h1>Counter</h1>
|
|
||||||
|
|
||||||
<p role="status">Current count: @currentCount</p>
|
|
||||||
|
|
||||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private int currentCount = 0;
|
|
||||||
|
|
||||||
private void IncrementCount()
|
|
||||||
{
|
|
||||||
currentCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,46 +5,6 @@
|
|||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject DashboardApiClient DashboardApi
|
@inject DashboardApiClient DashboardApi
|
||||||
|
|
||||||
<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>
|
<PageTitle>Dashboards</PageTitle>
|
||||||
|
|
||||||
<div class="dashboard-shell">
|
<div class="dashboard-shell">
|
||||||
|
|||||||
42
DbFirst.BlazorWebApp/Components/Pages/Dashboard.razor.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
@@ -1,7 +1,43 @@
|
|||||||
@page "/"
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
@page "/"
|
||||||
|
|
||||||
<PageTitle>Home</PageTitle>
|
<PageTitle>Home</PageTitle>
|
||||||
|
|
||||||
<h1>Hello, world!</h1>
|
<h1>Database first</h1>
|
||||||
|
|
||||||
Welcome to your new app.
|
<DxCarousel Width="100%"
|
||||||
|
Height="calc(100vh - 9rem)"
|
||||||
|
Data="@GetCarouselData()"
|
||||||
|
ImageSrcField="Source"
|
||||||
|
ImageAltField="AlternateText"
|
||||||
|
LoopNavigationEnabled="true"
|
||||||
|
SlideShowEnabled="true"
|
||||||
|
SlideShowDelay="3000"
|
||||||
|
PauseSlideShowOnHover="true"
|
||||||
|
ImageSizeMode="CarouselImageSizeMode.FitProportional">
|
||||||
|
</DxCarousel>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
List<CarouselData> GetCarouselData()
|
||||||
|
{
|
||||||
|
return new List<CarouselData>
|
||||||
|
{
|
||||||
|
new CarouselData("/images/DbFirstBefehl.png", "DbFirstBefehl"),
|
||||||
|
new CarouselData("/images/CQRS - Katalog-Datenfluss.png", "CQRS - Katalog-Datenfluss"),
|
||||||
|
new CarouselData("/images/CQRS - Catalog Create, Update, Delete.png", "CQRS - Catalog Create, Update, Delete"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CarouselData
|
||||||
|
{
|
||||||
|
public string Source { get; set; }
|
||||||
|
public string AlternateText { get; set; }
|
||||||
|
|
||||||
|
public CarouselData(string source, string alt)
|
||||||
|
{
|
||||||
|
Source = source;
|
||||||
|
AlternateText = alt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
@page "/weather"
|
|
||||||
@attribute [StreamRendering]
|
|
||||||
|
|
||||||
<PageTitle>Weather</PageTitle>
|
|
||||||
|
|
||||||
<h1>Weather</h1>
|
|
||||||
|
|
||||||
<p>This component demonstrates showing data.</p>
|
|
||||||
|
|
||||||
@if (forecasts == null)
|
|
||||||
{
|
|
||||||
<p><em>Loading...</em></p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Temp. (C)</th>
|
|
||||||
<th>Temp. (F)</th>
|
|
||||||
<th>Summary</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach (var forecast in forecasts)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td>@forecast.Date.ToShortDateString()</td>
|
|
||||||
<td>@forecast.TemperatureC</td>
|
|
||||||
<td>@forecast.TemperatureF</td>
|
|
||||||
<td>@forecast.Summary</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private WeatherForecast[]? forecasts;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
// Simulate asynchronous loading to demonstrate streaming rendering
|
|
||||||
await Task.Delay(500);
|
|
||||||
|
|
||||||
var startDate = DateOnly.FromDateTime(DateTime.Now);
|
|
||||||
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
|
|
||||||
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
|
||||||
{
|
|
||||||
Date = startDate.AddDays(index),
|
|
||||||
TemperatureC = Random.Shared.Next(-20, 55),
|
|
||||||
Summary = summaries[Random.Shared.Next(summaries.Length)]
|
|
||||||
}).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class WeatherForecast
|
|
||||||
{
|
|
||||||
public DateOnly Date { get; set; }
|
|
||||||
public int TemperatureC { get; set; }
|
|
||||||
public string? Summary { get; set; }
|
|
||||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
@using System.Net.Http
|
@using System.Net.Http
|
||||||
@using System.Net.Http.Json
|
@using System.Net.Http.Json
|
||||||
|
@using System.Text.Json
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Rendering
|
||||||
@using Microsoft.AspNetCore.Components.Routing
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
@using Microsoft.AspNetCore.Components.Web
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
@@ -10,8 +12,9 @@
|
|||||||
@using DbFirst.BlazorWebApp
|
@using DbFirst.BlazorWebApp
|
||||||
@using DbFirst.BlazorWebApp.Components
|
@using DbFirst.BlazorWebApp.Components
|
||||||
@using DbFirst.BlazorWebApp.Models
|
@using DbFirst.BlazorWebApp.Models
|
||||||
|
@using DbFirst.BlazorWebApp.Models.Grid
|
||||||
@using DbFirst.BlazorWebApp.Services
|
@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
|
@using DevExpress.Data.Filtering
|
||||||
@@ -11,10 +11,11 @@
|
|||||||
<PackageReference Include="DevExpress.Blazor.Dashboard" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.Dashboard" Version="25.2.3" />
|
||||||
<PackageReference Include="DevExpress.Blazor.Themes" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.Themes" Version="25.2.3" />
|
||||||
<PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" />
|
<PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" />
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="DevExpress.Blazor" Version="25.2.3" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.22" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.22" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="wwwroot\images\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
43
DbFirst.BlazorWebApp/Models/Grid/BandLayoutModels.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using DevExpress.Blazor;
|
||||||
|
|
||||||
|
namespace DbFirst.BlazorWebApp.Models.Grid
|
||||||
|
{
|
||||||
|
public class BandLayout
|
||||||
|
{
|
||||||
|
public List<BandDefinition> Bands { get; set; } = new();
|
||||||
|
public List<string> ColumnOrder { get; set; } = new();
|
||||||
|
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public GridPersistentLayout? GridLayout { get; set; }
|
||||||
|
public SizeMode SizeMode { get; set; } = SizeMode.Medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BandDefinition
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Caption { get; set; } = string.Empty;
|
||||||
|
public List<string> Columns { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BandOption
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Caption { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ColumnDefinition
|
||||||
|
{
|
||||||
|
public string FieldName { get; init; } = string.Empty;
|
||||||
|
public string Caption { get; init; } = string.Empty;
|
||||||
|
public string? Width { get; set; }
|
||||||
|
public string? DisplayFormat { get; init; }
|
||||||
|
public bool ReadOnly { get; init; }
|
||||||
|
public ColumnFilterType FilterType { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ColumnFilterType
|
||||||
|
{
|
||||||
|
Text,
|
||||||
|
Bool,
|
||||||
|
Date
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,9 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents();
|
.AddInteractiveServerComponents();
|
||||||
|
|
||||||
builder.Services.AddDevExpressBlazor();
|
builder.Services.AddDevExpressBlazor(options => options.BootstrapVersion = BootstrapVersion.v5);
|
||||||
|
builder.Services.AddScoped<ThemeState>();
|
||||||
|
builder.Services.AddScoped<BandLayoutService>();
|
||||||
|
|
||||||
var apiBaseUrl = builder.Configuration["ApiBaseUrl"];
|
var apiBaseUrl = builder.Configuration["ApiBaseUrl"];
|
||||||
if (!string.IsNullOrWhiteSpace(apiBaseUrl))
|
if (!string.IsNullOrWhiteSpace(apiBaseUrl))
|
||||||
@@ -29,6 +31,10 @@ if (!string.IsNullOrWhiteSpace(apiBaseUrl))
|
|||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
});
|
||||||
|
builder.Services.AddHttpClient<TimeApiClient>(client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -36,6 +42,7 @@ else
|
|||||||
builder.Services.AddHttpClient<DashboardApiClient>();
|
builder.Services.AddHttpClient<DashboardApiClient>();
|
||||||
builder.Services.AddHttpClient<MassDataApiClient>();
|
builder.Services.AddHttpClient<MassDataApiClient>();
|
||||||
builder.Services.AddHttpClient<LayoutApiClient>();
|
builder.Services.AddHttpClient<LayoutApiClient>();
|
||||||
|
builder.Services.AddHttpClient<TimeApiClient>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|||||||
103
DbFirst.BlazorWebApp/Services/BandLayoutService.cs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
using DbFirst.BlazorWebApp.Models;
|
||||||
|
using DbFirst.BlazorWebApp.Models.Grid;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace DbFirst.BlazorWebApp.Services
|
||||||
|
{
|
||||||
|
public class BandLayoutService(LayoutApiClient layoutApi, IJSRuntime jsRuntime)
|
||||||
|
{
|
||||||
|
private const string LayoutUserStorageKey = "layoutUser";
|
||||||
|
private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
public async Task<string> EnsureLayoutUserAsync()
|
||||||
|
{
|
||||||
|
var layoutUser = await jsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
|
||||||
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||||
|
{
|
||||||
|
layoutUser = Guid.NewGuid().ToString("N");
|
||||||
|
await jsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
|
||||||
|
}
|
||||||
|
return layoutUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BandLayout> LoadBandLayoutAsync(
|
||||||
|
string layoutType,
|
||||||
|
string layoutKey,
|
||||||
|
string layoutUser,
|
||||||
|
Dictionary<string, ColumnDefinition> columnLookup)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||||
|
return new BandLayout();
|
||||||
|
|
||||||
|
var stored = await layoutApi.GetAsync(layoutType, layoutKey, layoutUser);
|
||||||
|
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
|
||||||
|
{
|
||||||
|
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, _jsonOptions);
|
||||||
|
return NormalizeBandLayout(parsed, columnLookup);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BandLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveBandLayoutAsync(
|
||||||
|
string layoutType,
|
||||||
|
string layoutKey,
|
||||||
|
string layoutUser,
|
||||||
|
BandLayout bandLayout)
|
||||||
|
{
|
||||||
|
var layoutData = JsonSerializer.Serialize(bandLayout, _jsonOptions);
|
||||||
|
await layoutApi.UpsertAsync(new LayoutDto
|
||||||
|
{
|
||||||
|
LayoutType = layoutType,
|
||||||
|
LayoutKey = layoutKey,
|
||||||
|
UserName = layoutUser,
|
||||||
|
LayoutData = layoutData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ResetBandLayoutAsync(
|
||||||
|
string layoutType,
|
||||||
|
string layoutKey,
|
||||||
|
string layoutUser)
|
||||||
|
{
|
||||||
|
await layoutApi.DeleteAsync(layoutType, layoutKey, layoutUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
|
||||||
|
{
|
||||||
|
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var band in layout.Bands)
|
||||||
|
{
|
||||||
|
foreach (var column in band.Columns)
|
||||||
|
{
|
||||||
|
assignments[column] = band.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return assignments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BandLayout NormalizeBandLayout(
|
||||||
|
BandLayout? layout,
|
||||||
|
Dictionary<string, ColumnDefinition> columnLookup)
|
||||||
|
{
|
||||||
|
layout ??= new BandLayout();
|
||||||
|
layout.Bands ??= new List<BandDefinition>();
|
||||||
|
layout.ColumnOrder ??= new List<string>();
|
||||||
|
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var band in layout.Bands)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(band.Id))
|
||||||
|
band.Id = Guid.NewGuid().ToString("N");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(band.Caption))
|
||||||
|
band.Caption = "Band";
|
||||||
|
|
||||||
|
band.Columns = band.Columns?.Where(columnLookup.ContainsKey).ToList() ?? new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
DbFirst.BlazorWebApp/Services/ThemeState.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using DevExpress.Blazor;
|
||||||
|
|
||||||
|
namespace DbFirst.BlazorWebApp.Services;
|
||||||
|
|
||||||
|
public class ThemeState
|
||||||
|
{
|
||||||
|
private readonly IThemeChangeService themeChangeService;
|
||||||
|
|
||||||
|
public ThemeState(IThemeChangeService themeChangeService)
|
||||||
|
{
|
||||||
|
this.themeChangeService = themeChangeService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsDarkMode { get; private set; }
|
||||||
|
|
||||||
|
public event Action? OnChange;
|
||||||
|
|
||||||
|
public void SetDarkMode(bool isDarkMode)
|
||||||
|
{
|
||||||
|
if (IsDarkMode == isDarkMode)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsDarkMode = isDarkMode;
|
||||||
|
var theme = Themes.Fluent.Clone(properties =>
|
||||||
|
{
|
||||||
|
properties.Mode = isDarkMode ? ThemeMode.Dark : ThemeMode.Light;
|
||||||
|
properties.ApplyToPageElements = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
themeChangeService.SetTheme(theme);
|
||||||
|
OnChange?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
27
DbFirst.BlazorWebApp/Services/TimeApiClient.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
|
namespace DbFirst.BlazorWebApp.Services;
|
||||||
|
|
||||||
|
public class TimeApiClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private const string Endpoint = "api/time";
|
||||||
|
|
||||||
|
public TimeApiClient(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DateTime?> InsertAndGetLastAsync()
|
||||||
|
{
|
||||||
|
var response = await _httpClient.PostAsync(Endpoint, null);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<TimeResponse>();
|
||||||
|
return result?.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TimeResponse
|
||||||
|
{
|
||||||
|
public DateTime? Now { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,37 @@
|
|||||||
|
:root {
|
||||||
|
--global-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
font-size: var(--global-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme-Variablen */
|
||||||
|
.app-light {
|
||||||
|
--band-editor-bg: #f8f9fa;
|
||||||
|
--band-editor-border: #dee2e6;
|
||||||
|
--band-toggle-hover-bg: #e9ecef;
|
||||||
|
--grid-stripe-bg: rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-dark {
|
||||||
|
background-color: #1b1b1b;
|
||||||
|
color: #f1f1f1;
|
||||||
|
--band-editor-bg: #2d2d2d;
|
||||||
|
--band-editor-border: #444444;
|
||||||
|
--band-toggle-hover-bg: #3a3a3a;
|
||||||
|
--grid-stripe-bg: rgba(255, 255, 255, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
a, .btn-link {
|
a, .btn-link {
|
||||||
color: #006bb7;
|
color: #006bb7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-dark a, .app-dark .btn-link {
|
||||||
|
color: #6cb6ff;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #1b6ec2;
|
background-color: #1b6ec2;
|
||||||
@@ -13,7 +39,7 @@ a, .btn-link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -37,7 +63,7 @@ h1:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.blazor-error-boundary {
|
.blazor-error-boundary {
|
||||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDg2IDY2LjAxODMgMjYzLjU4NiA2Ni4wMTgzWk0yNjMuNTc2IDg2LjA1NDdDMjYxLjA0OSA4Ni4wNTQ3IDI1OS43ODUgODcuMzAwNSAyNTkuNzg2IDg5Ljc5MjEgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||||
padding: 1rem 1rem 1rem 3.7rem;
|
padding: 1rem 1rem 1rem 3.7rem;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
@@ -49,3 +75,113 @@ h1:focus {
|
|||||||
.darker-border-checkbox.form-check-input {
|
.darker-border-checkbox.form-check-input {
|
||||||
border-color: #929292;
|
border-color: #929292;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Grid Band-Editor */
|
||||||
|
.band-editor {
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid var(--band-editor-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--band-editor-bg);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.band-editor-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.band-editor-toggle:hover {
|
||||||
|
background-color: var(--band-toggle-hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.band-editor-toggle-icon {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.band-editor-toggle-icon.expanded {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.band-editor-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 12px 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.band-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.band-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.band-columns {
|
||||||
|
max-width: 720px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-section {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid Zebra-Striping */
|
||||||
|
dxbl-grid tbody tr:nth-child(even) td {
|
||||||
|
background-color: var(--grid-stripe-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MassData-spezifisch */
|
||||||
|
.page-size-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-size-label {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-size-combo {
|
||||||
|
width: 13ch;
|
||||||
|
min-width: 13ch;
|
||||||
|
max-width: 13ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-size-combo input {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lade-Spinner */
|
||||||
|
.loading-container {
|
||||||
|
min-height: 160px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|||||||
BIN
DbFirst.BlazorWebApp/wwwroot/images/Blazor Server Lifecycle.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
BIN
DbFirst.BlazorWebApp/wwwroot/images/Datenfluss Catalogs.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
DbFirst.BlazorWebApp/wwwroot/images/Datenfluss Massdata.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
DbFirst.BlazorWebApp/wwwroot/images/DbFirstBefehl.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
DbFirst.BlazorWebApp/wwwroot/images/Fehlerbehandlung.png
Normal file
|
After Width: | Height: | Size: 742 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
BIN
DbFirst.BlazorWebApp/wwwroot/images/Übersicht Architektur.png
Normal file
|
After Width: | Height: | Size: 201 KiB |
3
DbFirst.BlazorWebApp/wwwroot/js/size-manager.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
window.setSize = function (fontSize) {
|
||||||
|
document.documentElement.style.setProperty('--global-size', fontSize);
|
||||||
|
};
|
||||||
6
DbFirst.Domain/Entities/Time.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace DbFirst.Domain.Entities;
|
||||||
|
|
||||||
|
public class TimeRecord
|
||||||
|
{
|
||||||
|
public DateTime? Now { get; set; }
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ public partial class ApplicationDbContext : DbContext
|
|||||||
|
|
||||||
public virtual DbSet<VwmyCatalog> VwmyCatalogs { get; set; }
|
public virtual DbSet<VwmyCatalog> VwmyCatalogs { get; set; }
|
||||||
public virtual DbSet<SmfLayout> SmfLayouts { get; set; }
|
public virtual DbSet<SmfLayout> SmfLayouts { get; set; }
|
||||||
|
public virtual DbSet<TimeRecord> Times { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -83,6 +84,16 @@ public partial class ApplicationDbContext : DbContext
|
|||||||
.HasColumnName("CHANGED_WHEN");
|
.HasColumnName("CHANGED_WHEN");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<TimeRecord>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasNoKey();
|
||||||
|
entity.ToTable("TIME");
|
||||||
|
|
||||||
|
entity.Property(e => e.Now)
|
||||||
|
.HasColumnType("datetime")
|
||||||
|
.HasColumnName("NOW");
|
||||||
|
});
|
||||||
|
|
||||||
OnModelCreatingPartial(modelBuilder);
|
OnModelCreatingPartial(modelBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
DbFirst.Infrastructure/Repositories/TimeRepository.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using DbFirst.Application.Repositories;
|
||||||
|
using DbFirst.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace DbFirst.Infrastructure.Repositories;
|
||||||
|
|
||||||
|
public class TimeRepository : ITimeRepository
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _db;
|
||||||
|
|
||||||
|
public TimeRepository(ApplicationDbContext db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InsertAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _db.Database.ExecuteSqlRawAsync("INSERT INTO [TIME] (NOW) VALUES (GETDATE())", cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TimeRecord?> GetLastAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _db.Times
|
||||||
|
.AsNoTracking()
|
||||||
|
.OrderByDescending(t => t.Now)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
DbFirst.sln
@@ -11,8 +11,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.Infrastructure", "D
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.Domain", "DbFirst.Domain\DbFirst.Domain.csproj", "{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.Domain", "DbFirst.Domain\DbFirst.Domain.csproj", "{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.BlazorWasm", "DbFirst.BlazorWasm\DbFirst.BlazorWasm.csproj", "{666BE786-6D04-4224-9948-FF13597481A0}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.BlazorWebApp", "DbFirst.BlazorWebApp\DbFirst.BlazorWebApp.csproj", "{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.BlazorWebApp", "DbFirst.BlazorWebApp\DbFirst.BlazorWebApp.csproj", "{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
@@ -73,18 +71,6 @@ Global
|
|||||||
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|x64.Build.0 = Release|Any CPU
|
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|x86.ActiveCfg = Release|Any CPU
|
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|x86.Build.0 = Release|Any CPU
|
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{666BE786-6D04-4224-9948-FF13597481A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{666BE786-6D04-4224-9948-FF13597481A0}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{666BE786-6D04-4224-9948-FF13597481A0}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{666BE786-6D04-4224-9948-FF13597481A0}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{666BE786-6D04-4224-9948-FF13597481A0}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{666BE786-6D04-4224-9948-FF13597481A0}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
|||||||