Compare commits
13 Commits
940df826f7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d7b3591cc | ||
|
|
006ee78422 | ||
|
|
a52d615750 | ||
|
|
9bbe34dece | ||
|
|
05ea47f42c | ||
|
|
945c8aaf4a | ||
|
|
4ef80ce875 | ||
|
|
88c34ef94b | ||
|
|
85b9b0b51a | ||
|
|
013088a25f | ||
|
|
dbe1d9d206 | ||
|
|
dc2cccac1f | ||
|
|
32b6d30ba1 |
53
DbFirst.API/Controllers/MassDataController.cs
Normal file
53
DbFirst.API/Controllers/MassDataController.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using DbFirst.Application.MassData;
|
||||
using DbFirst.Application.MassData.Commands;
|
||||
using DbFirst.Application.MassData.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DbFirst.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class MassDataController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public MassDataController(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
[HttpGet("count")]
|
||||
public async Task<ActionResult<int>> GetCount(CancellationToken cancellationToken)
|
||||
{
|
||||
var count = await _mediator.Send(new GetMassDataCountQuery(), cancellationToken);
|
||||
return Ok(count);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<MassDataReadDto>>> GetAll([FromQuery] int? skip, [FromQuery] int? take, CancellationToken cancellationToken)
|
||||
{
|
||||
var resolvedTake = take is null or <= 0 ? 200 : take;
|
||||
var result = await _mediator.Send(new GetAllMassDataQuery(skip, resolvedTake), cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("{customerName}")]
|
||||
public async Task<ActionResult<MassDataReadDto>> GetByCustomerName(string customerName, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _mediator.Send(new GetMassDataByCustomerNameQuery(customerName), cancellationToken);
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost("upsert")]
|
||||
public async Task<ActionResult<MassDataReadDto>> Upsert(MassDataWriteDto dto, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _mediator.Send(new UpsertMassDataByCustomerNameCommand(dto), cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
19
DbFirst.API/Dashboards/DashboardChangeNotifier.cs
Normal file
19
DbFirst.API/Dashboards/DashboardChangeNotifier.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using DbFirst.API.Hubs;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace DbFirst.API.Dashboards;
|
||||
|
||||
public class DashboardChangeNotifier : IDashboardChangeNotifier
|
||||
{
|
||||
private readonly IHubContext<DashboardsHub> _hubContext;
|
||||
|
||||
public DashboardChangeNotifier(IHubContext<DashboardsHub> hubContext)
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
}
|
||||
|
||||
public void NotifyChanged()
|
||||
{
|
||||
_ = _hubContext.Clients.All.SendAsync("DashboardsChanged");
|
||||
}
|
||||
}
|
||||
6
DbFirst.API/Dashboards/IDashboardChangeNotifier.cs
Normal file
6
DbFirst.API/Dashboards/IDashboardChangeNotifier.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace DbFirst.API.Dashboards;
|
||||
|
||||
public interface IDashboardChangeNotifier
|
||||
{
|
||||
void NotifyChanged();
|
||||
}
|
||||
@@ -11,12 +11,14 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
|
||||
private readonly string _connectionString;
|
||||
private readonly string _tableName;
|
||||
private readonly Func<string?>? _userProvider;
|
||||
private readonly IDashboardChangeNotifier? _notifier;
|
||||
|
||||
public SqlDashboardStorage(string connectionString, string tableName = "TBDD_SMF_CONFIG", Func<string?>? userProvider = null)
|
||||
public SqlDashboardStorage(string connectionString, string tableName = "TBDD_SMF_CONFIG", Func<string?>? userProvider = null, IDashboardChangeNotifier? notifier = null)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
_tableName = tableName;
|
||||
_userProvider = userProvider;
|
||||
_notifier = notifier;
|
||||
}
|
||||
|
||||
public IEnumerable<DashboardInfo> GetAvailableDashboardsInfo()
|
||||
@@ -98,6 +100,7 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
|
||||
|
||||
connection.Open();
|
||||
command.ExecuteNonQuery();
|
||||
_notifier?.NotifyChanged();
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -118,6 +121,8 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
|
||||
{
|
||||
throw new ArgumentException($"Dashboard '{dashboardId}' not found.");
|
||||
}
|
||||
|
||||
_notifier?.NotifyChanged();
|
||||
}
|
||||
|
||||
public void DeleteDashboard(string dashboardId)
|
||||
@@ -128,5 +133,6 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
|
||||
|
||||
connection.Open();
|
||||
command.ExecuteNonQuery();
|
||||
_notifier?.NotifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
7
DbFirst.API/Hubs/DashboardsHub.cs
Normal file
7
DbFirst.API/Hubs/DashboardsHub.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace DbFirst.API.Hubs;
|
||||
|
||||
public class DashboardsHub : Hub
|
||||
{
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using DbFirst.API.Middleware;
|
||||
using DbFirst.API.Dashboards;
|
||||
using DbFirst.API.Hubs;
|
||||
using DbFirst.Application;
|
||||
using DbFirst.Application.Repositories;
|
||||
using DbFirst.Domain;
|
||||
@@ -51,8 +52,11 @@ builder.Services.AddInfrastructure(builder.Configuration);
|
||||
builder.Services.AddApplication();
|
||||
|
||||
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
|
||||
builder.Services.AddScoped<IMassDataRepository, MassDataRepository>();
|
||||
|
||||
builder.Services.AddDevExpressControls();
|
||||
builder.Services.AddSignalR();
|
||||
builder.Services.AddSingleton<IDashboardChangeNotifier, DashboardChangeNotifier>();
|
||||
builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvider) => {
|
||||
var dashboardsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "Dashboards");
|
||||
Directory.CreateDirectory(dashboardsPath);
|
||||
@@ -112,7 +116,8 @@ builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvi
|
||||
DashboardConfigurator configurator = new DashboardConfigurator();
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? string.Empty;
|
||||
var dashboardStorage = new SqlDashboardStorage(connectionString, "TBDD_SMF_CONFIG");
|
||||
var notifier = serviceProvider.GetRequiredService<IDashboardChangeNotifier>();
|
||||
var dashboardStorage = new SqlDashboardStorage(connectionString, "TBDD_SMF_CONFIG", notifier: notifier);
|
||||
configurator.SetDashboardStorage(dashboardStorage);
|
||||
|
||||
DataSourceInMemoryStorage dataSourceStorage = new DataSourceInMemoryStorage();
|
||||
@@ -155,7 +160,7 @@ app.UseCors();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapDashboardRoute("api/dashboard", "DefaultDashboard");
|
||||
|
||||
app.MapHub<DashboardsHub>("/hubs/dashboards");
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;"
|
||||
"DefaultConnection": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;",
|
||||
"MassDataConnection": "Server=SDD-VMP04-SQL19\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;"
|
||||
},
|
||||
"Dashboard": {
|
||||
"BaseUrl": "https://localhost:7204"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.MassData.Commands;
|
||||
|
||||
public record UpsertMassDataByCustomerNameCommand(MassDataWriteDto Dto) : IRequest<MassDataReadDto>;
|
||||
@@ -0,0 +1,24 @@
|
||||
using AutoMapper;
|
||||
using DbFirst.Application.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.MassData.Commands;
|
||||
|
||||
public class UpsertMassDataByCustomerNameHandler : IRequestHandler<UpsertMassDataByCustomerNameCommand, MassDataReadDto>
|
||||
{
|
||||
private readonly IMassDataRepository _repository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public UpsertMassDataByCustomerNameHandler(IMassDataRepository repository, IMapper mapper)
|
||||
{
|
||||
_repository = repository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<MassDataReadDto> Handle(UpsertMassDataByCustomerNameCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var dto = request.Dto;
|
||||
var updated = await _repository.UpsertByCustomerNameAsync(dto.CustomerName, dto.Amount, dto.StatusFlag, dto.Category, cancellationToken);
|
||||
return _mapper.Map<MassDataReadDto>(updated);
|
||||
}
|
||||
}
|
||||
12
DbFirst.Application/MassData/MassDataProfile.cs
Normal file
12
DbFirst.Application/MassData/MassDataProfile.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using AutoMapper;
|
||||
using DbFirst.Domain.Entities;
|
||||
|
||||
namespace DbFirst.Application.MassData;
|
||||
|
||||
public class MassDataProfile : Profile
|
||||
{
|
||||
public MassDataProfile()
|
||||
{
|
||||
CreateMap<Massdata, MassDataReadDto>();
|
||||
}
|
||||
}
|
||||
12
DbFirst.Application/MassData/MassDataReadDto.cs
Normal file
12
DbFirst.Application/MassData/MassDataReadDto.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace DbFirst.Application.MassData;
|
||||
|
||||
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; }
|
||||
}
|
||||
9
DbFirst.Application/MassData/MassDataWriteDto.cs
Normal file
9
DbFirst.Application/MassData/MassDataWriteDto.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace DbFirst.Application.MassData;
|
||||
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using AutoMapper;
|
||||
using DbFirst.Application.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.MassData.Queries;
|
||||
|
||||
public class GetAllMassDataHandler : IRequestHandler<GetAllMassDataQuery, List<MassDataReadDto>>
|
||||
{
|
||||
private readonly IMassDataRepository _repository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public GetAllMassDataHandler(IMassDataRepository repository, IMapper mapper)
|
||||
{
|
||||
_repository = repository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<List<MassDataReadDto>> Handle(GetAllMassDataQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var items = await _repository.GetAllAsync(request.Skip, request.Take, cancellationToken);
|
||||
return _mapper.Map<List<MassDataReadDto>>(items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.MassData.Queries;
|
||||
|
||||
public record GetAllMassDataQuery(int? Skip, int? Take) : IRequest<List<MassDataReadDto>>;
|
||||
@@ -0,0 +1,23 @@
|
||||
using AutoMapper;
|
||||
using DbFirst.Application.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.MassData.Queries;
|
||||
|
||||
public class GetMassDataByCustomerNameHandler : IRequestHandler<GetMassDataByCustomerNameQuery, MassDataReadDto?>
|
||||
{
|
||||
private readonly IMassDataRepository _repository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public GetMassDataByCustomerNameHandler(IMassDataRepository repository, IMapper mapper)
|
||||
{
|
||||
_repository = repository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<MassDataReadDto?> Handle(GetMassDataByCustomerNameQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var item = await _repository.GetByCustomerNameAsync(request.CustomerName, cancellationToken);
|
||||
return item == null ? null : _mapper.Map<MassDataReadDto>(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.MassData.Queries;
|
||||
|
||||
public record GetMassDataByCustomerNameQuery(string CustomerName) : IRequest<MassDataReadDto?>;
|
||||
@@ -0,0 +1,19 @@
|
||||
using DbFirst.Application.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.MassData.Queries;
|
||||
|
||||
public class GetMassDataCountHandler : IRequestHandler<GetMassDataCountQuery, int>
|
||||
{
|
||||
private readonly IMassDataRepository _repository;
|
||||
|
||||
public GetMassDataCountHandler(IMassDataRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<int> Handle(GetMassDataCountQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _repository.GetCountAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.MassData.Queries;
|
||||
|
||||
public record GetMassDataCountQuery : IRequest<int>;
|
||||
11
DbFirst.Application/Repositories/IMassDataRepository.cs
Normal file
11
DbFirst.Application/Repositories/IMassDataRepository.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using DbFirst.Domain.Entities;
|
||||
|
||||
namespace DbFirst.Application.Repositories;
|
||||
|
||||
public interface IMassDataRepository
|
||||
{
|
||||
Task<int> GetCountAsync(CancellationToken cancellationToken = default);
|
||||
Task<List<Massdata>> GetAllAsync(int? skip = null, int? take = null, CancellationToken cancellationToken = default);
|
||||
Task<Massdata?> GetByCustomerNameAsync(string customerName, CancellationToken cancellationToken = default);
|
||||
Task<Massdata> UpsertByCustomerNameAsync(string customerName, decimal amount, bool statusFlag, string category, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@inject CatalogApiClient Api
|
||||
|
||||
<style>
|
||||
@@ -44,6 +45,9 @@
|
||||
background-position: right 0.5rem center;
|
||||
background-size: 0.9rem;
|
||||
}
|
||||
.catalog-edit-popup {
|
||||
min-width: 720px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||
@@ -55,41 +59,6 @@ else if (!string.IsNullOrWhiteSpace(infoMessage))
|
||||
<div class="alert alert-success" role="alert">@infoMessage</div>
|
||||
}
|
||||
|
||||
<div class="mb-3">
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Primary" Click="@StartCreate">Neuen Eintrag anlegen</DxButton>
|
||||
</div>
|
||||
|
||||
@if (showForm)
|
||||
{
|
||||
<div class="action-panel">
|
||||
<EditForm Model="formModel" OnValidSubmit="HandleSubmit" Context="editCtx">
|
||||
<DxFormLayout ColCount="2">
|
||||
<DxFormLayoutItem Caption="Titel" Context="itemCtx">
|
||||
<DxTextBox @bind-Text="formModel.CatTitle" Enabled="@(isEditing ? formModel.UpdateProcedure != 0 : true)" />
|
||||
</DxFormLayoutItem>
|
||||
<DxFormLayoutItem Caption="Kennung" Context="itemCtx">
|
||||
<DxTextBox @bind-Text="formModel.CatString" />
|
||||
</DxFormLayoutItem>
|
||||
@if (isEditing)
|
||||
{
|
||||
<DxFormLayoutItem Caption="Update-Prozedur" Context="itemCtx">
|
||||
<DxComboBox Data="@procedureOptions"
|
||||
TextFieldName="Text"
|
||||
ValueFieldName="Value"
|
||||
@bind-Value="formModel.UpdateProcedure" />
|
||||
</DxFormLayoutItem>
|
||||
}
|
||||
<DxFormLayoutItem Caption=" " Context="itemCtx">
|
||||
<DxStack Orientation="Orientation.Horizontal" Spacing="8">
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Success" ButtonType="ButtonType.Submit" SubmitFormOnClick="true" Context="btnCtx">@((isEditing ? "Speichern" : "Anlegen"))</DxButton>
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Click="@CancelEdit" Context="btnCtx">Abbrechen</DxButton>
|
||||
</DxStack>
|
||||
</DxFormLayoutItem>
|
||||
</DxFormLayout>
|
||||
</EditForm>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isLoading)
|
||||
{
|
||||
<p><em>Lade Daten...</em></p>
|
||||
@@ -101,8 +70,20 @@ else if (items.Count == 0)
|
||||
else
|
||||
{
|
||||
<div class="grid-section">
|
||||
<DxGrid Data="@items" TItem="CatalogReadDto" KeyFieldName="@nameof(CatalogReadDto.Guid)" ShowFilterRow="true" PageSize="10" CssClass="mb-4 catalog-grid">
|
||||
<DxGrid Data="@items"
|
||||
TItem="CatalogReadDto"
|
||||
KeyFieldName="@nameof(CatalogReadDto.Guid)"
|
||||
ShowFilterRow="true"
|
||||
PageSize="10"
|
||||
CssClass="mb-4 catalog-grid"
|
||||
EditMode="GridEditMode.PopupEditForm"
|
||||
PopupEditFormCssClass="catalog-edit-popup"
|
||||
PopupEditFormHeaderText="@popupHeaderText"
|
||||
CustomizeEditModel="OnCustomizeEditModel"
|
||||
EditModelSaving="OnEditModelSaving"
|
||||
DataItemDeleting="OnDataItemDeleting">
|
||||
<Columns>
|
||||
<DxGridCommandColumn Width="120px" />
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.Guid)" Caption="Id" Width="140px" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
||||
@@ -124,45 +105,73 @@ else
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von">
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" />
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von">
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" />
|
||||
<DxGridDataColumn Caption="" Width="220px" AllowSort="false">
|
||||
<CellDisplayTemplate Context="cell">
|
||||
@{ var item = (CatalogReadDto)cell.DataItem; }
|
||||
<div style="white-space: nowrap;">
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Size="ButtonSize.Small" Click="@(() => StartEdit(item))">Bearbeiten</DxButton>
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Danger" Size="ButtonSize.Small" Click="@(() => DeleteCatalog(item.Guid))">Löschen</DxButton>
|
||||
</div>
|
||||
</CellDisplayTemplate>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
</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 CatalogWriteDto formModel = new();
|
||||
private int editingId;
|
||||
private bool isLoading;
|
||||
private bool isEditing;
|
||||
private bool showForm;
|
||||
private string? errorMessage;
|
||||
private string? infoMessage;
|
||||
private EditContext? editContext;
|
||||
private ValidationMessageStore? validationMessageStore;
|
||||
private string popupHeaderText = "Edit";
|
||||
|
||||
private readonly List<ProcedureOption> procedureOptions = new()
|
||||
{
|
||||
@@ -175,6 +184,66 @@ else
|
||||
await LoadCatalogs();
|
||||
}
|
||||
|
||||
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 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;
|
||||
@@ -194,88 +263,115 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private void StartCreate()
|
||||
private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
|
||||
{
|
||||
formModel = new CatalogWriteDto();
|
||||
editingId = 0;
|
||||
isEditing = false;
|
||||
showForm = true;
|
||||
infoMessage = null;
|
||||
errorMessage = null;
|
||||
}
|
||||
infoMessage = null;
|
||||
|
||||
private void StartEdit(CatalogReadDto item)
|
||||
{
|
||||
formModel = new CatalogWriteDto
|
||||
validationMessageStore?.Clear();
|
||||
editContext?.NotifyValidationStateChanged();
|
||||
|
||||
var editModel = (CatalogEditModel)e.EditModel;
|
||||
if (!ValidateEditModel(editModel, e.IsNew))
|
||||
{
|
||||
CatTitle = item.CatTitle,
|
||||
CatString = item.CatString,
|
||||
UpdateProcedure = 0
|
||||
};
|
||||
editingId = item.Guid;
|
||||
isEditing = true;
|
||||
showForm = true;
|
||||
infoMessage = null;
|
||||
errorMessage = null;
|
||||
}
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
errorMessage = null;
|
||||
infoMessage = null;
|
||||
var dto = new CatalogWriteDto
|
||||
{
|
||||
CatTitle = editModel.CatTitle,
|
||||
CatString = editModel.CatString,
|
||||
UpdateProcedure = editModel.UpdateProcedure
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
if (isEditing)
|
||||
if (e.IsNew)
|
||||
{
|
||||
var updated = await Api.UpdateAsync(editingId, formModel);
|
||||
if (!updated.Success)
|
||||
{
|
||||
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog aktualisiert.";
|
||||
}
|
||||
else
|
||||
{
|
||||
var created = await Api.CreateAsync(formModel);
|
||||
var created = await Api.CreateAsync(dto);
|
||||
if (!created.Success || created.Value == null)
|
||||
{
|
||||
errorMessage = created.Error ?? "Anlegen fehlgeschlagen.";
|
||||
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.";
|
||||
}
|
||||
|
||||
showForm = false;
|
||||
await LoadCatalogs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Speichern: {ex.Message}";
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelEdit()
|
||||
private void AddValidationError(CatalogEditModel editModel, string fieldName, string message)
|
||||
{
|
||||
showForm = false;
|
||||
infoMessage = null;
|
||||
errorMessage = null;
|
||||
if (editContext == null || validationMessageStore == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var field = new FieldIdentifier(editModel, fieldName);
|
||||
validationMessageStore.Add(field, message);
|
||||
editContext.NotifyValidationStateChanged();
|
||||
}
|
||||
|
||||
private async Task DeleteCatalog(int id)
|
||||
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(id);
|
||||
var deleted = await Api.DeleteAsync(item.Guid);
|
||||
if (!deleted.Success)
|
||||
{
|
||||
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -285,9 +381,25 @@ else
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Löschen: {ex.Message}";
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPopupHeaderText(bool isNew)
|
||||
{
|
||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
408
DbFirst.BlazorWasm/Components/MassDataGrid.razor
Normal file
408
DbFirst.BlazorWasm/Components/MassDataGrid.razor
Normal file
@@ -0,0 +1,408 @@
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@inject MassDataApiClient Api
|
||||
|
||||
<style>
|
||||
.action-panel { margin-bottom: 16px; }
|
||||
.grid-section { margin-top: 12px; }
|
||||
.pager-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
</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="grid-section">
|
||||
<DxGrid Data="@items"
|
||||
TItem="MassDataReadDto"
|
||||
KeyFieldName="@nameof(MassDataReadDto.Id)"
|
||||
ShowFilterRow="true"
|
||||
ShowGroupPanel="true"
|
||||
AllowColumnResize="true"
|
||||
PagerVisible="false"
|
||||
PageSize="100"
|
||||
CssClass="mb-3 massdata-grid"
|
||||
EditMode="GridEditMode.PopupEditForm"
|
||||
PopupEditFormCssClass="massdata-edit-popup"
|
||||
PopupEditFormHeaderText="@popupHeaderText"
|
||||
CustomizeEditModel="OnCustomizeEditModel"
|
||||
EditModelSaving="OnEditModelSaving"
|
||||
DataItemDeleting="OnDataItemDeleting">
|
||||
<Columns>
|
||||
<DxGridCommandColumn Width="120px" />
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Id)" Caption="Id" Width="90px" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.CustomerName)" Caption="CustomerName">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Amount)" Caption="Amount" DisplayFormat="c2">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Category)" Caption="Category" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.StatusFlag)" Caption="Status" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxComboBox Data="@statusFilterOptions"
|
||||
TData="BoolFilterOption"
|
||||
TValue="bool?"
|
||||
TextFieldName="Text"
|
||||
ValueFieldName="Value"
|
||||
Value="@(filter.FilterRowValue as bool?)"
|
||||
ValueChanged="@(value => filter.FilterRowValue = value)"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.AddedWhen)" Caption="Angelegt am" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.ChangedWhen)" Caption="Geändert am" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
</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>
|
||||
|
||||
<div class="pager-container">
|
||||
<DxPager PageCount="@pageCount" ActivePageIndex="@pageIndex" ActivePageIndexChanged="OnPageChanged" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private const int PageSize = 100;
|
||||
private List<MassDataReadDto> items = new();
|
||||
private bool isLoading;
|
||||
private string? errorMessage;
|
||||
private string? infoMessage;
|
||||
private int pageIndex;
|
||||
private int pageCount = 1;
|
||||
private string popupHeaderText = "Edit";
|
||||
private EditContext? editContext;
|
||||
private ValidationMessageStore? validationMessageStore;
|
||||
|
||||
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()
|
||||
{
|
||||
await LoadPage(0);
|
||||
}
|
||||
|
||||
private async Task LoadPage(int page)
|
||||
{
|
||||
isLoading = true;
|
||||
errorMessage = null;
|
||||
try
|
||||
{
|
||||
var total = await Api.GetCountAsync();
|
||||
pageCount = Math.Max(1, (int)Math.Ceiling(total / (double)PageSize));
|
||||
pageIndex = Math.Clamp(page, 0, pageCount - 1);
|
||||
|
||||
var skip = pageIndex * PageSize;
|
||||
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 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 void 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 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;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
<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>
|
||||
|
||||
@@ -22,10 +22,15 @@
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="dashboards/default">
|
||||
<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>
|
||||
|
||||
|
||||
7
DbFirst.BlazorWasm/Models/DashboardInfoDto.cs
Normal file
7
DbFirst.BlazorWasm/Models/DashboardInfoDto.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace DbFirst.BlazorWasm.Models;
|
||||
|
||||
public class DashboardInfoDto
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
12
DbFirst.BlazorWasm/Models/MassDataReadDto.cs
Normal file
12
DbFirst.BlazorWasm/Models/MassDataReadDto.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
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; }
|
||||
}
|
||||
9
DbFirst.BlazorWasm/Models/MassDataWriteDto.cs
Normal file
9
DbFirst.BlazorWasm/Models/MassDataWriteDto.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
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 +1,9 @@
|
||||
@page "/dashboard"
|
||||
@page "/dashboards/{DashboardId?}"
|
||||
@implements IAsyncDisposable
|
||||
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
|
||||
@inject NavigationManager Navigation
|
||||
@inject DashboardApiClient DashboardApi
|
||||
|
||||
<style>
|
||||
.dashboard-shell {
|
||||
@@ -48,24 +50,114 @@
|
||||
<div class="dashboard-shell">
|
||||
<aside class="dashboard-nav">
|
||||
<div class="dashboard-nav-title">Dashboards</div>
|
||||
<NavLink class="dashboard-nav-link" href="dashboards/default">Default Dashboard (Designer)</NavLink>
|
||||
@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">
|
||||
<DxDashboard Endpoint="@DashboardEndpoint" InitialDashboardId="DefaultDashboard" WorkingMode="WorkingMode.Designer" style="width: 100%; height: 800px;">
|
||||
<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 void OnParametersSet()
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase))
|
||||
await RefreshDashboards();
|
||||
|
||||
_hubConnection = new HubConnectionBuilder()
|
||||
.WithUrl(HubEndpoint)
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
|
||||
_hubConnection.On("DashboardsChanged", async () =>
|
||||
{
|
||||
Navigation.NavigateTo("dashboards/default", replace: true);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
7
DbFirst.BlazorWasm/Pages/Massdata.razor
Normal file
7
DbFirst.BlazorWasm/Pages/Massdata.razor
Normal file
@@ -0,0 +1,7 @@
|
||||
@page "/massdata"
|
||||
|
||||
<PageTitle>MassData</PageTitle>
|
||||
|
||||
<h1>MassData</h1>
|
||||
|
||||
<MassDataGrid />
|
||||
@@ -17,5 +17,7 @@ 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>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
21
DbFirst.BlazorWasm/Services/DashboardApiClient.cs
Normal file
21
DbFirst.BlazorWasm/Services/DashboardApiClient.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
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>();
|
||||
}
|
||||
}
|
||||
52
DbFirst.BlazorWasm/Services/MassDataApiClient.cs
Normal file
52
DbFirst.BlazorWasm/Services/MassDataApiClient.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
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 result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>($"{Endpoint}?skip={skip}&take={take}");
|
||||
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>();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
@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
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@inject CatalogApiClient Api
|
||||
|
||||
<style>
|
||||
.action-panel { margin-bottom: 16px; }
|
||||
.grid-section { margin-top: 12px; }
|
||||
.catalog-edit-popup {
|
||||
min-width: 720px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||
@@ -14,41 +18,6 @@ else if (!string.IsNullOrWhiteSpace(infoMessage))
|
||||
<div class="alert alert-success" role="alert">@infoMessage</div>
|
||||
}
|
||||
|
||||
<div class="mb-3">
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Primary" Click="@StartCreate">Neuen Eintrag anlegen</DxButton>
|
||||
</div>
|
||||
|
||||
@if (showForm)
|
||||
{
|
||||
<div class="action-panel">
|
||||
<EditForm Model="formModel" OnValidSubmit="HandleSubmit" Context="editCtx">
|
||||
<DxFormLayout ColCount="2">
|
||||
<DxFormLayoutItem Caption="Titel" Context="itemCtx">
|
||||
<DxTextBox @bind-Text="formModel.CatTitle" Enabled="@(isEditing ? formModel.UpdateProcedure != 0 : true)" />
|
||||
</DxFormLayoutItem>
|
||||
<DxFormLayoutItem Caption="Kennung" Context="itemCtx">
|
||||
<DxTextBox @bind-Text="formModel.CatString" />
|
||||
</DxFormLayoutItem>
|
||||
@if (isEditing)
|
||||
{
|
||||
<DxFormLayoutItem Caption="Update-Prozedur" Context="itemCtx">
|
||||
<DxComboBox Data="@procedureOptions"
|
||||
TextFieldName="Text"
|
||||
ValueFieldName="Value"
|
||||
@bind-Value="formModel.UpdateProcedure" />
|
||||
</DxFormLayoutItem>
|
||||
}
|
||||
<DxFormLayoutItem Caption=" " Context="itemCtx">
|
||||
<DxStack Orientation="Orientation.Horizontal" Spacing="8">
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Success" ButtonType="ButtonType.Submit" SubmitFormOnClick="true" Context="btnCtx">@((isEditing ? "Speichern" : "Anlegen"))</DxButton>
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Click="@CancelEdit" Context="btnCtx">Abbrechen</DxButton>
|
||||
</DxStack>
|
||||
</DxFormLayoutItem>
|
||||
</DxFormLayout>
|
||||
</EditForm>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isLoading)
|
||||
{
|
||||
<p><em>Lade Daten...</em></p>
|
||||
@@ -60,38 +29,108 @@ else if (items.Count == 0)
|
||||
else
|
||||
{
|
||||
<div class="grid-section">
|
||||
<DxGrid Data="@items" TItem="CatalogReadDto" KeyFieldName="@nameof(CatalogReadDto.Guid)" ShowFilterRow="true" PageSize="10" CssClass="mb-4 catalog-grid">
|
||||
<DxGrid Data="@items"
|
||||
TItem="CatalogReadDto"
|
||||
KeyFieldName="@nameof(CatalogReadDto.Guid)"
|
||||
ShowFilterRow="true"
|
||||
PageSize="10"
|
||||
CssClass="mb-4 catalog-grid"
|
||||
EditMode="GridEditMode.PopupEditForm"
|
||||
PopupEditFormCssClass="catalog-edit-popup"
|
||||
PopupEditFormHeaderText="@popupHeaderText"
|
||||
CustomizeEditModel="OnCustomizeEditModel"
|
||||
EditModelSaving="OnEditModelSaving"
|
||||
DataItemDeleting="OnDataItemDeleting">
|
||||
<Columns>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.Guid)" Caption="Id" Width="140px" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending" />
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatTitle)" Caption="Titel" />
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatString)" Caption="String" />
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von" />
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" />
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von" />
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" />
|
||||
<DxGridDataColumn Caption="" Width="220px" AllowSort="false">
|
||||
<CellDisplayTemplate Context="cell">
|
||||
@{ var item = (CatalogReadDto)cell.DataItem; }
|
||||
<div style="white-space: nowrap;">
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Size="ButtonSize.Small" Click="@(() => StartEdit(item))">Bearbeiten</DxButton>
|
||||
<DxButton RenderStyle="ButtonRenderStyle.Danger" Size="ButtonSize.Small" Click="@(() => DeleteCatalog(item.Guid))">Löschen</DxButton>
|
||||
</div>
|
||||
</CellDisplayTemplate>
|
||||
<DxGridCommandColumn Width="120px" />
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.Guid)" Caption="Id" Width="140px" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatTitle)" Caption="Titel">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatString)" Caption="String">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
</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 CatalogWriteDto formModel = new();
|
||||
private int editingId;
|
||||
private bool isLoading;
|
||||
private bool isEditing;
|
||||
private bool showForm;
|
||||
private string? errorMessage;
|
||||
private string? infoMessage;
|
||||
private EditContext? editContext;
|
||||
private ValidationMessageStore? validationMessageStore;
|
||||
private string popupHeaderText = "Edit";
|
||||
|
||||
private readonly List<ProcedureOption> procedureOptions = new()
|
||||
{
|
||||
@@ -104,6 +143,71 @@ else
|
||||
await LoadCatalogs();
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -123,88 +227,115 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private void StartCreate()
|
||||
private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
|
||||
{
|
||||
formModel = new CatalogWriteDto();
|
||||
editingId = 0;
|
||||
isEditing = false;
|
||||
showForm = true;
|
||||
infoMessage = null;
|
||||
errorMessage = null;
|
||||
}
|
||||
infoMessage = null;
|
||||
|
||||
private void StartEdit(CatalogReadDto item)
|
||||
{
|
||||
formModel = new CatalogWriteDto
|
||||
validationMessageStore?.Clear();
|
||||
editContext?.NotifyValidationStateChanged();
|
||||
|
||||
var editModel = (CatalogEditModel)e.EditModel;
|
||||
if (!ValidateEditModel(editModel, e.IsNew))
|
||||
{
|
||||
CatTitle = item.CatTitle,
|
||||
CatString = item.CatString,
|
||||
UpdateProcedure = 0
|
||||
};
|
||||
editingId = item.Guid;
|
||||
isEditing = true;
|
||||
showForm = true;
|
||||
infoMessage = null;
|
||||
errorMessage = null;
|
||||
}
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
errorMessage = null;
|
||||
infoMessage = null;
|
||||
var dto = new CatalogWriteDto
|
||||
{
|
||||
CatTitle = editModel.CatTitle,
|
||||
CatString = editModel.CatString,
|
||||
UpdateProcedure = editModel.UpdateProcedure
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
if (isEditing)
|
||||
if (e.IsNew)
|
||||
{
|
||||
var updated = await Api.UpdateAsync(editingId, formModel);
|
||||
if (!updated.Success)
|
||||
{
|
||||
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog aktualisiert.";
|
||||
}
|
||||
else
|
||||
{
|
||||
var created = await Api.CreateAsync(formModel);
|
||||
var created = await Api.CreateAsync(dto);
|
||||
if (!created.Success || created.Value == null)
|
||||
{
|
||||
errorMessage = created.Error ?? "Anlegen fehlgeschlagen.";
|
||||
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.";
|
||||
}
|
||||
|
||||
showForm = false;
|
||||
await LoadCatalogs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Speichern: {ex.Message}";
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelEdit()
|
||||
private void AddValidationError(CatalogEditModel editModel, string fieldName, string message)
|
||||
{
|
||||
showForm = false;
|
||||
infoMessage = null;
|
||||
errorMessage = null;
|
||||
if (editContext == null || validationMessageStore == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var field = new FieldIdentifier(editModel, fieldName);
|
||||
validationMessageStore.Add(field, message);
|
||||
editContext.NotifyValidationStateChanged();
|
||||
}
|
||||
|
||||
private async Task DeleteCatalog(int id)
|
||||
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(id);
|
||||
var deleted = await Api.DeleteAsync(item.Guid);
|
||||
if (!deleted.Success)
|
||||
{
|
||||
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -214,9 +345,20 @@ else
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Löschen: {ex.Message}";
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
@@ -32,10 +32,16 @@
|
||||
</div>
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="dashboards/default">
|
||||
<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>
|
||||
|
||||
|
||||
408
DbFirst.BlazorWebApp/Components/MassDataGrid.razor
Normal file
408
DbFirst.BlazorWebApp/Components/MassDataGrid.razor
Normal file
@@ -0,0 +1,408 @@
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@inject MassDataApiClient Api
|
||||
|
||||
<style>
|
||||
.action-panel { margin-bottom: 16px; }
|
||||
.grid-section { margin-top: 12px; }
|
||||
.pager-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
</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="grid-section">
|
||||
<DxGrid Data="@items"
|
||||
TItem="MassDataReadDto"
|
||||
KeyFieldName="@nameof(MassDataReadDto.Id)"
|
||||
ShowFilterRow="true"
|
||||
ShowGroupPanel="true"
|
||||
AllowColumnResize="true"
|
||||
PagerVisible="false"
|
||||
PageSize="100"
|
||||
CssClass="mb-3 massdata-grid"
|
||||
EditMode="GridEditMode.PopupEditForm"
|
||||
PopupEditFormCssClass="massdata-edit-popup"
|
||||
PopupEditFormHeaderText="@popupHeaderText"
|
||||
CustomizeEditModel="OnCustomizeEditModel"
|
||||
EditModelSaving="OnEditModelSaving"
|
||||
DataItemDeleting="OnDataItemDeleting">
|
||||
<Columns>
|
||||
<DxGridCommandColumn Width="120px" />
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Id)" Caption="Id" Width="90px" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.CustomerName)" Caption="CustomerName">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Amount)" Caption="Amount" DisplayFormat="c2">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Category)" Caption="Category" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.StatusFlag)" Caption="Status" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxComboBox Data="@statusFilterOptions"
|
||||
TData="BoolFilterOption"
|
||||
TValue="bool?"
|
||||
TextFieldName="Text"
|
||||
ValueFieldName="Value"
|
||||
Value="@(filter.FilterRowValue as bool?)"
|
||||
ValueChanged="@(value => filter.FilterRowValue = value)"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.AddedWhen)" Caption="Added" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.ChangedWhen)" Caption="Changed" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
</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>
|
||||
|
||||
<div class="pager-container">
|
||||
<DxPager PageCount="@pageCount" ActivePageIndex="@pageIndex" ActivePageIndexChanged="OnPageChanged" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private const int PageSize = 100;
|
||||
private List<MassDataReadDto> items = new();
|
||||
private bool isLoading;
|
||||
private string? errorMessage;
|
||||
private string? infoMessage;
|
||||
private int pageIndex;
|
||||
private int pageCount = 1;
|
||||
private string popupHeaderText = "Edit";
|
||||
private EditContext? editContext;
|
||||
private ValidationMessageStore? validationMessageStore;
|
||||
|
||||
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()
|
||||
{
|
||||
await LoadPage(0);
|
||||
}
|
||||
|
||||
private async Task LoadPage(int page)
|
||||
{
|
||||
isLoading = true;
|
||||
errorMessage = null;
|
||||
try
|
||||
{
|
||||
var total = await Api.GetCountAsync();
|
||||
pageCount = Math.Max(1, (int)Math.Ceiling(total / (double)PageSize));
|
||||
pageIndex = Math.Clamp(page, 0, pageCount - 1);
|
||||
|
||||
var skip = pageIndex * PageSize;
|
||||
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 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 void 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 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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
@page "/dashboard"
|
||||
@page "/dashboards/{DashboardId?}"
|
||||
@implements IAsyncDisposable
|
||||
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
|
||||
@inject NavigationManager Navigation
|
||||
@inject DashboardApiClient DashboardApi
|
||||
|
||||
<style>
|
||||
.dashboard-shell {
|
||||
@@ -48,24 +50,114 @@
|
||||
<div class="dashboard-shell">
|
||||
<aside class="dashboard-nav">
|
||||
<div class="dashboard-nav-title">Dashboards</div>
|
||||
<NavLink class="dashboard-nav-link" href="dashboards/default">Default Dashboard (Designer)</NavLink>
|
||||
@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">
|
||||
<DxDashboard Endpoint="@DashboardEndpoint" InitialDashboardId="DefaultDashboard" WorkingMode="WorkingMode.Designer" style="width: 100%; height: 800px;">
|
||||
<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 void OnParametersSet()
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase))
|
||||
await RefreshDashboards();
|
||||
|
||||
_hubConnection = new HubConnectionBuilder()
|
||||
.WithUrl(HubEndpoint)
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
|
||||
_hubConnection.On("DashboardsChanged", async () =>
|
||||
{
|
||||
Navigation.NavigateTo("dashboards/default", replace: true);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
7
DbFirst.BlazorWebApp/Components/Pages/Massdata.razor
Normal file
7
DbFirst.BlazorWebApp/Components/Pages/Massdata.razor
Normal file
@@ -0,0 +1,7 @@
|
||||
@page "/massdata"
|
||||
|
||||
<PageTitle>MassData</PageTitle>
|
||||
|
||||
<h1>MassData</h1>
|
||||
|
||||
<MassDataGrid />
|
||||
@@ -6,6 +6,7 @@
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using Microsoft.AspNetCore.SignalR.Client
|
||||
@using DbFirst.BlazorWebApp
|
||||
@using DbFirst.BlazorWebApp.Components
|
||||
@using DbFirst.BlazorWebApp.Models
|
||||
|
||||
@@ -12,5 +12,9 @@
|
||||
<PackageReference Include="DevExpress.Blazor.Themes" 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" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
7
DbFirst.BlazorWebApp/Models/DashboardInfoDto.cs
Normal file
7
DbFirst.BlazorWebApp/Models/DashboardInfoDto.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace DbFirst.BlazorWebApp.Models;
|
||||
|
||||
public class DashboardInfoDto
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
12
DbFirst.BlazorWebApp/Models/MassDataReadDto.cs
Normal file
12
DbFirst.BlazorWebApp/Models/MassDataReadDto.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace DbFirst.BlazorWebApp.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; }
|
||||
}
|
||||
9
DbFirst.BlazorWebApp/Models/MassDataWriteDto.cs
Normal file
9
DbFirst.BlazorWebApp/Models/MassDataWriteDto.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace DbFirst.BlazorWebApp.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; }
|
||||
}
|
||||
@@ -17,10 +17,20 @@ if (!string.IsNullOrWhiteSpace(apiBaseUrl))
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
builder.Services.AddHttpClient<DashboardApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
builder.Services.AddHttpClient<MassDataApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Services.AddHttpClient<CatalogApiClient>();
|
||||
builder.Services.AddHttpClient<DashboardApiClient>();
|
||||
builder.Services.AddHttpClient<MassDataApiClient>();
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
21
DbFirst.BlazorWebApp/Services/DashboardApiClient.cs
Normal file
21
DbFirst.BlazorWebApp/Services/DashboardApiClient.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Net.Http.Json;
|
||||
using DbFirst.BlazorWebApp.Models;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.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>();
|
||||
}
|
||||
}
|
||||
52
DbFirst.BlazorWebApp/Services/MassDataApiClient.cs
Normal file
52
DbFirst.BlazorWebApp/Services/MassDataApiClient.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Net.Http.Json;
|
||||
using DbFirst.BlazorWebApp.Models;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.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 result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>($"{Endpoint}?skip={skip}&take={take}");
|
||||
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>();
|
||||
}
|
||||
}
|
||||
12
DbFirst.Domain/Entities/Massdata.cs
Normal file
12
DbFirst.Domain/Entities/Massdata.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace DbFirst.Domain.Entities;
|
||||
|
||||
public class Massdata
|
||||
{
|
||||
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; }
|
||||
}
|
||||
@@ -11,6 +11,10 @@ public static class DependencyInjection
|
||||
services.Configure<TableConfigurations>(configuration.GetSection("TableConfigurations"));
|
||||
services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
services.AddDbContext<MassDataDbContext>(options =>
|
||||
options.UseSqlServer(configuration.GetConnectionString("MassDataConnection")));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
44
DbFirst.Infrastructure/MassDataDbContext.cs
Normal file
44
DbFirst.Infrastructure/MassDataDbContext.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using DbFirst.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DbFirst.Infrastructure;
|
||||
|
||||
public class MassDataDbContext : DbContext
|
||||
{
|
||||
public MassDataDbContext(DbContextOptions<MassDataDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual DbSet<Massdata> Massdata { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Massdata>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.ToTable("MASSDATA");
|
||||
|
||||
entity.Property(e => e.Id).HasColumnName("ID");
|
||||
entity.Property(e => e.CustomerName)
|
||||
.HasMaxLength(200)
|
||||
.IsUnicode(false)
|
||||
.HasColumnName("CustomerName");
|
||||
entity.Property(e => e.Amount)
|
||||
.HasColumnType("decimal(12,2)")
|
||||
.HasColumnName("Amount");
|
||||
entity.Property(e => e.Category)
|
||||
.HasMaxLength(100)
|
||||
.IsUnicode(false)
|
||||
.HasColumnName("Category");
|
||||
entity.Property(e => e.StatusFlag)
|
||||
.HasColumnName("StatusFlag");
|
||||
entity.Property(e => e.AddedWhen)
|
||||
.HasColumnType("datetime")
|
||||
.HasColumnName("ADDED_WHEN");
|
||||
entity.Property(e => e.ChangedWhen)
|
||||
.HasColumnType("datetime")
|
||||
.HasColumnName("CHANGED_WHEN");
|
||||
});
|
||||
}
|
||||
}
|
||||
71
DbFirst.Infrastructure/Repositories/MassDataRepository.cs
Normal file
71
DbFirst.Infrastructure/Repositories/MassDataRepository.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.Data;
|
||||
using DbFirst.Application.Repositories;
|
||||
using DbFirst.Domain.Entities;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DbFirst.Infrastructure.Repositories;
|
||||
|
||||
public class MassDataRepository : IMassDataRepository
|
||||
{
|
||||
private readonly MassDataDbContext _db;
|
||||
|
||||
public MassDataRepository(MassDataDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<int> GetCountAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _db.Massdata.AsNoTracking().CountAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Massdata>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _db.Massdata.AsNoTracking().ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Massdata?> GetByCustomerNameAsync(string customerName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _db.Massdata.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.CustomerName == customerName, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Massdata>> GetAllAsync(int? skip = null, int? take = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _db.Massdata.AsNoTracking().OrderBy(x => x.Id).AsQueryable();
|
||||
|
||||
if (skip.HasValue)
|
||||
{
|
||||
query = query.Skip(skip.Value);
|
||||
}
|
||||
|
||||
if (take.HasValue)
|
||||
{
|
||||
query = query.Take(take.Value);
|
||||
}
|
||||
|
||||
return await query.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Massdata> UpsertByCustomerNameAsync(string customerName, decimal amount, bool statusFlag, string category, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var customerParam = new SqlParameter("@CustomerName", SqlDbType.VarChar, 200) { Value = customerName };
|
||||
var amountParam = new SqlParameter("@Amount", SqlDbType.Decimal) { Value = amount, Precision = 12, Scale = 2 };
|
||||
var statusParam = new SqlParameter("@StatusFlag", SqlDbType.Bit) { Value = statusFlag };
|
||||
var categoryParam = new SqlParameter("@Category", SqlDbType.VarChar, 100) { Value = category };
|
||||
|
||||
await _db.Database.ExecuteSqlRawAsync(
|
||||
"EXEC dbo.PRMassdata_UpsertByCustomerName @CustomerName, @Amount, @StatusFlag, @Category",
|
||||
parameters: new[] { customerParam, amountParam, statusParam, categoryParam },
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
var updated = await GetByCustomerNameAsync(customerName, cancellationToken);
|
||||
if (updated == null)
|
||||
{
|
||||
throw new InvalidOperationException("Upsert completed but record could not be loaded.");
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user