Compare commits

2 Commits

Author SHA1 Message Date
OlgunR
86feec930b Add Clock page with live DB time and TimeApiClient service
Introduced a new Clock page that displays and updates the current database server time every second by calling a backend API. Added the TimeApiClient service to handle API requests for the server time. Registered TimeApiClient in Program.cs and updated the navigation menu to include a link to the new Clock page. Includes error handling and custom UI styling for the clock display.
2026-03-30 15:16:33 +02:00
OlgunR
f5224e20f2 Add time record API endpoint and supporting infrastructure
Introduced a new TimeController with a POST endpoint to insert and retrieve the latest time record. Added ITimeRepository, TimeRepository, and TimeRecord entity. Implemented MediatR command and handler for time insertion. Updated ApplicationDbContext and DI configuration to support the new feature.
2026-03-30 15:16:03 +02:00
90 changed files with 1403 additions and 2390 deletions

View File

@@ -1,7 +1,7 @@
using DbFirst.Application.Catalogs;
using DbFirst.Application.Catalogs.Commands; using DbFirst.Application.Catalogs.Commands;
using DbFirst.Application.Catalogs.Queries; using DbFirst.Application.Catalogs.Queries;
using DbFirst.Contracts.Catalogs; using DbFirst.Domain;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -37,7 +37,7 @@ public class CatalogsController : ControllerBase
} }
[HttpPost] [HttpPost]
public async Task<ActionResult<CatalogReadDto>> Create([FromBody]CatalogWriteDto dto, CancellationToken cancellationToken) public async Task<ActionResult<CatalogReadDto>> Create(CatalogWriteDto dto, CancellationToken cancellationToken)
{ {
var created = await _mediator.Send(new CreateCatalogCommand(dto), cancellationToken); var created = await _mediator.Send(new CreateCatalogCommand(dto), cancellationToken);
if (created == null) if (created == null)
@@ -48,8 +48,19 @@ public class CatalogsController : ControllerBase
} }
[HttpPut("{id:int}")] [HttpPut("{id:int}")]
public async Task<ActionResult<CatalogReadDto>> Update(int id, [FromBody] CatalogWriteDto dto, CancellationToken cancellationToken) public async Task<ActionResult<CatalogReadDto>> Update(int id, CatalogWriteDto dto, CancellationToken cancellationToken)
{ {
var current = await _mediator.Send(new GetCatalogByIdQuery(id), cancellationToken);
if (current == null)
{
return NotFound();
}
if (dto.UpdateProcedure == CatalogUpdateProcedure.Update &&
!string.Equals(current.CatTitle, dto.CatTitle, StringComparison.OrdinalIgnoreCase))
{
return BadRequest("Titel kann nicht geändert werden.");
}
var updated = await _mediator.Send(new UpdateCatalogCommand(id, dto), cancellationToken); var updated = await _mediator.Send(new UpdateCatalogCommand(id, dto), cancellationToken);
if (updated == null) if (updated == null)
{ {

View File

@@ -1,8 +1,7 @@
using System.Text;
using DbFirst.Application.Repositories; using DbFirst.Application.Repositories;
using DbFirst.Contracts.Layouts;
using DbFirst.Domain.Entities; using DbFirst.Domain.Entities;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Text;
namespace DbFirst.API.Controllers; namespace DbFirst.API.Controllers;
@@ -84,4 +83,12 @@ public class LayoutsController : ControllerBase
LayoutData = layoutData LayoutData = layoutData
}; };
} }
public sealed class LayoutDto
{
public string LayoutType { get; set; } = string.Empty;
public string LayoutKey { get; set; } = string.Empty;
public string UserName { get; set; } = string.Empty;
public string LayoutData { get; set; } = string.Empty;
}
} }

View File

@@ -1,6 +1,6 @@
using DbFirst.Application.MassData;
using DbFirst.Application.MassData.Commands; using DbFirst.Application.MassData.Commands;
using DbFirst.Application.MassData.Queries; using DbFirst.Application.MassData.Queries;
using DbFirst.Contracts.MassData;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -50,7 +50,7 @@ public class MassDataController : ControllerBase
} }
[HttpPost("upsert")] [HttpPost("upsert")]
public async Task<ActionResult<MassDataReadDto>> Upsert([FromBody]MassDataWriteDto dto, CancellationToken cancellationToken) public async Task<ActionResult<MassDataReadDto>> Upsert(MassDataWriteDto dto, CancellationToken cancellationToken)
{ {
var result = await _mediator.Send(new UpsertMassDataByCustomerNameCommand(dto), cancellationToken); var result = await _mediator.Send(new UpsertMassDataByCustomerNameCommand(dto), cancellationToken);
return Ok(result); return Ok(result);

View File

@@ -0,0 +1,28 @@
using DbFirst.Application.Time.Commands;
using DbFirst.Domain.Entities;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace DbFirst.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class TimeController : ControllerBase
{
private readonly IMediator _mediator;
public TimeController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<ActionResult<TimeRecord>> InsertAndGetLast(CancellationToken cancellationToken)
{
var result = await _mediator.Send(new InsertTimeCommand(), cancellationToken);
if (result == null)
return NotFound();
return Ok(result);
}
}

View File

@@ -6,23 +6,14 @@ namespace DbFirst.API.Dashboards;
public class DashboardChangeNotifier : IDashboardChangeNotifier public class DashboardChangeNotifier : IDashboardChangeNotifier
{ {
private readonly IHubContext<DashboardsHub> _hubContext; private readonly IHubContext<DashboardsHub> _hubContext;
private readonly ILogger<DashboardChangeNotifier> _logger;
public DashboardChangeNotifier(IHubContext<DashboardsHub> hubContext, ILogger<DashboardChangeNotifier> logger) public DashboardChangeNotifier(IHubContext<DashboardsHub> hubContext)
{ {
_hubContext = hubContext; _hubContext = hubContext;
_logger = logger;
} }
public async Task NotifyChangedAsync() public void NotifyChanged()
{ {
try _ = _hubContext.Clients.All.SendAsync("DashboardsChanged");
{
await _hubContext.Clients.All.SendAsync("DashboardsChanged");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to notify dashboard clients.");
}
} }
} }

View File

@@ -1,115 +0,0 @@
using DbFirst.Domain.Entities;
using DevExpress.DashboardCommon;
using DevExpress.DashboardWeb;
using DevExpress.DataAccess.Json;
using System.Xml.Linq;
namespace DbFirst.API.Dashboards;
public static class DashboardConfiguratorFactory
{
public static DashboardConfigurator Create(
IServiceProvider serviceProvider,
IConfiguration configuration,
IWebHostEnvironment environment)
{
// Den gesamten Inhalt des Lambdas hierher verschieben
// serviceProvider, configuration, environment statt builder.* verwenden
var dashboardsPath = Path.Combine(environment.ContentRootPath, "Data", "Dashboards");
Directory.CreateDirectory(dashboardsPath);
var defaultDashboardPath = Path.Combine(dashboardsPath, "DefaultDashboard.xml");
if (!File.Exists(defaultDashboardPath))
{
var defaultDashboard = new Dashboard();
defaultDashboard.Title.Text = "Default Dashboard";
defaultDashboard.SaveToXml(defaultDashboardPath);
}
var dashboardBaseUrl = configuration["Dashboard:BaseUrl"]
?? configuration["ApiBaseUrl"]
?? configuration["ASPNETCORE_URLS"]?.Split(';', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()
?? "https://localhost:7204";
dashboardBaseUrl = dashboardBaseUrl.TrimEnd('/');
var catalogsGridDashboardPath = Path.Combine(dashboardsPath, "CatalogsGrid.xml");
if (!File.Exists(catalogsGridDashboardPath))
{
var dashboard = new Dashboard();
dashboard.Title.Text = "Catalogs (Dashboard Grid)";
var catalogDataSource = new DashboardJsonDataSource("Catalogs (API)")
{
ComponentName = "catalogsDataSource",
JsonSource = new UriJsonSource(new Uri($"{dashboardBaseUrl}/api/catalogs"))
};
dashboard.DataSources.Add(catalogDataSource);
var grid = new GridDashboardItem
{
DataSource = catalogDataSource,
Name = "Catalogs"
};
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.Guid))) { Name = "Id" });
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.CatTitle))) { Name = "Titel" });
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.CatString))) { Name = "String" });
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.AddedWho))) { Name = "Angelegt von" });
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.AddedWhen))) { Name = "Angelegt am" });
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.ChangedWho))) { Name = "Geändert von" });
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.ChangedWhen))) { Name = "Geändert am" });
dashboard.Items.Add(grid);
var layoutGroup = new DashboardLayoutGroup { Orientation = DashboardLayoutGroupOrientation.Vertical };
layoutGroup.ChildNodes.Add(new DashboardLayoutItem(grid));
dashboard.LayoutRoot = layoutGroup;
dashboard.SaveToXml(catalogsGridDashboardPath);
}
DashboardConfigurator configurator = new DashboardConfigurator();
var connectionString = configuration.GetConnectionString("DefaultConnection") ?? string.Empty;
var notifier = serviceProvider.GetRequiredService<IDashboardChangeNotifier>();
var dashboardStorage = new SqlDashboardStorage(connectionString, "TBDD_SMF_CONFIG", notifier: notifier);
configurator.SetDashboardStorage(dashboardStorage);
DataSourceInMemoryStorage dataSourceStorage = new DataSourceInMemoryStorage();
DashboardJsonDataSource jsonDataSourceUrl = new DashboardJsonDataSource("JSON Data Source (URL)");
jsonDataSourceUrl.JsonSource = new UriJsonSource(
new Uri("https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json"));
jsonDataSourceUrl.RootElement = "Customers";
dataSourceStorage.RegisterDataSource("jsonDataSourceUrl", jsonDataSourceUrl.SaveToXml());
var catalogsJsonDataSource = new DashboardJsonDataSource("Catalogs (API)")
{
ComponentName = "catalogsDataSource",
JsonSource = new UriJsonSource(new Uri($"{dashboardBaseUrl}/api/catalogs"))
};
dataSourceStorage.RegisterDataSource(catalogsJsonDataSource.ComponentName, catalogsJsonDataSource.SaveToXml());
dataSourceStorage.RegisterDataSource(catalogsJsonDataSource.Name, catalogsJsonDataSource.SaveToXml());
configurator.SetDataSourceStorage(dataSourceStorage);
EnsureDashboardInStorage(dashboardStorage, "DefaultDashboard", defaultDashboardPath);
EnsureDashboardInStorage(dashboardStorage, "CatalogsGrid", catalogsGridDashboardPath);
return configurator;
}
private static void EnsureDashboardInStorage(IEditableDashboardStorage storage, string id, string filePath)
{
var exists = storage.GetAvailableDashboardsInfo().Any(info => string.Equals(info.ID, id, StringComparison.OrdinalIgnoreCase));
if (exists || !File.Exists(filePath))
{
return;
}
var doc = XDocument.Load(filePath);
storage.AddDashboard(doc, id);
}
}

View File

@@ -2,5 +2,5 @@ namespace DbFirst.API.Dashboards;
public interface IDashboardChangeNotifier public interface IDashboardChangeNotifier
{ {
Task NotifyChangedAsync(); void NotifyChanged();
} }

View File

@@ -1,8 +1,8 @@
using DevExpress.DashboardWeb;
using Microsoft.Data.SqlClient;
using System.Data; using System.Data;
using System.Text; using System.Text;
using System.Xml.Linq; using System.Xml.Linq;
using DevExpress.DashboardWeb;
using Microsoft.Data.SqlClient;
namespace DbFirst.API.Dashboards; namespace DbFirst.API.Dashboards;
@@ -100,7 +100,7 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
connection.Open(); connection.Open();
command.ExecuteNonQuery(); command.ExecuteNonQuery();
_ = _notifier?.NotifyChangedAsync(); _notifier?.NotifyChanged();
return id; return id;
} }
@@ -122,7 +122,7 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
throw new ArgumentException($"Dashboard '{dashboardId}' not found."); throw new ArgumentException($"Dashboard '{dashboardId}' not found.");
} }
_ = _notifier?.NotifyChangedAsync(); _notifier?.NotifyChanged();
} }
public void DeleteDashboard(string dashboardId) public void DeleteDashboard(string dashboardId)
@@ -133,6 +133,6 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
connection.Open(); connection.Open();
command.ExecuteNonQuery(); command.ExecuteNonQuery();
_ = _notifier?.NotifyChangedAsync(); _notifier?.NotifyChanged();
} }
} }

View File

@@ -7,7 +7,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="16.1.1" /> <PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="DevExpress.AspNetCore.Dashboard" Version="25.2.3" /> <PackageReference Include="DevExpress.AspNetCore.Dashboard" Version="25.2.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.22" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.22" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.22" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.22" />
@@ -16,6 +17,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using System.Net;
using System.Text.Json;
namespace DbFirst.API.Middleware; namespace DbFirst.API.Middleware;
@@ -19,35 +20,33 @@ public class ExceptionHandlingMiddleware
{ {
await _next(context); await _next(context);
} }
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, "Domain validation error");
await WriteProblemAsync(context, StatusCodes.Status400BadRequest, "Eingabe ungültig", ex.Message);
}
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Unhandled exception"); _logger.LogError(ex, "Unhandled exception");
await WriteProblemAsync(context, StatusCodes.Status500InternalServerError, "Serverfehler", ex.Message); await WriteProblemDetailsAsync(context, ex);
} }
} }
private static async Task WriteProblemAsync(HttpContext context, int status, string title, string detail) private static async Task WriteProblemDetailsAsync(HttpContext context, Exception ex)
{ {
if (context.Response.HasStarted) return; if (context.Response.HasStarted)
{
throw ex;
}
context.Response.Clear(); context.Response.Clear();
context.Response.StatusCode = status; context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/problem+json"; context.Response.ContentType = "application/json";
var problem = new ProblemDetails var problem = new
{ {
Type = $"https://tools.ietf.org/html/rfc9110#section-{(status == 400 ? "15.5.1" : "15.6.1")}", type = "https://tools.ietf.org/html/rfc9110#section-15.6.1",
Title = title, title = "Serverfehler",
Status = status, status = context.Response.StatusCode,
Detail = detail, detail = ex.Message,
Extensions = { ["traceId"] = context.TraceIdentifier } traceId = context.TraceIdentifier
}; };
await context.Response.WriteAsJsonAsync(problem); await context.Response.WriteAsync(JsonSerializer.Serialize(problem));
} }
} }

View File

@@ -1,11 +1,18 @@
using DbFirst.API.Middleware;
using DbFirst.API.Dashboards; using DbFirst.API.Dashboards;
using DbFirst.API.Hubs; using DbFirst.API.Hubs;
using DbFirst.API.Middleware;
using DbFirst.Application; using DbFirst.Application;
using DbFirst.Application.Repositories;
using DbFirst.Domain;
using DbFirst.Domain.Entities;
using DbFirst.Infrastructure; using DbFirst.Infrastructure;
using DbFirst.Infrastructure.Repositories;
using DevExpress.AspNetCore; using DevExpress.AspNetCore;
using DevExpress.DashboardAspNetCore; using DevExpress.DashboardAspNetCore;
using DevExpress.DashboardCommon;
using DevExpress.DashboardWeb; using DevExpress.DashboardWeb;
using DevExpress.DataAccess.Json;
using System.Xml.Linq;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -13,7 +20,6 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
builder.Services.AddProblemDetails();
// TODO: allow listed origins configured in appsettings.json // TODO: allow listed origins configured in appsettings.json
// In any case, dont let them to free to use without cors. if there is no origin specified, block all. // In any case, dont let them to free to use without cors. if there is no origin specified, block all.
@@ -30,7 +36,7 @@ builder.Services.AddCors(options =>
} }
else else
{ {
var origins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? []; var origins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
if (origins.Length > 0) if (origins.Length > 0)
{ {
policy.WithOrigins(origins) policy.WithOrigins(origins)
@@ -45,11 +51,99 @@ builder.Services.AddCors(options =>
builder.Services.AddInfrastructure(builder.Configuration); builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddApplication(); builder.Services.AddApplication();
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
builder.Services.AddScoped<IMassDataRepository, MassDataRepository>();
builder.Services.AddScoped<ILayoutRepository, LayoutRepository>();
builder.Services.AddScoped<ITimeRepository, TimeRepository>();
builder.Services.AddDevExpressControls(); builder.Services.AddDevExpressControls();
builder.Services.AddSignalR(); builder.Services.AddSignalR();
builder.Services.AddSingleton<IDashboardChangeNotifier, DashboardChangeNotifier>(); builder.Services.AddSingleton<IDashboardChangeNotifier, DashboardChangeNotifier>();
builder.Services.AddScoped<DashboardConfigurator>(sp => builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvider) => {
DashboardConfiguratorFactory.Create(sp, builder.Configuration, builder.Environment)); var dashboardsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "Dashboards");
Directory.CreateDirectory(dashboardsPath);
var defaultDashboardPath = Path.Combine(dashboardsPath, "DefaultDashboard.xml");
if (!File.Exists(defaultDashboardPath))
{
var defaultDashboard = new Dashboard();
defaultDashboard.Title.Text = "Default Dashboard";
defaultDashboard.SaveToXml(defaultDashboardPath);
}
var dashboardBaseUrl = builder.Configuration["Dashboard:BaseUrl"]
?? builder.Configuration["ApiBaseUrl"]
?? builder.Configuration["ASPNETCORE_URLS"]?.Split(';', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()
?? "https://localhost:7204";
dashboardBaseUrl = dashboardBaseUrl.TrimEnd('/');
var catalogsGridDashboardPath = Path.Combine(dashboardsPath, "CatalogsGrid.xml");
if (!File.Exists(catalogsGridDashboardPath))
{
var dashboard = new Dashboard();
dashboard.Title.Text = "Catalogs (Dashboard Grid)";
var catalogDataSource = new DashboardJsonDataSource("Catalogs (API)")
{
ComponentName = "catalogsDataSource",
JsonSource = new UriJsonSource(new Uri($"{dashboardBaseUrl}/api/catalogs"))
};
dashboard.DataSources.Add(catalogDataSource);
var grid = new GridDashboardItem
{
DataSource = catalogDataSource,
Name = "Catalogs"
};
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.Guid))) { Name = "Id" });
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.CatTitle))) { Name = "Titel" });
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.CatString))) { Name = "String" });
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.AddedWho))) { Name = "Angelegt von" });
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.AddedWhen))) { Name = "Angelegt am" });
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.ChangedWho))) { Name = "Geändert von" });
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.ChangedWhen))) { Name = "Geändert am" });
dashboard.Items.Add(grid);
var layoutGroup = new DashboardLayoutGroup { Orientation = DashboardLayoutGroupOrientation.Vertical };
layoutGroup.ChildNodes.Add(new DashboardLayoutItem(grid));
dashboard.LayoutRoot = layoutGroup;
dashboard.SaveToXml(catalogsGridDashboardPath);
}
DashboardConfigurator configurator = new DashboardConfigurator();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? string.Empty;
var notifier = serviceProvider.GetRequiredService<IDashboardChangeNotifier>();
var dashboardStorage = new SqlDashboardStorage(connectionString, "TBDD_SMF_CONFIG", notifier: notifier);
configurator.SetDashboardStorage(dashboardStorage);
DataSourceInMemoryStorage dataSourceStorage = new DataSourceInMemoryStorage();
DashboardJsonDataSource jsonDataSourceUrl = new DashboardJsonDataSource("JSON Data Source (URL)");
jsonDataSourceUrl.JsonSource = new UriJsonSource(
new Uri("https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json"));
jsonDataSourceUrl.RootElement = "Customers";
dataSourceStorage.RegisterDataSource("jsonDataSourceUrl", jsonDataSourceUrl.SaveToXml());
var catalogsJsonDataSource = new DashboardJsonDataSource("Catalogs (API)")
{
ComponentName = "catalogsDataSource",
JsonSource = new UriJsonSource(new Uri($"{dashboardBaseUrl}/api/catalogs"))
};
dataSourceStorage.RegisterDataSource(catalogsJsonDataSource.ComponentName, catalogsJsonDataSource.SaveToXml());
dataSourceStorage.RegisterDataSource(catalogsJsonDataSource.Name, catalogsJsonDataSource.SaveToXml());
configurator.SetDataSourceStorage(dataSourceStorage);
EnsureDashboardInStorage(dashboardStorage, "DefaultDashboard", defaultDashboardPath);
EnsureDashboardInStorage(dashboardStorage, "CatalogsGrid", catalogsGridDashboardPath);
return configurator;
});
var app = builder.Build(); var app = builder.Build();
@@ -72,3 +166,15 @@ app.MapHub<DashboardsHub>("/hubs/dashboards");
app.MapControllers(); app.MapControllers();
app.Run(); app.Run();
static void EnsureDashboardInStorage(IEditableDashboardStorage storage, string id, string filePath)
{
var exists = storage.GetAvailableDashboardsInfo().Any(info => string.Equals(info.ID, id, StringComparison.OrdinalIgnoreCase));
if (exists || !File.Exists(filePath))
{
return;
}
var doc = XDocument.Load(filePath);
storage.AddDashboard(doc, id);
}

View File

@@ -1,5 +1,4 @@
using AutoMapper; using AutoMapper;
using DbFirst.Contracts.Catalogs;
using DbFirst.Domain.Entities; using DbFirst.Domain.Entities;
namespace DbFirst.Application.Catalogs; namespace DbFirst.Application.Catalogs;

View File

@@ -0,0 +1,12 @@
namespace DbFirst.Application.Catalogs;
public class CatalogReadDto
{
public int Guid { get; set; }
public string CatTitle { get; set; } = null!;
public string CatString { get; set; } = null!;
public string AddedWho { get; set; } = null!;
public DateTime AddedWhen { get; set; }
public string? ChangedWho { get; set; }
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -0,0 +1,10 @@
using DbFirst.Domain;
namespace DbFirst.Application.Catalogs;
public class CatalogWriteDto
{
public string CatTitle { get; set; } = null!;
public string CatString { get; set; } = null!;
public CatalogUpdateProcedure UpdateProcedure { get; set; } = CatalogUpdateProcedure.Update;
}

View File

@@ -1,4 +1,3 @@
using DbFirst.Contracts.Catalogs;
using MediatR; using MediatR;
namespace DbFirst.Application.Catalogs.Commands; namespace DbFirst.Application.Catalogs.Commands;

View File

@@ -1,6 +1,5 @@
using AutoMapper; using AutoMapper;
using DbFirst.Application.Repositories; using DbFirst.Application.Repositories;
using DbFirst.Contracts.Catalogs;
using DbFirst.Domain.Entities; using DbFirst.Domain.Entities;
using MediatR; using MediatR;

View File

@@ -1,4 +1,3 @@
using DbFirst.Contracts.Catalogs;
using MediatR; using MediatR;
namespace DbFirst.Application.Catalogs.Commands; namespace DbFirst.Application.Catalogs.Commands;

View File

@@ -1,8 +1,7 @@
using AutoMapper; using AutoMapper;
using DbFirst.Application.Repositories; using DbFirst.Application.Repositories;
using DbFirst.Contracts.Catalogs;
using DbFirst.Domain;
using DbFirst.Domain.Entities; using DbFirst.Domain.Entities;
using DbFirst.Domain;
using MediatR; using MediatR;
namespace DbFirst.Application.Catalogs.Commands; namespace DbFirst.Application.Catalogs.Commands;
@@ -26,20 +25,18 @@ public class UpdateCatalogHandler : IRequestHandler<UpdateCatalogCommand, Catalo
return null; return null;
} }
if (request.Dto.UpdateProcedure == CatalogUpdateProcedure.Update &&
!string.Equals(existing.CatTitle, request.Dto.CatTitle, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Titel kann nicht geändert werden.");
}
var entity = _mapper.Map<VwmyCatalog>(request.Dto); var entity = _mapper.Map<VwmyCatalog>(request.Dto);
entity.Guid = request.Id; entity.Guid = request.Id;
entity.CatTitle = request.Dto.UpdateProcedure == CatalogUpdateProcedure.Update
? existing.CatTitle
: request.Dto.CatTitle;
entity.AddedWho = existing.AddedWho; entity.AddedWho = existing.AddedWho;
entity.AddedWhen = existing.AddedWhen; entity.AddedWhen = existing.AddedWhen;
entity.ChangedWho = "system"; entity.ChangedWho = "system";
entity.ChangedWhen = DateTime.UtcNow; entity.ChangedWhen = DateTime.UtcNow;
var updated = await _repository.UpdateAsync(request.Id, entity, request.Dto.UpdateProcedure, cancellationToken); var procedure = request.Dto.UpdateProcedure;
var updated = await _repository.UpdateAsync(request.Id, entity, procedure, cancellationToken);
return updated == null ? null : _mapper.Map<CatalogReadDto>(updated); return updated == null ? null : _mapper.Map<CatalogReadDto>(updated);
} }
} }

View File

@@ -1,6 +1,5 @@
using AutoMapper; using AutoMapper;
using DbFirst.Application.Repositories; using DbFirst.Application.Repositories;
using DbFirst.Contracts.Catalogs;
using MediatR; using MediatR;
namespace DbFirst.Application.Catalogs.Queries; namespace DbFirst.Application.Catalogs.Queries;

View File

@@ -1,4 +1,3 @@
using DbFirst.Contracts.Catalogs;
using MediatR; using MediatR;
namespace DbFirst.Application.Catalogs.Queries; namespace DbFirst.Application.Catalogs.Queries;

View File

@@ -1,6 +1,5 @@
using AutoMapper; using AutoMapper;
using DbFirst.Application.Repositories; using DbFirst.Application.Repositories;
using DbFirst.Contracts.Catalogs;
using MediatR; using MediatR;
namespace DbFirst.Application.Catalogs.Queries; namespace DbFirst.Application.Catalogs.Queries;

View File

@@ -1,4 +1,3 @@
using DbFirst.Contracts.Catalogs;
using MediatR; using MediatR;
namespace DbFirst.Application.Catalogs.Queries; namespace DbFirst.Application.Catalogs.Queries;

View File

@@ -7,18 +7,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="16.1.1" /> <PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="MediatR" Version="14.1.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\DbFirst.Contracts\DbFirst.Contracts.csproj" />
<ProjectReference Include="..\DbFirst.Domain\DbFirst.Domain.csproj" /> <ProjectReference Include="..\DbFirst.Domain\DbFirst.Domain.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Layouts\" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using MediatR;
namespace DbFirst.Application; namespace DbFirst.Application;
@@ -6,9 +7,8 @@ public static class DependencyInjection
{ {
public static IServiceCollection AddApplication(this IServiceCollection services) public static IServiceCollection AddApplication(this IServiceCollection services)
{ {
services.AddAutoMapper(cfg => cfg.AddMaps(typeof(DependencyInjection).Assembly)); services.AddAutoMapper(typeof(DependencyInjection).Assembly);
services.AddMediatR(cfg => services.AddMediatR(typeof(DependencyInjection).Assembly);
cfg.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
return services; return services;
} }
} }

View File

@@ -1,4 +1,3 @@
using DbFirst.Contracts.MassData;
using MediatR; using MediatR;
namespace DbFirst.Application.MassData.Commands; namespace DbFirst.Application.MassData.Commands;

View File

@@ -1,6 +1,5 @@
using AutoMapper; using AutoMapper;
using DbFirst.Application.Repositories; using DbFirst.Application.Repositories;
using DbFirst.Contracts.MassData;
using MediatR; using MediatR;
namespace DbFirst.Application.MassData.Commands; namespace DbFirst.Application.MassData.Commands;

View File

@@ -1,5 +1,4 @@
using AutoMapper; using AutoMapper;
using DbFirst.Contracts.MassData;
using DbFirst.Domain.Entities; using DbFirst.Domain.Entities;
namespace DbFirst.Application.MassData; namespace DbFirst.Application.MassData;

View File

@@ -1,4 +1,4 @@
namespace DbFirst.Contracts.MassData; namespace DbFirst.Application.MassData;
public class MassDataReadDto public class MassDataReadDto
{ {

View File

@@ -1,4 +1,4 @@
namespace DbFirst.Contracts.MassData; namespace DbFirst.Application.MassData;
public class MassDataWriteDto public class MassDataWriteDto
{ {

View File

@@ -1,6 +1,5 @@
using AutoMapper; using AutoMapper;
using DbFirst.Application.Repositories; using DbFirst.Application.Repositories;
using DbFirst.Contracts.MassData;
using MediatR; using MediatR;
namespace DbFirst.Application.MassData.Queries; namespace DbFirst.Application.MassData.Queries;

View File

@@ -1,4 +1,3 @@
using DbFirst.Contracts.MassData;
using MediatR; using MediatR;
namespace DbFirst.Application.MassData.Queries; namespace DbFirst.Application.MassData.Queries;

View File

@@ -1,6 +1,5 @@
using AutoMapper; using AutoMapper;
using DbFirst.Application.Repositories; using DbFirst.Application.Repositories;
using DbFirst.Contracts.MassData;
using MediatR; using MediatR;
namespace DbFirst.Application.MassData.Queries; namespace DbFirst.Application.MassData.Queries;

View File

@@ -1,4 +1,3 @@
using DbFirst.Contracts.MassData;
using MediatR; using MediatR;
namespace DbFirst.Application.MassData.Queries; namespace DbFirst.Application.MassData.Queries;

View File

@@ -0,0 +1,9 @@
using DbFirst.Domain.Entities;
namespace DbFirst.Application.Repositories;
public interface ITimeRepository
{
Task InsertAsync(CancellationToken cancellationToken = default);
Task<TimeRecord?> GetLastAsync(CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,6 @@
using DbFirst.Domain.Entities;
using MediatR;
namespace DbFirst.Application.Time.Commands;
public record InsertTimeCommand : IRequest<TimeRecord?>;

View File

@@ -0,0 +1,21 @@
using DbFirst.Application.Repositories;
using DbFirst.Domain.Entities;
using MediatR;
namespace DbFirst.Application.Time.Commands;
public class InsertTimeHandler : IRequestHandler<InsertTimeCommand, TimeRecord?>
{
private readonly ITimeRepository _repository;
public InsertTimeHandler(ITimeRepository repository)
{
_repository = repository;
}
public async Task<TimeRecord?> Handle(InsertTimeCommand request, CancellationToken cancellationToken)
{
await _repository.InsertAsync(cancellationToken);
return await _repository.GetLastAsync(cancellationToken);
}
}

View File

@@ -1,6 +0,0 @@
namespace DbFirst.BlazorWebApp;
public class AppSettings
{
public string ApiBaseUrl { get; set; } = string.Empty;
}

View File

@@ -5,7 +5,10 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" /> <base href="/" />
@DxResourceManager.RegisterTheme(Themes.Fluent) @DxResourceManager.RegisterTheme(Themes.Fluent.Clone(properties =>
{
properties.ApplyToPageElements = true;
}))
@DxResourceManager.RegisterScripts() @DxResourceManager.RegisterScripts()
<link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" />
@@ -18,20 +21,11 @@
<link href="_content/DevExpress.Blazor.Dashboard/dx-dashboard.light.min.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Dashboard/dx-dashboard.light.min.css" rel="stylesheet" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" /> <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
<link rel="stylesheet" href="app.css" /> <link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="DbFirst.BlazorWebApp.styles.css" /> <link rel="stylesheet" href="DbFirst.BlazorWebApp.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<script src="js/size-manager.js"></script> <script src="js/size-manager.js"></script>
<script>
window.setDxDarkOverride = function (enabled) {
if (enabled)
document.documentElement.classList.add('dx-dark');
else
document.documentElement.classList.remove('dx-dark');
};
</script>
<HeadOutlet /> <HeadOutlet />
</head> </head>

View File

@@ -1,55 +0,0 @@
<div class="band-editor">
<button class="band-editor-toggle" @onclick="() => IsExpanded = !IsExpanded">
<span class="band-editor-toggle-icon @(IsExpanded ? "expanded" : "")">&#9658;</span>
<span>Layout</span>
</button>
@if (IsExpanded)
{
<div class="band-editor-body">
<div class="band-controls">
<DxButton Text="Band hinzufügen" Click="OnAddBand" />
<DxButton Text="Layout speichern" Click="OnSaveLayout" Enabled="@CanSave" />
<DxButton Text="Layout zurücksetzen" Click="OnResetLayout" />
</div>
@foreach (var band in Bands)
{
<div class="band-row">
<DxTextBox Text="@band.Caption" TextChanged="@(value => OnBandCaptionChanged.InvokeAsync((band, value)))" />
<DxButton Text="Entfernen" Click="@(() => OnRemoveBand.InvokeAsync(band))" />
</div>
}
<DxFormLayout CssClass="band-columns" ColCount="2">
@foreach (var column in Columns)
{
<DxFormLayoutItem Caption="@column.Caption">
<DxComboBox Data="@BandOptions"
TData="BandOption"
TValue="string"
TextFieldName="Caption"
ValueFieldName="Id"
Value="@GetColumnBand(column.FieldName)"
ValueChanged="@(value => OnColumnBandChanged.InvokeAsync((column.FieldName, value)))"
Width="100%" />
</DxFormLayoutItem>
}
</DxFormLayout>
</div>
}
</div>
@code {
private bool IsExpanded { get; set; }
[Parameter, EditorRequired] public List<BandDefinition> Bands { get; set; } = new();
[Parameter, EditorRequired] public List<BandOption> BandOptions { get; set; } = new();
[Parameter, EditorRequired] public List<ColumnDefinition> Columns { get; set; } = new();
[Parameter, EditorRequired] public Func<string, string> GetColumnBand { get; set; } = _ => string.Empty;
[Parameter, EditorRequired] public bool CanSave { get; set; }
[Parameter] public EventCallback OnAddBand { get; set; }
[Parameter] public EventCallback OnSaveLayout { get; set; }
[Parameter] public EventCallback OnResetLayout { get; set; }
[Parameter] public EventCallback<BandDefinition> OnRemoveBand { get; set; }
[Parameter] public EventCallback<(BandDefinition Band, string Value)> OnBandCaptionChanged { get; set; }
[Parameter] public EventCallback<(string FieldName, string? BandId)> OnColumnBandChanged { get; set; }
}

View File

@@ -1,278 +0,0 @@
using DbFirst.BlazorWebApp.Models.Grid;
using DbFirst.BlazorWebApp.Services;
using DevExpress.Blazor;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Rendering;
namespace DbFirst.BlazorWebApp.Components;
public abstract class BandGridBase<TItem> : ComponentBase
{
[Inject] protected BandLayoutService BandLayoutService { get; set; } = default!;
// --- Abstract: jedes Grid definiert diese selbst ---
protected abstract string LayoutKey { get; }
protected abstract List<ColumnDefinition> ColumnDefinitions { get; }
// --- Band-Layout Felder ---
protected BandLayout bandLayout = new();
protected Dictionary<string, string> columnBandAssignments = new();
protected List<BandOption> bandOptions = new();
protected Dictionary<string, ColumnDefinition> columnLookup = new();
protected string? layoutUser;
protected bool gridLayoutApplied;
protected IGrid? gridRef;
// --- SizeMode ---
protected SizeMode _sizeMode = SizeMode.Medium;
protected static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
protected string? errorMessage;
protected string? infoMessage;
protected bool isLoading;
protected bool hasLoaded;
protected EditContext? editContext;
protected ValidationMessageStore? validationMessageStore;
protected string popupHeaderText = "Edit";
protected int _focusedVisibleIndex;
private const string LayoutType = "GRID_BANDS";
// --- Lifecycle ---
protected async Task InitializeBandLayoutAsync()
{
columnLookup = ColumnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
ApplyColumnLayoutFromStorage();
_sizeMode = bandLayout.SizeMode;
UpdateBandOptions();
}
protected async Task ApplyGridLayoutAfterRenderAsync()
{
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
{
gridRef.LoadLayout(bandLayout.GridLayout);
gridLayoutApplied = true;
await InvokeAsync(StateHasChanged);
}
}
// --- Layout speichern / zurücksetzen ---
protected async Task SaveLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser)) return;
CaptureColumnLayoutFromGrid();
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
}
protected async Task ResetLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser)) return;
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
bandLayout = new BandLayout();
columnBandAssignments.Clear();
UpdateBandOptions();
foreach (var column in ColumnDefinitions)
column.Width = null;
columnLookup = ColumnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
_sizeMode = SizeMode.Medium;
gridRef?.LoadLayout(new GridPersistentLayout());
gridLayoutApplied = false;
}
private void CaptureColumnLayoutFromGrid()
{
if (gridRef == null) return;
var layout = gridRef.SaveLayout();
bandLayout.GridLayout = layout;
bandLayout.SizeMode = _sizeMode;
var orderedColumns = layout.Columns
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
.OrderBy(c => c.VisibleIndex)
.ToList();
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
bandLayout.ColumnWidths = orderedColumns
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
}
private void ApplyColumnLayoutFromStorage()
{
foreach (var column in ColumnDefinitions)
{
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
column.Width = width;
}
columnLookup = ColumnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
}
// --- Band-Methoden ---
protected bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
protected virtual bool ShowCommandColumn => true;
protected void AddBand()
{
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
UpdateBandOptions();
}
protected void RemoveBand(BandDefinition band)
{
bandLayout.Bands.Remove(band);
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
columnBandAssignments.Remove(key);
UpdateBandOptions();
SyncBandsFromAssignments();
}
protected void UpdateBandCaption(BandDefinition band, string value)
{
band.Caption = value;
UpdateBandOptions();
}
protected void UpdateColumnBand(string fieldName, string? bandId)
{
if (string.IsNullOrWhiteSpace(bandId))
columnBandAssignments.Remove(fieldName);
else
columnBandAssignments[fieldName] = bandId;
SyncBandsFromAssignments();
}
protected string GetColumnBand(string fieldName)
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
protected void SyncBandsFromAssignments()
{
foreach (var band in bandLayout.Bands)
{
band.Columns = ColumnDefinitions
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
.Select(c => c.FieldName)
.ToList();
}
_ = InvokeAsync(StateHasChanged);
}
protected void UpdateBandOptions()
{
bandOptions = [new() { Id = string.Empty, Caption = "Ohne Band" }];
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
}
// --- SizeMode ---
protected string FormatSizeText(SizeMode size) => size switch
{
SizeMode.Small => "Klein",
SizeMode.Medium => "Mittel",
SizeMode.Large => "Groß",
_ => size.ToString()
};
protected void OnSizeChange(DropDownButtonItemClickEventArgs args)
{
_sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
}
// --- RenderColumns / BuildDataColumn ---
protected RenderFragment RenderColumns() => builder =>
{
var seq = 0;
if (ShowCommandColumn)
{
builder.OpenComponent<DxGridCommandColumn>(seq++);
builder.AddAttribute(seq++, "Width", "120px");
builder.CloseComponent();
}
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var column in ColumnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
BuildDataColumn(builder, ref seq, column);
foreach (var band in bandLayout.Bands)
{
if (band.Columns.Count == 0) continue;
builder.OpenComponent<DxGridBandColumn>(seq++);
builder.AddAttribute(seq++, "Caption", band.Caption);
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
{
var bandSeq = 0;
foreach (var columnName in band.Columns)
{
if (columnLookup.TryGetValue(columnName, out var column))
BuildDataColumn(bandBuilder, ref bandSeq, column);
}
}));
builder.CloseComponent();
}
};
protected void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
{
builder.OpenComponent<DxGridDataColumn>(seq++);
builder.AddAttribute(seq++, "FieldName", column.FieldName);
builder.AddAttribute(seq++, "Caption", column.Caption);
if (!string.IsNullOrWhiteSpace(column.Width))
builder.AddAttribute(seq++, "Width", column.Width);
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
if (column.ReadOnly)
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
}
protected void SetEditContext(EditContext context)
{
if (editContext == context) return;
if (editContext != null)
editContext.OnFieldChanged -= OnEditFieldChanged;
editContext = context;
validationMessageStore = new ValidationMessageStore(editContext);
editContext.OnFieldChanged += OnEditFieldChanged;
}
protected virtual void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
{
validationMessageStore?.Clear();
editContext?.NotifyValidationStateChanged();
}
protected void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
protected async Task EditFocusedRow()
=> await gridRef!.StartEditRowAsync(_focusedVisibleIndex);
protected Task DeleteFocusedRow()
{
gridRef!.ShowRowDeleteConfirmation(_focusedVisibleIndex);
return Task.CompletedTask;
}
protected async Task SaveLayoutWithFeedbackAsync()
{
try
{
await SaveLayoutAsync();
infoMessage = "Layout gespeichert.";
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
}
}
protected async Task ResetLayoutWithFeedbackAsync()
{
await ResetLayoutAsync();
infoMessage = "Layout zurückgesetzt.";
errorMessage = null;
}
}

View File

@@ -1,5 +1,5 @@
@inherits BandGridBase<CatalogReadDto> @inject CatalogApiClient Api
@inject ICatalogApiClient Api @inject BandLayoutService BandLayoutService
@if (!string.IsNullOrWhiteSpace(errorMessage)) @if (!string.IsNullOrWhiteSpace(errorMessage))
{ {
@@ -24,21 +24,47 @@ else if (items.Count == 0)
} }
else else
{ {
<BandEditor Bands="@bandLayout.Bands" <div class="band-editor">
BandOptions="@bandOptions" <button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded">
Columns="@ColumnDefinitions" <span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">&#9658;</span>
GetColumnBand="GetColumnBand" <span>Layout</span>
CanSave="@CanSaveBandLayout" </button>
OnAddBand="AddBand" @if (bandEditorExpanded)
OnSaveLayout="SaveLayoutWithFeedbackAsync" {
OnResetLayout="ResetLayoutWithFeedbackAsync" <div class="band-editor-body">
OnRemoveBand="RemoveBand" <div class="band-controls">
OnBandCaptionChanged="@(args => UpdateBandCaption(args.Band, args.Value))" <DxButton Text="Band hinzufügen" Click="AddBand" />
OnColumnBandChanged="@(args => UpdateColumnBand(args.FieldName, args.BandId))" /> <DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
</div>
@foreach (var band in bandLayout.Bands)
{
<div class="band-row">
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
</div>
}
<DxFormLayout CssClass="band-columns" ColCount="2">
@foreach (var column in columnDefinitions)
{
<DxFormLayoutItem Caption="@column.Caption">
<DxComboBox Data="@bandOptions"
TData="BandOption"
TValue="string"
TextFieldName="Caption"
ValueFieldName="Id"
Value="@GetColumnBand(column.FieldName)"
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
Width="100%" />
</DxFormLayoutItem>
}
</DxFormLayout>
</div>
}
</div>
<div class="grid-section"> <div class="grid-section">
<DxGrid Data="@items" <DxGrid Data="@items"
ColumnChooserButtonDisplayMode="GridColumnChooserButtonDisplayMode.Always"
TItem="CatalogReadDto" TItem="CatalogReadDto"
KeyFieldName="@nameof(CatalogReadDto.Guid)" KeyFieldName="@nameof(CatalogReadDto.Guid)"
SizeMode="@_sizeMode" SizeMode="@_sizeMode"
@@ -59,42 +85,9 @@ else
DataItemDeleting="OnDataItemDeleting" DataItemDeleting="OnDataItemDeleting"
FocusedRowEnabled="true" FocusedRowEnabled="true"
@bind-FocusedRowKey="focusedRowKey" @bind-FocusedRowKey="focusedRowKey"
RowClick="@(args => _focusedVisibleIndex = args.VisibleIndex)"
@ref="gridRef"> @ref="gridRef">
<ToolbarTemplate> <ToolbarTemplate>
<DxToolbar> <DxToolbar>
<DxToolbarItem Alignment="ToolbarItemAlignment.Left">
<Template Context="_">
<DxButton IconCssClass="bi bi-plus-lg"
RenderStyle="ButtonRenderStyle.Secondary"
RenderStyleMode="ButtonRenderStyleMode.Text"
Click="@(() => gridRef!.StartEditNewRowAsync())" />
</Template>
</DxToolbarItem>
<DxToolbarItem Alignment="ToolbarItemAlignment.Left">
<Template Context="_">
<DxButton IconCssClass="bi bi-pencil"
RenderStyle="ButtonRenderStyle.Secondary"
RenderStyleMode="ButtonRenderStyleMode.Text"
Click="EditFocusedRow" />
</Template>
</DxToolbarItem>
<DxToolbarItem Alignment="ToolbarItemAlignment.Left">
<Template Context="_">
<DxButton IconCssClass="bi bi-trash"
RenderStyle="ButtonRenderStyle.Secondary"
RenderStyleMode="ButtonRenderStyleMode.Text"
Click="DeleteFocusedRow" />
</Template>
</DxToolbarItem>
<DxToolbarItem Alignment="ToolbarItemAlignment.Right">
<Template Context="_">
<DxButton Text="Spalten"
RenderStyle="ButtonRenderStyle.Secondary"
RenderStyleMode="ButtonRenderStyleMode.Text"
Click="@(() => gridRef!.ShowColumnChooser())" />
</Template>
</DxToolbarItem>
<DxToolbarItem Alignment="ToolbarItemAlignment.Right"> <DxToolbarItem Alignment="ToolbarItemAlignment.Right">
<Template Context="_"> <Template Context="_">
<DxDropDownButton Text="@FormatSizeText(_sizeMode)" <DxDropDownButton Text="@FormatSizeText(_sizeMode)"
@@ -149,12 +142,26 @@ else
@code { @code {
private List<CatalogReadDto> items = new(); private List<CatalogReadDto> items = new();
private bool isLoading;
private bool hasLoaded;
private string? errorMessage;
private string? infoMessage;
private EditContext? editContext;
private ValidationMessageStore? validationMessageStore;
private IGrid? gridRef;
private int? focusedRowKey; private int? focusedRowKey;
private string popupHeaderText = "Edit";
private const string LayoutType = "GRID_BANDS";
private const string LayoutKey = "CatalogsGrid";
private string? layoutUser;
private BandLayout bandLayout = new();
private Dictionary<string, string> columnBandAssignments = new();
private List<BandOption> bandOptions = new();
private Dictionary<string, ColumnDefinition> columnLookup = new();
private bool gridLayoutApplied;
private bool bandEditorExpanded;
protected override string LayoutKey => "CatalogsGrid"; private List<ColumnDefinition> columnDefinitions = new()
protected override bool ShowCommandColumn => false;
protected override List<ColumnDefinition> ColumnDefinitions { get; } = new()
{ {
new() { FieldName = nameof(CatalogReadDto.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text }, new() { FieldName = nameof(CatalogReadDto.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.CatTitle), Caption = "Titel", FilterType = ColumnFilterType.Text }, new() { FieldName = nameof(CatalogReadDto.CatTitle), Caption = "Titel", FilterType = ColumnFilterType.Text },
@@ -171,15 +178,44 @@ else
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" } new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
}; };
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
private SizeMode _sizeMode = SizeMode.Medium;
private static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
private string FormatSizeText(SizeMode size) => size switch
{
SizeMode.Small => "Klein",
SizeMode.Medium => "Mittel",
SizeMode.Large => "Groß",
_ => size.ToString()
};
private void OnSizeChange(DropDownButtonItemClickEventArgs args)
{
_sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
}
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await InitializeBandLayoutAsync(); columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
ApplyColumnLayoutFromStorage();
_sizeMode = bandLayout.SizeMode;
UpdateBandOptions();
await LoadCatalogs(); await LoadCatalogs();
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
await ApplyGridLayoutAfterRenderAsync(); if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
{
gridRef.LoadLayout(bandLayout.GridLayout);
gridLayoutApplied = true;
await InvokeAsync(StateHasChanged);
}
} }
private async Task LoadCatalogs() private async Task LoadCatalogs()
@@ -202,7 +238,185 @@ else
} }
} }
protected override void OnEditFieldChanged(object? sender, FieldChangedEventArgs e) private async Task SaveLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
return;
try
{
CaptureColumnLayoutFromGrid();
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
infoMessage = "Layout gespeichert.";
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
}
}
private async Task ResetLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
return;
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
bandLayout = new BandLayout();
columnBandAssignments.Clear();
UpdateBandOptions();
foreach (var column in columnDefinitions)
column.Width = null;
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
_sizeMode = SizeMode.Medium;
if (gridRef != null)
gridRef.LoadLayout(new GridPersistentLayout());
gridLayoutApplied = false;
infoMessage = "Layout zurückgesetzt.";
errorMessage = null;
}
private void CaptureColumnLayoutFromGrid()
{
if (gridRef == null)
return;
var layout = gridRef.SaveLayout();
bandLayout.GridLayout = layout;
bandLayout.SizeMode = _sizeMode;
var orderedColumns = layout.Columns
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
.OrderBy(c => c.VisibleIndex)
.ToList();
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
bandLayout.ColumnWidths = orderedColumns
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
}
private void ApplyColumnLayoutFromStorage()
{
foreach (var column in columnDefinitions)
{
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
column.Width = width;
}
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
}
private void AddBand()
{
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
UpdateBandOptions();
}
private void RemoveBand(BandDefinition band)
{
bandLayout.Bands.Remove(band);
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
columnBandAssignments.Remove(key);
UpdateBandOptions();
SyncBandsFromAssignments();
}
private void UpdateBandCaption(BandDefinition band, string value)
{
band.Caption = value;
UpdateBandOptions();
}
private void UpdateColumnBand(string fieldName, string? bandId)
{
if (string.IsNullOrWhiteSpace(bandId))
columnBandAssignments.Remove(fieldName);
else
columnBandAssignments[fieldName] = bandId;
SyncBandsFromAssignments();
}
private string GetColumnBand(string fieldName)
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
private void SyncBandsFromAssignments()
{
foreach (var band in bandLayout.Bands)
{
band.Columns = columnDefinitions
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
.Select(c => c.FieldName)
.ToList();
}
StateHasChanged();
}
private void UpdateBandOptions()
{
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
}
private RenderFragment RenderColumns() => builder =>
{
var seq = 0;
builder.OpenComponent<DxGridCommandColumn>(seq++);
builder.AddAttribute(seq++, "Width", "120px");
builder.CloseComponent();
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
BuildDataColumn(builder, ref seq, column);
foreach (var band in bandLayout.Bands)
{
if (band.Columns.Count == 0) continue;
builder.OpenComponent<DxGridBandColumn>(seq++);
builder.AddAttribute(seq++, "Caption", band.Caption);
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
{
var bandSeq = 0;
foreach (var columnName in band.Columns)
{
if (columnLookup.TryGetValue(columnName, out var column))
BuildDataColumn(bandBuilder, ref bandSeq, column);
}
}));
builder.CloseComponent();
}
};
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
{
builder.OpenComponent<DxGridDataColumn>(seq++);
builder.AddAttribute(seq++, "FieldName", column.FieldName);
builder.AddAttribute(seq++, "Caption", column.Caption);
if (!string.IsNullOrWhiteSpace(column.Width))
builder.AddAttribute(seq++, "Width", column.Width);
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
if (column.ReadOnly)
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
}
private void SetEditContext(EditContext context)
{
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 (validationMessageStore == null || editContext == null) return;
@@ -220,6 +434,8 @@ else
} }
} }
private void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e) private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{ {
popupHeaderText = e.IsNew ? "Neu" : "Edit"; popupHeaderText = e.IsNew ? "Neu" : "Edit";
@@ -259,7 +475,7 @@ else
{ {
CatTitle = editModel.CatTitle, CatTitle = editModel.CatTitle,
CatString = editModel.CatString, CatString = editModel.CatString,
UpdateProcedure = (CatalogUpdateProcedure)editModel.UpdateProcedure UpdateProcedure = editModel.UpdateProcedure
}; };
try try

View File

@@ -1,23 +1,15 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@implements IDisposable @implements IDisposable
@inject ThemeState ThemeState @inject ThemeState ThemeState
@inject IJSRuntime JS
<div class="page @(ThemeState.IsDarkMode ? "app-dark" : "app-light") @(ThemeState.IsNativeDarkTheme ? "native-dark" : "")"> <div class="page @(ThemeState.IsDarkMode ? "app-dark" : "app-light")">
<div class="sidebar"> <div class="sidebar">
<NavMenu /> <NavMenu />
</div> </div>
<main> <main>
<div class="top-row px-4"> <div class="top-row px-4">
<DxComboBox Data="@ThemeState.AvailableThemes" <DxButton Text="@(ThemeState.IsDarkMode ? "Dark Mode aus" : "Dark Mode an")" Click="ToggleTheme" />
Value="@ThemeState.CurrentThemeName"
ValueChanged="@((string t) => ThemeState.SetTheme(t))"
style="width: 130px;" />
<span style="margin-left: 12px;">
<DxButton Text="@(ThemeState.IsDarkMode ? "Dark Mode aus" : "Dark Mode an")"
Click="ToggleTheme" />
</span>
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a> <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div> </div>
@@ -34,43 +26,9 @@
</div> </div>
@code { @code {
private bool _isInteractive;
protected override void OnInitialized() protected override void OnInitialized()
{ {
ThemeState.OnChange += OnThemeChanged; ThemeState.OnChange += StateHasChanged;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_isInteractive = true;
}
await ApplyDxDarkOverrideAsync();
}
private async void OnThemeChanged()
{
StateHasChanged();
if (_isInteractive)
{
await ApplyDxDarkOverrideAsync();
}
}
private async Task ApplyDxDarkOverrideAsync()
{
if (!_isInteractive) return;
try
{
bool needsOverride = ThemeState.IsDarkMode && !ThemeState.IsNativeDarkTheme;
await JS.InvokeVoidAsync("setDxDarkOverride", needsOverride);
}
catch (JSException)
{
// JS-Funktion noch nicht verfügbar kein Circuit-Crash
}
} }
private void ToggleTheme() private void ToggleTheme()
@@ -80,6 +38,6 @@
public void Dispose() public void Dispose()
{ {
ThemeState.OnChange -= OnThemeChanged; ThemeState.OnChange -= StateHasChanged;
} }
} }

View File

@@ -18,13 +18,11 @@ main {
} }
.sidebar { .sidebar {
background-color: var(--dx-color-surface-container, #f4f4f4); background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
border-right: 1px solid var(--dx-color-outline-variant, #e0e0e0);
} }
.page.app-dark .sidebar { .page.app-dark .sidebar {
background-color: var(--dx-color-surface-container, #1e1e1e); background-image: linear-gradient(180deg, #171717 0%, #0f2a46 70%);
border-right-color: var(--dx-color-outline-variant, #333);
} }
.top-row { .top-row {

View File

@@ -1,24 +1,42 @@
<div class="nav-brand-row"> <div class="top-row ps-3 navbar navbar-dark">
<a class="nav-brand-link" href="">DbFirst</a> <div class="container-fluid">
<button class="nav-toggle-btn" @onclick="ToggleMenu" title="Navigation menu">&#9776;</button> <a class="navbar-brand" href="">DbFirst.BlazorWebApp</a>
</div>
</div> </div>
<div class="nav-scrollable @(menuOpen ? "nav-open" : "")"> <input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<DxTreeView CssClass="sidebar-tree">
<Nodes> <div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<DxTreeViewNode Text="Home" NavigateUrl="/" IconCssClass="dxi dxi-home" /> <nav class="flex-column">
<DxTreeViewNode Text="Data Management" Expanded="true"> <div class="nav-item px-3">
<Nodes> <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<DxTreeViewNode Text="Catalogs" NavigateUrl="/catalogs" IconCssClass="dxi dxi-folder" /> <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
<DxTreeViewNode Text="Dashboards" NavigateUrl="/dashboards" IconCssClass="dxi dxi-chart-bar" /> </NavLink>
<DxTreeViewNode Text="Mass Data" NavigateUrl="/massdata" IconCssClass="dxi dxi-table" /> </div>
</Nodes>
</DxTreeViewNode> <div class="nav-item px-3">
</Nodes> <NavLink class="nav-link" href="catalogs">
</DxTreeView> <span class="bi bi-collection-nav-menu" aria-hidden="true"></span> Catalogs
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="dashboards">
<span class="bi bi-speedometer-nav-menu" aria-hidden="true"></span> Dashboards
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="massdata">
<span class="bi bi-table-nav-menu" aria-hidden="true"></span> MassData
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="clock">
<span class="bi bi-clock-nav-menu" aria-hidden="true"></span> Clock
</NavLink>
</div>
</nav>
</div> </div>
@code {
private bool menuOpen = false;
private void ToggleMenu() => menuOpen = !menuOpen;
}

View File

@@ -1,52 +1,117 @@
.nav-brand-row { .navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem; height: 3.5rem;
display: flex; background-color: rgba(0,0,0,0.4);
align-items: center;
justify-content: space-between;
padding: 0 1rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
} }
.nav-brand-link { .navbar-brand {
font-size: 1.05rem; font-size: 1.1rem;
font-weight: 600;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.nav-toggle-btn { .bi {
display: block; display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.bi-collection-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M2 3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 .5.5v1H2V3z'/%3E%3Cpath d='M2 5h12v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5z'/%3E%3C/svg%3E");
}
.bi-speedometer-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M1 11a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v4zm5 0a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v8zm5 0a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v2z'/%3E%3C/svg%3E");
}
.bi-table-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M1 2a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2zm1 1v2h12V3H2zm12 3H2v2h12V6zm0 3H2v2h12V9zm0 3H2v1h12v-1z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none; background: none;
border: none; border: none;
font-size: 1.4rem; border-radius: 4px;
cursor: pointer; height: 3rem;
padding: 0.25rem 0.5rem; display: flex;
line-height: 1; align-items: center;
} line-height: 3rem;
.nav-scrollable {
display: none;
}
.nav-scrollable.nav-open {
display: block;
}
.sidebar-tree {
width: 100%; width: 100%;
} }
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) { @media (min-width: 641px) {
.nav-toggle-btn { .navbar-toggler {
display: none; display: none;
} }
.nav-scrollable { .nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block; display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem); height: calc(100vh - 3.5rem);
overflow-y: auto; overflow-y: auto;
} }
} }

View File

@@ -1,5 +1,5 @@
@inherits BandGridBase<MassDataReadDto> @inject MassDataApiClient Api
@inject IMassDataApiClient Api @inject BandLayoutService BandLayoutService
@if (!string.IsNullOrWhiteSpace(errorMessage)) @if (!string.IsNullOrWhiteSpace(errorMessage))
{ {
@@ -25,17 +25,44 @@ else if (items.Count == 0)
else else
{ {
<BandEditor Bands="@bandLayout.Bands" <div class="band-editor">
BandOptions="@bandOptions" <button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded">
Columns="@ColumnDefinitions" <span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">&#9658;</span>
GetColumnBand="GetColumnBand" <span>Layout</span>
CanSave="@CanSaveBandLayout" </button>
OnAddBand="AddBand" @if (bandEditorExpanded)
OnSaveLayout="SaveLayoutWithFeedbackAsync" {
OnResetLayout="ResetLayoutWithFeedbackAsync" <div class="band-editor-body">
OnRemoveBand="RemoveBand" <div class="band-controls">
OnBandCaptionChanged="@(args => UpdateBandCaption(args.Band, args.Value))" <DxButton Text="Band hinzufügen" Click="AddBand" />
OnColumnBandChanged="@(args => UpdateColumnBand(args.FieldName, args.BandId))" /> <DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
</div>
@foreach (var band in bandLayout.Bands)
{
<div class="band-row">
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
</div>
}
<DxFormLayout CssClass="band-columns" ColCount="2">
@foreach (var column in columnDefinitions)
{
<DxFormLayoutItem Caption="@column.Caption">
<DxComboBox Data="@bandOptions"
TData="BandOption"
TValue="string"
TextFieldName="Caption"
ValueFieldName="Id"
Value="@GetColumnBand(column.FieldName)"
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
Width="100%" />
</DxFormLayoutItem>
}
</DxFormLayout>
</div>
}
</div>
<div class="mb-3 page-size-selector"> <div class="mb-3 page-size-selector">
<span class="page-size-label">Datensätze je Seite:</span> <span class="page-size-label">Datensätze je Seite:</span>
@@ -51,7 +78,6 @@ else
<div class="grid-section"> <div class="grid-section">
<DxGrid Data="@items" <DxGrid Data="@items"
ColumnChooserButtonDisplayMode="GridColumnChooserButtonDisplayMode.Always"
TItem="MassDataReadDto" TItem="MassDataReadDto"
KeyFieldName="@nameof(MassDataReadDto.Id)" KeyFieldName="@nameof(MassDataReadDto.Id)"
SizeMode="@_sizeMode" SizeMode="@_sizeMode"
@@ -72,42 +98,9 @@ else
DataItemDeleting="OnDataItemDeleting" DataItemDeleting="OnDataItemDeleting"
FocusedRowEnabled="true" FocusedRowEnabled="true"
@bind-FocusedRowKey="focusedRowKey" @bind-FocusedRowKey="focusedRowKey"
RowClick="@(args => _focusedVisibleIndex = args.VisibleIndex)"
@ref="gridRef"> @ref="gridRef">
<ToolbarTemplate> <ToolbarTemplate>
<DxToolbar> <DxToolbar>
<DxToolbarItem Alignment="ToolbarItemAlignment.Left">
<Template Context="_">
<DxButton IconCssClass="bi bi-plus-lg"
RenderStyle="ButtonRenderStyle.Secondary"
RenderStyleMode="ButtonRenderStyleMode.Text"
Click="@(() => gridRef!.StartEditNewRowAsync())" />
</Template>
</DxToolbarItem>
<DxToolbarItem Alignment="ToolbarItemAlignment.Left">
<Template Context="_">
<DxButton IconCssClass="bi bi-pencil"
RenderStyle="ButtonRenderStyle.Secondary"
RenderStyleMode="ButtonRenderStyleMode.Text"
Click="EditFocusedRow" />
</Template>
</DxToolbarItem>
<DxToolbarItem Alignment="ToolbarItemAlignment.Left">
<Template Context="_">
<DxButton IconCssClass="bi bi-trash"
RenderStyle="ButtonRenderStyle.Secondary"
RenderStyleMode="ButtonRenderStyleMode.Text"
Click="DeleteFocusedRow" />
</Template>
</DxToolbarItem>
<DxToolbarItem Alignment="ToolbarItemAlignment.Right">
<Template Context="_">
<DxButton Text="Spalten"
RenderStyle="ButtonRenderStyle.Secondary"
RenderStyleMode="ButtonRenderStyleMode.Text"
Click="@(() => gridRef!.ShowColumnChooser())" />
</Template>
</DxToolbarItem>
<DxToolbarItem Alignment="ToolbarItemAlignment.Right"> <DxToolbarItem Alignment="ToolbarItemAlignment.Right">
<Template Context="_"> <Template Context="_">
<DxDropDownButton Text="@FormatSizeText(_sizeMode)" <DxDropDownButton Text="@FormatSizeText(_sizeMode)"
@@ -175,15 +168,29 @@ else
@code { @code {
private List<MassDataReadDto> items = new(); private List<MassDataReadDto> items = new();
private bool isLoading;
private bool hasLoaded;
private string? errorMessage;
private string? infoMessage;
private int pageIndex; private int pageIndex;
private int pageCount = 1; private int pageCount = 1;
private int? pageSize = 100; private int? pageSize = 100;
private string popupHeaderText = "Edit";
private EditContext? editContext;
private ValidationMessageStore? validationMessageStore;
private IGrid? gridRef;
private int? focusedRowKey; private int? focusedRowKey;
private const string LayoutType = "GRID_BANDS";
private const string LayoutKey = "MassDataGrid";
private string? layoutUser;
private BandLayout bandLayout = new();
private Dictionary<string, string> columnBandAssignments = new();
private List<BandOption> bandOptions = new();
private Dictionary<string, ColumnDefinition> columnLookup = new();
private bool gridLayoutApplied;
private bool bandEditorExpanded;
protected override string LayoutKey => "MassDataGrid"; private List<ColumnDefinition> columnDefinitions = new()
protected override bool ShowCommandColumn => false;
protected override List<ColumnDefinition> ColumnDefinitions { get; } = new()
{ {
new() { FieldName = nameof(MassDataReadDto.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text }, new() { FieldName = nameof(MassDataReadDto.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(MassDataReadDto.CustomerName), Caption = "CustomerName", FilterType = ColumnFilterType.Text }, new() { FieldName = nameof(MassDataReadDto.CustomerName), Caption = "CustomerName", FilterType = ColumnFilterType.Text },
@@ -208,15 +215,44 @@ else
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" } new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
}; };
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
private SizeMode _sizeMode = SizeMode.Medium;
private static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
private string FormatSizeText(SizeMode size) => size switch
{
SizeMode.Small => "Klein",
SizeMode.Medium => "Mittel",
SizeMode.Large => "Groß",
_ => size.ToString()
};
private void OnSizeChange(DropDownButtonItemClickEventArgs args)
{
_sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
}
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await InitializeBandLayoutAsync(); columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
ApplyColumnLayoutFromStorage();
_sizeMode = bandLayout.SizeMode;
UpdateBandOptions();
await LoadPage(0); await LoadPage(0);
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
await ApplyGridLayoutAfterRenderAsync(); if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
{
gridRef.LoadLayout(bandLayout.GridLayout);
gridLayoutApplied = true;
await InvokeAsync(StateHasChanged);
}
} }
private async Task LoadPage(int page) private async Task LoadPage(int page)
@@ -252,7 +288,171 @@ else
await LoadPage(0); await LoadPage(0);
} }
protected override void OnEditFieldChanged(object? sender, FieldChangedEventArgs e) private async Task SaveLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
return;
try
{
CaptureColumnLayoutFromGrid();
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
infoMessage = "Layout gespeichert.";
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
}
}
private async Task ResetLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
return;
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
bandLayout = new BandLayout();
columnBandAssignments.Clear();
UpdateBandOptions();
foreach (var column in columnDefinitions)
column.Width = null;
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
_sizeMode = SizeMode.Medium;
if (gridRef != null)
gridRef.LoadLayout(new GridPersistentLayout());
gridLayoutApplied = false;
infoMessage = "Layout zurückgesetzt.";
errorMessage = null;
}
private void CaptureColumnLayoutFromGrid()
{
if (gridRef == null) return;
var layout = gridRef.SaveLayout();
bandLayout.GridLayout = layout;
bandLayout.SizeMode = _sizeMode;
var orderedColumns = layout.Columns
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
.OrderBy(c => c.VisibleIndex)
.ToList();
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
bandLayout.ColumnWidths = orderedColumns
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
}
private void ApplyColumnLayoutFromStorage()
{
foreach (var column in columnDefinitions)
{
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
column.Width = width;
}
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
}
private void AddBand()
{
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
UpdateBandOptions();
}
private void RemoveBand(BandDefinition band)
{
bandLayout.Bands.Remove(band);
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
columnBandAssignments.Remove(key);
UpdateBandOptions();
SyncBandsFromAssignments();
}
private void UpdateBandCaption(BandDefinition band, string value)
{
band.Caption = value;
UpdateBandOptions();
}
private void UpdateColumnBand(string fieldName, string? bandId)
{
if (string.IsNullOrWhiteSpace(bandId))
columnBandAssignments.Remove(fieldName);
else
columnBandAssignments[fieldName] = bandId;
SyncBandsFromAssignments();
}
private string GetColumnBand(string fieldName)
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
private void SyncBandsFromAssignments()
{
foreach (var band in bandLayout.Bands)
{
band.Columns = columnDefinitions
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
.Select(c => c.FieldName)
.ToList();
}
StateHasChanged();
}
private void UpdateBandOptions()
{
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
}
private RenderFragment RenderColumns() => builder =>
{
var seq = 0;
builder.OpenComponent<DxGridCommandColumn>(seq++);
builder.AddAttribute(seq++, "Width", "120px");
builder.CloseComponent();
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
BuildDataColumn(builder, ref seq, column);
foreach (var band in bandLayout.Bands)
{
if (band.Columns.Count == 0) continue;
builder.OpenComponent<DxGridBandColumn>(seq++);
builder.AddAttribute(seq++, "Caption", band.Caption);
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
{
var bandSeq = 0;
foreach (var columnName in band.Columns)
{
if (columnLookup.TryGetValue(columnName, out var column))
BuildDataColumn(bandBuilder, ref bandSeq, column);
}
}));
builder.CloseComponent();
}
};
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
{
builder.OpenComponent<DxGridDataColumn>(seq++);
builder.AddAttribute(seq++, "FieldName", column.FieldName);
builder.AddAttribute(seq++, "Caption", column.Caption);
if (!string.IsNullOrWhiteSpace(column.Width))
builder.AddAttribute(seq++, "Width", column.Width);
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
if (column.ReadOnly)
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
}
private void SetEditContext(EditContext context)
{
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 (validationMessageStore == null || editContext == null) return;
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure)) if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure))
@@ -268,7 +468,9 @@ else
} }
} }
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e) private void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
private async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{ {
if (e.IsNew) if (e.IsNew)
{ {
@@ -321,18 +523,19 @@ else
Category = editModel.Category, Category = editModel.Category,
StatusFlag = editModel.StatusFlag StatusFlag = editModel.StatusFlag
}; };
try
var result = await Api.UpsertAsync(dto);
if (!result.Success)
{ {
errorMessage = result.Error ?? "Speichern fehlgeschlagen."; var saved = await Api.UpsertAsync(dto);
e.Cancel = true;
return;
}
infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert."; infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert.";
focusedRowKey = result.Value?.Id; focusedRowKey = saved.Id;
await LoadPage(pageIndex); await LoadPage(pageIndex);
} }
catch (Exception ex)
{
errorMessage = $"Fehler beim Speichern: {ex.Message}";
e.Cancel = true;
}
}
private void AddValidationError(MassDataEditModel editModel, string fieldName, string message) private void AddValidationError(MassDataEditModel editModel, string fieldName, string message)
{ {

View File

@@ -0,0 +1,100 @@
@rendermode InteractiveServer
@page "/clock"
@inject TimeApiClient TimeApi
@implements IAsyncDisposable
<PageTitle>Clock</PageTitle>
<h3>DB Server Clock</h3>
<div class="clock-wrapper">
<div class="clock-display @(_error != null ? "clock-error" : "")">
@if (_dbTime.HasValue)
{
<span class="clock-time">@_dbTime.Value.ToString("HH:mm:ss")</span>
<span class="clock-date">@_dbTime.Value.ToString("dd.MM.yyyy")</span>
}
else if (_error != null)
{
<span class="clock-time">--:--:--</span>
<span class="clock-date text-danger">@_error</span>
}
else
{
<span class="clock-time">...</span>
}
</div>
</div>
<style>
.clock-wrapper {
display: flex;
justify-content: center;
align-items: center;
height: 40vh;
}
.clock-display {
display: flex;
flex-direction: column;
align-items: center;
background: var(--bs-body-bg, #1e1e2e);
border: 2px solid var(--bs-border-color, #444);
border-radius: 1rem;
padding: 2rem 4rem;
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
}
.clock-time {
font-size: 5rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: 0.1em;
color: var(--bs-primary, #0d6efd);
}
.clock-date {
font-size: 1.4rem;
margin-top: 0.5rem;
opacity: 0.75;
}
.clock-error .clock-time {
color: var(--bs-danger, #dc3545);
}
</style>
@code {
private DateTime? _dbTime;
private string? _error;
private Timer? _timer;
protected override async Task OnInitializedAsync()
{
await TickAsync();
_timer = new Timer(async _ =>
{
await TickAsync();
await InvokeAsync(StateHasChanged);
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
}
private async Task TickAsync()
{
try
{
_dbTime = await TimeApi.InsertAndGetLastAsync();
_error = null;
}
catch (Exception ex)
{
_error = ex.Message;
}
}
public async ValueTask DisposeAsync()
{
if (_timer != null)
await _timer.DisposeAsync();
}
}

View File

@@ -1,9 +1,9 @@
@page "/dashboard" @page "/dashboard"
@page "/dashboards/{DashboardId?}" @page "/dashboards/{DashboardId?}"
@implements IAsyncDisposable @implements IAsyncDisposable
@inject IOptions<AppSettings> AppSettingsOptions @inject Microsoft.Extensions.Configuration.IConfiguration Configuration
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject IDashboardApiClient DashboardApi @inject DashboardApiClient DashboardApi
<PageTitle>Dashboards</PageTitle> <PageTitle>Dashboards</PageTitle>
@@ -45,17 +45,12 @@
private string SelectedDashboardId { get; set; } = string.Empty; private string SelectedDashboardId { get; set; } = string.Empty;
private string DashboardKey => $"{SelectedDashboardId}-{(IsDesigner ? "designer" : "viewer")}"; private string DashboardKey => $"{SelectedDashboardId}-{(IsDesigner ? "designer" : "viewer")}";
private string DashboardEndpoint => $"{AppSettingsOptions.Value.ApiBaseUrl.TrimEnd('/')}/api/dashboard"; private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
private string HubEndpoint => $"{AppSettingsOptions.Value.ApiBaseUrl.TrimEnd('/')}/hubs/dashboards"; private string HubEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/hubs/dashboards";
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await RefreshDashboards(); await RefreshDashboards();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
_hubConnection = new HubConnectionBuilder() _hubConnection = new HubConnectionBuilder()
.WithUrl(HubEndpoint) .WithUrl(HubEndpoint)
@@ -72,6 +67,11 @@
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
if (dashboards.Count == 0)
{
await RefreshDashboards();
}
var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase) var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase)
? null ? null
: DashboardId; : DashboardId;

View File

@@ -31,8 +31,8 @@
public class CarouselData public class CarouselData
{ {
public string Source { get; set; } = string.Empty; public string Source { get; set; }
public string AlternateText { get; set; } = string.Empty; public string AlternateText { get; set; }
public CarouselData(string source, string alt) public CarouselData(string source, string alt)
{ {

View File

@@ -1,5 +1,4 @@
@rendermode @(new InteractiveServerRenderMode(prerender: false)) @rendermode InteractiveServer
<Router AppAssembly="typeof(Program).Assembly"> <Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData"> <Found Context="routeData">

View File

@@ -1,27 +1,19 @@
@using System.Net.Http @using System.Net.Http
@using System.Net.Http.Json @using System.Net.Http.Json
@using System.Text.Json @using System.Text.Json
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Rendering @using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.SignalR.Client
@using Microsoft.JSInterop
@using Microsoft.Extensions.Options
@using static Microsoft.AspNetCore.Components.Web.RenderMode @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
@using DbFirst.BlazorWebApp.Components @using DbFirst.BlazorWebApp.Components
@using DbFirst.BlazorWebApp.Models
@using DbFirst.BlazorWebApp.Models.Grid @using DbFirst.BlazorWebApp.Models.Grid
@using DbFirst.BlazorWebApp.Services @using DbFirst.BlazorWebApp.Services
@using DbFirst.Contracts.Catalogs
@using DbFirst.Contracts.Dashboards
@using DbFirst.Contracts.MassData
@using DbFirst.Contracts.Layouts
@using DbFirst.Domain
@using DevExpress.Blazor @using DevExpress.Blazor
@using DevExpress.DashboardBlazor @using DevExpress.DashboardBlazor
@using DevExpress.DashboardWeb @using DevExpress.DashboardWeb

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
@@ -18,8 +18,4 @@
<Folder Include="wwwroot\images\" /> <Folder Include="wwwroot\images\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DbFirst.Contracts\DbFirst.Contracts.csproj" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,7 +0,0 @@
namespace DbFirst.BlazorWebApp.Models;
public record ApiResult<T>(bool Success, T? Value, string? Error)
{
public static ApiResult<T> Ok(T? value) => new(true, value, null);
public static ApiResult<T> Fail(string? error) => new(false, default, error);
}

View File

@@ -0,0 +1,12 @@
namespace DbFirst.BlazorWebApp.Models;
public class CatalogReadDto
{
public int Guid { get; set; }
public string CatTitle { get; set; } = null!;
public string CatString { get; set; } = null!;
public string AddedWho { get; set; } = null!;
public DateTime AddedWhen { get; set; }
public string? ChangedWho { get; set; }
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace DbFirst.BlazorWebApp.Models;
public class CatalogWriteDto
{
public string CatTitle { get; set; } = string.Empty;
public string CatString { get; set; } = string.Empty;
public int UpdateProcedure { get; set; }
}

View File

@@ -1,4 +1,4 @@
namespace DbFirst.Contracts.Dashboards; namespace DbFirst.BlazorWebApp.Models;
public class DashboardInfoDto public class DashboardInfoDto
{ {

View File

@@ -4,8 +4,8 @@ namespace DbFirst.BlazorWebApp.Models.Grid
{ {
public class BandLayout public class BandLayout
{ {
public List<BandDefinition> Bands { get; set; } = []; public List<BandDefinition> Bands { get; set; } = new();
public List<string> ColumnOrder { get; set; } = []; public List<string> ColumnOrder { get; set; } = new();
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase); public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public GridPersistentLayout? GridLayout { get; set; } public GridPersistentLayout? GridLayout { get; set; }
public SizeMode SizeMode { get; set; } = SizeMode.Medium; public SizeMode SizeMode { get; set; } = SizeMode.Medium;
@@ -15,7 +15,7 @@ namespace DbFirst.BlazorWebApp.Models.Grid
{ {
public string Id { get; set; } = string.Empty; public string Id { get; set; } = string.Empty;
public string Caption { get; set; } = string.Empty; public string Caption { get; set; } = string.Empty;
public List<string> Columns { get; set; } = []; public List<string> Columns { get; set; } = new();
} }
public class BandOption public class BandOption

View File

@@ -1,4 +1,4 @@
namespace DbFirst.Contracts.Layouts; namespace DbFirst.BlazorWebApp.Models;
public class LayoutDto public class LayoutDto
{ {

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

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

View File

@@ -1,4 +1,3 @@
using DbFirst.BlazorWebApp;
using DbFirst.BlazorWebApp.Components; using DbFirst.BlazorWebApp.Components;
using DbFirst.BlazorWebApp.Services; using DbFirst.BlazorWebApp.Services;
using DevExpress.Blazor; using DevExpress.Blazor;
@@ -14,17 +13,37 @@ builder.Services.AddScoped<ThemeState>();
builder.Services.AddScoped<BandLayoutService>(); builder.Services.AddScoped<BandLayoutService>();
var apiBaseUrl = builder.Configuration["ApiBaseUrl"]; var apiBaseUrl = builder.Configuration["ApiBaseUrl"];
builder.Services.Configure<AppSettings>(builder.Configuration);
void ConfigureClient(HttpClient client)
{
if (!string.IsNullOrWhiteSpace(apiBaseUrl)) if (!string.IsNullOrWhiteSpace(apiBaseUrl))
{
builder.Services.AddHttpClient<CatalogApiClient>(client =>
{
client.BaseAddress = new Uri(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);
});
builder.Services.AddHttpClient<LayoutApiClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
});
builder.Services.AddHttpClient<TimeApiClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
});
}
else
{
builder.Services.AddHttpClient<CatalogApiClient>();
builder.Services.AddHttpClient<DashboardApiClient>();
builder.Services.AddHttpClient<MassDataApiClient>();
builder.Services.AddHttpClient<LayoutApiClient>();
builder.Services.AddHttpClient<TimeApiClient>();
} }
builder.Services.AddHttpClient<ICatalogApiClient, CatalogApiClient>(ConfigureClient);
builder.Services.AddHttpClient<IDashboardApiClient, DashboardApiClient>(ConfigureClient);
builder.Services.AddHttpClient<IMassDataApiClient, MassDataApiClient>(ConfigureClient);
builder.Services.AddHttpClient<ILayoutApiClient, LayoutApiClient>(ConfigureClient);
var app = builder.Build(); var app = builder.Build();

View File

@@ -1,59 +0,0 @@
using System.Net;
namespace DbFirst.BlazorWebApp.Services;
internal sealed class ProblemDetailsDto
{
public string? Type { get; set; }
public string? Title { get; set; }
public string? Detail { get; set; }
}
internal static class ApiClientHelper
{
public static async Task<string> ReadErrorAsync(HttpResponseMessage response)
{
string? problemTitle = null;
string? problemDetail = null;
try
{
var problem = await response.Content.ReadFromJsonAsync<ProblemDetailsDto>();
if (problem != null)
{
problemTitle = problem.Title;
problemDetail = problem.Detail ?? problem.Type;
}
}
catch { }
var status = response.StatusCode;
var reason = response.ReasonPhrase;
var body = await response.Content.ReadAsStringAsync();
string? detail = problemDetail;
if (string.IsNullOrWhiteSpace(detail) && !string.IsNullOrWhiteSpace(body))
detail = body;
return status switch
{
HttpStatusCode.BadRequest => $"Eingabe ungültig{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.NotFound => $"Nicht gefunden{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Conflict => $"Konflikt{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Unauthorized => $"Nicht autorisiert{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Forbidden => $"Nicht erlaubt{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.InternalServerError => $"Serverfehler{FormatSuffix(problemTitle, detail, reason)}",
_ => $"Fehler {(int)status} {reason ?? string.Empty}{FormatSuffix(problemTitle, detail, reason)}"
};
}
private static string FormatSuffix(string? title, string? detail, string? reason)
{
var parts = new List<string>();
if (!string.IsNullOrWhiteSpace(title)) parts.Add(title);
if (!string.IsNullOrWhiteSpace(detail)) parts.Add(detail);
if (parts.Count == 0 && !string.IsNullOrWhiteSpace(reason)) parts.Add(reason);
if (parts.Count == 0) return string.Empty;
return ": " + string.Join(" | ", parts);
}
}

View File

@@ -1,30 +1,24 @@
using DbFirst.BlazorWebApp.Models.Grid; using DbFirst.BlazorWebApp.Models;
using DbFirst.Contracts.Layouts; using DbFirst.BlazorWebApp.Models.Grid;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using System.Text.Json; using System.Text.Json;
namespace DbFirst.BlazorWebApp.Services namespace DbFirst.BlazorWebApp.Services
{ {
public class BandLayoutService(ILayoutApiClient layoutApi, IJSRuntime jsRuntime) public class BandLayoutService(LayoutApiClient layoutApi, IJSRuntime jsRuntime)
{ {
private const string LayoutUserStorageKey = "layoutUser"; private const string LayoutUserStorageKey = "layoutUser";
private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
private string? _cachedLayoutUser;
public async Task<string> EnsureLayoutUserAsync() public async Task<string> EnsureLayoutUserAsync()
{ {
if (!string.IsNullOrWhiteSpace(_cachedLayoutUser))
return _cachedLayoutUser;
var layoutUser = await jsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey); var layoutUser = await jsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
if (string.IsNullOrWhiteSpace(layoutUser)) if (string.IsNullOrWhiteSpace(layoutUser))
{ {
layoutUser = Guid.NewGuid().ToString("N"); layoutUser = Guid.NewGuid().ToString("N");
await jsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser); await jsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
} }
return layoutUser;
_cachedLayoutUser = layoutUser;
return _cachedLayoutUser;
} }
public async Task<BandLayout> LoadBandLayoutAsync( public async Task<BandLayout> LoadBandLayoutAsync(
@@ -88,9 +82,9 @@ namespace DbFirst.BlazorWebApp.Services
Dictionary<string, ColumnDefinition> columnLookup) Dictionary<string, ColumnDefinition> columnLookup)
{ {
layout ??= new BandLayout(); layout ??= new BandLayout();
layout.Bands ??= []; layout.Bands ??= new List<BandDefinition>();
layout.ColumnOrder ??= []; layout.ColumnOrder ??= new List<string>();
layout.ColumnWidths ??= []; layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
foreach (var band in layout.Bands) foreach (var band in layout.Bands)
{ {

View File

@@ -1,9 +1,10 @@
using System.Net;
using System.Net.Http.Json;
using DbFirst.BlazorWebApp.Models; using DbFirst.BlazorWebApp.Models;
using DbFirst.Contracts.Catalogs;
namespace DbFirst.BlazorWebApp.Services; namespace DbFirst.BlazorWebApp.Services;
public class CatalogApiClient : ICatalogApiClient public class CatalogApiClient
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private const string Endpoint = "api/catalogs"; private const string Endpoint = "api/catalogs";
@@ -13,51 +14,123 @@ public class CatalogApiClient : ICatalogApiClient
_httpClient = httpClient; _httpClient = httpClient;
} }
public async Task<List<CatalogReadDto>> GetAllAsync(CancellationToken ct = default) public async Task<List<CatalogReadDto>> GetAllAsync()
{ {
var result = await _httpClient.GetFromJsonAsync<List<CatalogReadDto>>(Endpoint, ct); var result = await _httpClient.GetFromJsonAsync<List<CatalogReadDto>>(Endpoint);
return result ?? []; return result ?? new List<CatalogReadDto>();
} }
public async Task<CatalogReadDto?> GetByIdAsync(int id, CancellationToken ct = default) public async Task<CatalogReadDto?> GetByIdAsync(int id)
{ {
return await _httpClient.GetFromJsonAsync<CatalogReadDto>($"{Endpoint}/{id}", ct); return await _httpClient.GetFromJsonAsync<CatalogReadDto>($"{Endpoint}/{id}");
} }
public async Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto, CancellationToken ct = default) public async Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto)
{ {
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto, ct); var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
var payload = await response.Content.ReadFromJsonAsync<CatalogReadDto>(); var payload = await response.Content.ReadFromJsonAsync<CatalogReadDto>();
return ApiResult<CatalogReadDto?>.Ok(payload); return ApiResult<CatalogReadDto?>.Ok(payload);
} }
var error = await ApiClientHelper.ReadErrorAsync(response); var error = await ReadErrorAsync(response);
return ApiResult<CatalogReadDto?>.Fail(error); return ApiResult<CatalogReadDto?>.Fail(error);
} }
public async Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto, CancellationToken ct = default) public async Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto)
{ {
var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto, ct); var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
return ApiResult<bool>.Ok(true); return ApiResult<bool>.Ok(true);
} }
var error = await ApiClientHelper.ReadErrorAsync(response); var error = await ReadErrorAsync(response);
return ApiResult<bool>.Fail(error); return ApiResult<bool>.Fail(error);
} }
public async Task<ApiResult<bool>> DeleteAsync(int id, CancellationToken ct = default) public async Task<ApiResult<bool>> DeleteAsync(int id)
{ {
var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}", ct); var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}");
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
return ApiResult<bool>.Ok(true); return ApiResult<bool>.Ok(true);
} }
var error = await ApiClientHelper.ReadErrorAsync(response); var error = await ReadErrorAsync(response);
return ApiResult<bool>.Fail(error); return ApiResult<bool>.Fail(error);
} }
private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
{
string? problemTitle = null;
string? problemDetail = null;
try
{
var problem = await response.Content.ReadFromJsonAsync<ProblemDetailsDto>();
if (problem != null)
{
problemTitle = problem.Title;
problemDetail = problem.Detail ?? problem.Type;
}
}
catch
{
}
var status = response.StatusCode;
var reason = response.ReasonPhrase;
var body = await response.Content.ReadAsStringAsync();
string? detail = problemDetail;
if (string.IsNullOrWhiteSpace(detail) && !string.IsNullOrWhiteSpace(body))
{
detail = body;
}
if (status == HttpStatusCode.Conflict)
{
return "Datensatz existiert bereits. Bitte wählen Sie einen anderen Titel.";
}
if (status == HttpStatusCode.BadRequest && (detail?.Contains("CatTitle cannot be changed", StringComparison.OrdinalIgnoreCase) ?? false))
{
return "Titel kann nicht geändert werden.";
}
return status switch
{
HttpStatusCode.BadRequest => $"Eingabe ungültig{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.NotFound => $"Nicht gefunden{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Conflict => $"Konflikt{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Unauthorized => $"Nicht autorisiert{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Forbidden => $"Nicht erlaubt{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.InternalServerError => $"Serverfehler{FormatSuffix(problemTitle, detail, reason)}",
_ => $"Fehler {(int)status} {reason ?? string.Empty}{FormatSuffix(problemTitle, detail, reason)}"
};
}
private static string FormatSuffix(string? title, string? detail, string? reason)
{
var parts = new List<string>();
if (!string.IsNullOrWhiteSpace(title)) parts.Add(title);
if (!string.IsNullOrWhiteSpace(detail)) parts.Add(detail);
if (parts.Count == 0 && !string.IsNullOrWhiteSpace(reason)) parts.Add(reason);
if (parts.Count == 0) return string.Empty;
return ": " + string.Join(" | ", parts);
}
}
public record ApiResult<T>(bool Success, T? Value, string? Error)
{
public static ApiResult<T> Ok(T? value) => new(true, value, null);
public static ApiResult<T> Fail(string? error) => new(false, default, error);
}
internal sealed class ProblemDetailsDto
{
public string? Type { get; set; }
public string? Title { get; set; }
public string? Detail { get; set; }
} }

View File

@@ -1,8 +1,9 @@
using DbFirst.Contracts.Dashboards; using System.Net.Http.Json;
using DbFirst.BlazorWebApp.Models;
namespace DbFirst.BlazorWebApp.Services; namespace DbFirst.BlazorWebApp.Services;
public class DashboardApiClient : IDashboardApiClient public class DashboardApiClient
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private const string Endpoint = "api/dashboard/dashboards"; private const string Endpoint = "api/dashboard/dashboards";
@@ -12,9 +13,9 @@ public class DashboardApiClient : IDashboardApiClient
_httpClient = httpClient; _httpClient = httpClient;
} }
public async Task<List<DashboardInfoDto>> GetAllAsync(CancellationToken ct = default) public async Task<List<DashboardInfoDto>> GetAllAsync()
{ {
var result = await _httpClient.GetFromJsonAsync<List<DashboardInfoDto>>(Endpoint, ct); var result = await _httpClient.GetFromJsonAsync<List<DashboardInfoDto>>(Endpoint);
return result ?? []; return result ?? new List<DashboardInfoDto>();
} }
} }

View File

@@ -1,13 +0,0 @@
using DbFirst.BlazorWebApp.Models;
using DbFirst.Contracts.Catalogs;
namespace DbFirst.BlazorWebApp.Services;
public interface ICatalogApiClient
{
Task<List<CatalogReadDto>> GetAllAsync(CancellationToken ct = default);
Task<CatalogReadDto?> GetByIdAsync(int id, CancellationToken ct = default);
Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto, CancellationToken ct = default);
Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto, CancellationToken ct = default);
Task<ApiResult<bool>> DeleteAsync(int id, CancellationToken ct = default);
}

View File

@@ -1,9 +0,0 @@
using DbFirst.Contracts.Dashboards;
namespace DbFirst.BlazorWebApp.Services
{
public interface IDashboardApiClient
{
Task<List<DashboardInfoDto>> GetAllAsync(CancellationToken ct = default);
}
}

View File

@@ -1,11 +0,0 @@
using DbFirst.Contracts.Layouts;
namespace DbFirst.BlazorWebApp.Services
{
public interface ILayoutApiClient
{
Task<LayoutDto?> GetAsync(string layoutType, string layoutKey, string userName, CancellationToken ct = default);
Task<LayoutDto> UpsertAsync(LayoutDto dto, CancellationToken ct = default);
Task DeleteAsync(string layoutType, string layoutKey, string userName, CancellationToken ct = default);
}
}

View File

@@ -1,13 +0,0 @@
using DbFirst.BlazorWebApp.Models;
using DbFirst.Contracts.MassData;
namespace DbFirst.BlazorWebApp.Services
{
public interface IMassDataApiClient
{
Task<int> GetCountAsync(CancellationToken ct = default);
Task<List<MassDataReadDto>> GetAllAsync(int? skip, int? take, CancellationToken ct = default);
Task<ApiResult<MassDataReadDto?>> UpsertAsync(MassDataWriteDto dto, CancellationToken ct = default);
Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName, CancellationToken ct = default);
}
}

View File

@@ -1,8 +1,9 @@
using DbFirst.Contracts.Layouts; using System.Net.Http.Json;
using DbFirst.BlazorWebApp.Models;
namespace DbFirst.BlazorWebApp.Services; namespace DbFirst.BlazorWebApp.Services;
public class LayoutApiClient : ILayoutApiClient public class LayoutApiClient
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private const string Endpoint = "api/layouts"; private const string Endpoint = "api/layouts";
@@ -12,10 +13,10 @@ public class LayoutApiClient : ILayoutApiClient
_httpClient = httpClient; _httpClient = httpClient;
} }
public async Task<LayoutDto?> GetAsync(string layoutType, string layoutKey, string userName, CancellationToken ct = default) public async Task<LayoutDto?> GetAsync(string layoutType, string layoutKey, string userName)
{ {
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}"; var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
var response = await _httpClient.GetAsync(url, ct); var response = await _httpClient.GetAsync(url);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound) if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{ {
return null; return null;
@@ -25,12 +26,12 @@ public class LayoutApiClient : ILayoutApiClient
return await response.Content.ReadFromJsonAsync<LayoutDto>(); return await response.Content.ReadFromJsonAsync<LayoutDto>();
} }
public async Task<LayoutDto> UpsertAsync(LayoutDto dto, CancellationToken ct = default) public async Task<LayoutDto> UpsertAsync(LayoutDto dto)
{ {
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto, ct); var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
var detail = await ApiClientHelper.ReadErrorAsync(response); var detail = await ReadErrorAsync(response);
throw new InvalidOperationException(detail); throw new InvalidOperationException(detail);
} }
@@ -38,10 +39,21 @@ public class LayoutApiClient : ILayoutApiClient
return payload ?? dto; return payload ?? dto;
} }
public async Task DeleteAsync(string layoutType, string layoutKey, string userName, CancellationToken ct = default) private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
{
var body = await response.Content.ReadAsStringAsync();
if (!string.IsNullOrWhiteSpace(body))
{
return body;
}
return $"{(int)response.StatusCode} {response.ReasonPhrase}".Trim();
}
public async Task DeleteAsync(string layoutType, string layoutKey, string userName)
{ {
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}"; var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
var response = await _httpClient.DeleteAsync(url, ct); var response = await _httpClient.DeleteAsync(url);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound) if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{ {
return; return;

View File

@@ -1,9 +1,9 @@
using System.Net.Http.Json;
using DbFirst.BlazorWebApp.Models; using DbFirst.BlazorWebApp.Models;
using DbFirst.Contracts.MassData;
namespace DbFirst.BlazorWebApp.Services; namespace DbFirst.BlazorWebApp.Services;
public class MassDataApiClient : IMassDataApiClient public class MassDataApiClient
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private const string Endpoint = "api/massdata"; private const string Endpoint = "api/massdata";
@@ -13,13 +13,13 @@ public class MassDataApiClient : IMassDataApiClient
_httpClient = httpClient; _httpClient = httpClient;
} }
public async Task<int> GetCountAsync(CancellationToken ct = default) public async Task<int> GetCountAsync()
{ {
var result = await _httpClient.GetFromJsonAsync<int?>("api/massdata/count", ct); var result = await _httpClient.GetFromJsonAsync<int?>("api/massdata/count");
return result ?? 0; return result ?? 0;
} }
public async Task<List<MassDataReadDto>> GetAllAsync(int? skip, int? take, CancellationToken ct = default) public async Task<List<MassDataReadDto>> GetAllAsync(int? skip, int? take)
{ {
var query = new List<string>(); var query = new List<string>();
if (skip.HasValue) if (skip.HasValue)
@@ -32,31 +32,26 @@ public class MassDataApiClient : IMassDataApiClient
} }
var url = query.Count == 0 ? Endpoint : $"{Endpoint}?{string.Join("&", query)}"; var url = query.Count == 0 ? Endpoint : $"{Endpoint}?{string.Join("&", query)}";
var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>(url, ct); var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>(url);
return result ?? []; return result ?? new List<MassDataReadDto>();
} }
public async Task<ApiResult<MassDataReadDto?>> UpsertAsync(MassDataWriteDto dto, CancellationToken ct = default) public async Task<MassDataReadDto> UpsertAsync(MassDataWriteDto dto)
{
var response = await _httpClient.PostAsJsonAsync($"{Endpoint}/upsert", dto, ct);
if (response.IsSuccessStatusCode)
{ {
var response = await _httpClient.PostAsJsonAsync($"{Endpoint}/upsert", dto);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>(); var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>();
return ApiResult<MassDataReadDto?>.Ok(payload); return payload ?? new MassDataReadDto();
} }
var error = await ApiClientHelper.ReadErrorAsync(response); public async Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName)
return ApiResult<MassDataReadDto?>.Fail(error);
}
public async Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName, CancellationToken ct = default)
{ {
if (string.IsNullOrWhiteSpace(customerName)) if (string.IsNullOrWhiteSpace(customerName))
{ {
return null; return null;
} }
var response = await _httpClient.GetAsync($"{Endpoint}/{Uri.EscapeDataString(customerName)}", ct); var response = await _httpClient.GetAsync($"{Endpoint}/{Uri.EscapeDataString(customerName)}");
if (response.StatusCode == System.Net.HttpStatusCode.NotFound) if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{ {
return null; return null;

View File

@@ -12,54 +12,24 @@ public class ThemeState
} }
public bool IsDarkMode { get; private set; } public bool IsDarkMode { get; private set; }
public string CurrentThemeName { get; private set; } = "Fluent";
/// <summary>
/// Themes die eine native DevExpress Dark-Variante besitzen:
/// - Fluent ? Themes.Fluent.Clone(ThemeMode.Dark), verwendet --DS-* Token-System
/// - BlazingBerry ? Themes.BlazingDark
/// Alle anderen Themes (Purple, OfficeWhite, BootstrapExternal) haben keine offizielle
/// Dark-Variante; dort übernehmen CSS-Overrides auf --dxbl-grid-* Variablen die Arbeit.
/// </summary>
public bool IsNativeDarkTheme => IsDarkMode &&
(CurrentThemeName == "Fluent" || CurrentThemeName == "BlazingBerry");
public static readonly List<string> AvailableThemes = ["Fluent", "BlazingBerry", "Purple", "OfficeWhite", "BootstrapExternal"];
public event Action? OnChange; public event Action? OnChange;
public void SetTheme(string themeName)
{
if (CurrentThemeName == themeName) return;
CurrentThemeName = themeName;
ApplyTheme();
OnChange?.Invoke();
}
public void SetDarkMode(bool isDarkMode) public void SetDarkMode(bool isDarkMode)
{ {
if (IsDarkMode == isDarkMode) return; if (IsDarkMode == isDarkMode)
IsDarkMode = isDarkMode; {
ApplyTheme(); return;
OnChange?.Invoke();
} }
private void ApplyTheme() IsDarkMode = isDarkMode;
{
if (CurrentThemeName == "Fluent")
{
var theme = Themes.Fluent.Clone(properties => var theme = Themes.Fluent.Clone(properties =>
{ {
properties.Mode = IsDarkMode ? ThemeMode.Dark : ThemeMode.Light; properties.Mode = isDarkMode ? ThemeMode.Dark : ThemeMode.Light;
properties.ApplyToPageElements = true; properties.ApplyToPageElements = true;
}); });
themeChangeService.SetTheme(theme); themeChangeService.SetTheme(theme);
} OnChange?.Invoke();
else if (CurrentThemeName == "BlazingBerry") themeChangeService.SetTheme(IsDarkMode ? Themes.BlazingDark : Themes.BlazingBerry);
else if (CurrentThemeName == "Purple") themeChangeService.SetTheme(Themes.Purple);
else if (CurrentThemeName == "OfficeWhite") themeChangeService.SetTheme(Themes.OfficeWhite);
else if (CurrentThemeName == "BootstrapExternal") themeChangeService.SetTheme(Themes.BootstrapExternal);
else
themeChangeService.SetTheme(Themes.Fluent);
} }
} }

View File

@@ -0,0 +1,27 @@
using System.Net.Http.Json;
namespace DbFirst.BlazorWebApp.Services;
public class TimeApiClient
{
private readonly HttpClient _httpClient;
private const string Endpoint = "api/time";
public TimeApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<DateTime?> InsertAndGetLastAsync()
{
var response = await _httpClient.PostAsync(Endpoint, null);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TimeResponse>();
return result?.Now;
}
private sealed class TimeResponse
{
public DateTime? Now { get; set; }
}
}

View File

@@ -5,11 +5,5 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"ApiBaseUrl": "https://localhost:7204/", "ApiBaseUrl": "https://localhost:7204/"
"BrowserLink": {
"Enabled": false
},
"DetailedErrors": true
} }

View File

@@ -148,105 +148,6 @@ dxbl-grid tbody tr:nth-child(even) td {
background-color: var(--grid-stripe-bg) !important; background-color: var(--grid-stripe-bg) !important;
} }
/* ?? Dark-Mode-Overrides für nicht-native Themes ?????????????????????????????
Strategie: CSS-Custom-Properties werden von DevExpress DIREKT auf den
Komponenten-Elementen definiert (z. B. --dxbl-popup-bg:#fff auf .dxbl-modal).
Eine geerbte Variable aus html.dx-dark würde durch die direkte Zuweisung
überschrieben. Deshalb targeten wir exakt dieselben Elemente, aber mit einem
zusätzlichen Vorfahren-Selektor (html.dx-dark) für höhere Spezifizität:
html.dx-dark .dxbl-modal = (0,2,1) > .dxbl-modal = (0,1,0) ?
html.dx-dark wird per JS gesetzt, wenn IsDarkMode && !IsNativeDarkTheme.
?? */
/* Popup / Modal (CRUD-Dialoge) Variablen-Quelle: .dxbl-modal */
html.dx-dark .dxbl-modal {
--dxbl-popup-bg: #2d2d2d;
--dxbl-popup-color: #e8e8e8;
--dxbl-popup-border-color: #555;
--dxbl-popup-header-bg: #333;
--dxbl-popup-header-color: #e8e8e8;
--dxbl-popup-footer-bg: #333;
--dxbl-popup-footer-color: #e8e8e8;
}
/* Flyout (Column Chooser, Filter-Panel) Variablen-Quelle: .dxbl-flyout */
html.dx-dark .dxbl-flyout {
--dxbl-flyout-bg: #2d2d2d;
--dxbl-flyout-color: #e8e8e8;
--dxbl-flyout-border-color: #555;
--dxbl-flyout-header-bg: #333;
--dxbl-flyout-header-color: #e8e8e8;
--dxbl-flyout-footer-bg: #333;
}
/* Dropdown (ComboBox-Klappliste, Band-Dropdowns) Quelle: .dxbl-dropdown */
html.dx-dark .dxbl-dropdown,
html.dx-dark .dxbl-itemlist-dropdown {
--dxbl-dropdown-bg: #2d2d2d;
--dxbl-dropdown-color: #e8e8e8;
--dxbl-dropdown-border-color: #555;
--dxbl-dropdown-header-bg: #333;
--dxbl-dropdown-footer-bg: #333;
}
/* Edit-Dropdown (ComboBox-Popup wenn als Modal gerendert) Quelle: .dxbl-edit-dropdown */
html.dx-dark .dxbl-edit-dropdown {
--dxbl-edit-dropdown-bg: #2d2d2d;
--dxbl-edit-dropdown-color: #e8e8e8;
--dxbl-edit-dropdown-border-color: #555;
}
/* ListBox (Einträge in Dropdowns) Quelle: .dxbl-list-box */
html.dx-dark .dxbl-list-box,
html.dx-dark .dxbl-list-box-render-container {
--dxbl-list-box-bg: #2d2d2d;
--dxbl-list-box-color: #e8e8e8;
--dxbl-list-box-border-color: #555;
--dxbl-list-box-item-hover-bg: #3a3a3a;
--dxbl-list-box-item-hover-color: #e8e8e8;
}
/* TextEdit / ComboBox Eingabefeld Quelle: .dxbl-text-edit */
html.dx-dark .dxbl-text-edit {
--dxbl-text-edit-bg: #2d2d2d;
--dxbl-text-edit-color: #e8e8e8;
--dxbl-text-edit-border-color: #555;
--dxbl-text-edit-btn-bg: #3a3a3a;
--dxbl-text-edit-btn-color: #e8e8e8;
--dxbl-text-edit-btn-hover-bg: #444;
--dxbl-text-edit-btn-hover-color: #e8e8e8;
}
/* Buttons */
html.dx-dark .dxbl-btn {
--dxbl-btn-color: #e8e8e8;
--dxbl-btn-bg: #3a3a3a;
--dxbl-btn-border-color: #555;
--dxbl-btn-hover-bg: #444;
--dxbl-btn-hover-color: #e8e8e8;
--dxbl-btn-hover-border-color: #666;
}
/* FormLayout */
html.dx-dark .dxbl-fl {
--dxbl-fl-caption-color: #bbb;
--dxbl-fl-group-bg: #242424;
--dxbl-fl-group-color: #e8e8e8;
}
/* Grid */
html.dx-dark .dxbl-grid {
background-color: #242424;
color: #e8e8e8;
border-color: #444;
}
html.dx-dark .dxbl-grid > .dxbl-scroll-viewer,
html.dx-dark .dxbl-grid > .dxbl-grid-top-panel {
background-color: #242424;
color: #e8e8e8;
}
/* MassData-spezifisch */ /* MassData-spezifisch */
.page-size-selector { .page-size-selector {
display: flex; display: flex;
@@ -284,7 +185,3 @@ html.dx-dark .dxbl-grid > .dxbl-grid-top-panel {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.top-row .btn-gap {
margin-left: 8px;
}

View File

@@ -1,4 +1,3 @@
window.setSize = function (fontSize) { window.setSize = function (fontSize) {
document.documentElement.style.setProperty('--global-size', fontSize); document.documentElement.style.setProperty('--global-size', fontSize);
}; };

View File

@@ -1,12 +0,0 @@
namespace DbFirst.Contracts.Catalogs;
public class CatalogReadDto
{
public int Guid { get; set; }
public string CatTitle { get; set; } = string.Empty;
public string CatString { get; set; } = string.Empty;
public string AddedWho { get; set; } = string.Empty;
public DateTime AddedWhen { get; set; }
public string? ChangedWho { get; set; }
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -1,10 +0,0 @@
using DbFirst.Domain;
namespace DbFirst.Contracts.Catalogs;
public class CatalogWriteDto
{
public string CatTitle { get; set; } = string.Empty;
public string CatString { get; set; } = string.Empty;
public CatalogUpdateProcedure UpdateProcedure { get; set; } = CatalogUpdateProcedure.Update;
}

View File

@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DbFirst.Domain\DbFirst.Domain.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
namespace DbFirst.Domain.Entities;
public class TimeRecord
{
public DateTime? Now { get; set; }
}

View File

@@ -4,11 +4,11 @@ public partial class VwmyCatalog
{ {
public int Guid { get; set; } public int Guid { get; set; }
public string CatTitle { get; set; } = string.Empty; public string CatTitle { get; set; } = null!;
public string CatString { get; set; } = string.Empty; public string CatString { get; set; } = null!;
public string AddedWho { get; set; } = string.Empty; public string AddedWho { get; set; } = null!;
public DateTime AddedWhen { get; set; } public DateTime AddedWhen { get; set; }

View File

@@ -16,6 +16,7 @@ public partial class ApplicationDbContext : DbContext
public virtual DbSet<VwmyCatalog> VwmyCatalogs { get; set; } public virtual DbSet<VwmyCatalog> VwmyCatalogs { get; set; }
public virtual DbSet<SmfLayout> SmfLayouts { get; set; } public virtual DbSet<SmfLayout> SmfLayouts { get; set; }
public virtual DbSet<TimeRecord> Times { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@@ -83,6 +84,16 @@ public partial class ApplicationDbContext : DbContext
.HasColumnName("CHANGED_WHEN"); .HasColumnName("CHANGED_WHEN");
}); });
modelBuilder.Entity<TimeRecord>(entity =>
{
entity.HasNoKey();
entity.ToTable("TIME");
entity.Property(e => e.Now)
.HasColumnType("datetime")
.HasColumnName("NOW");
});
OnModelCreatingPartial(modelBuilder); OnModelCreatingPartial(modelBuilder);
} }

View File

@@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="16.1.1" /> <PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.22" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.22" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.22" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.22" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.22"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.22">
@@ -15,7 +15,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
</ItemGroup> </ItemGroup>

View File

@@ -1,5 +1,3 @@
using DbFirst.Application.Repositories;
using DbFirst.Infrastructure.Repositories;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -17,10 +15,6 @@ public static class DependencyInjection
services.AddDbContext<MassDataDbContext>(options => services.AddDbContext<MassDataDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("MassDataConnection"))); options.UseSqlServer(configuration.GetConnectionString("MassDataConnection")));
services.AddScoped<ICatalogRepository, CatalogRepository>();
services.AddScoped<IMassDataRepository, MassDataRepository>();
services.AddScoped<ILayoutRepository, LayoutRepository>();
return services; return services;
} }
} }

View File

@@ -1,6 +1,6 @@
using DbFirst.Application.Repositories;
using DbFirst.Domain; using DbFirst.Domain;
using DbFirst.Domain.Entities; using DbFirst.Domain.Entities;
using DbFirst.Application.Repositories;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Data; using System.Data;

View File

@@ -34,7 +34,7 @@ public class LayoutRepository : ILayoutRepository
UserName = userName, UserName = userName,
LayoutData = layoutData, LayoutData = layoutData,
AddedWho = userName, AddedWho = userName,
AddedWhen = DateTime.UtcNow AddedWhen = DateTime.Now
}; };
_db.SmfLayouts.Add(entity); _db.SmfLayouts.Add(entity);
} }

View File

@@ -1,8 +1,8 @@
using System.Data;
using DbFirst.Application.Repositories; using DbFirst.Application.Repositories;
using DbFirst.Domain.Entities; using DbFirst.Domain.Entities;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Data;
namespace DbFirst.Infrastructure.Repositories; namespace DbFirst.Infrastructure.Repositories;
@@ -20,6 +20,11 @@ public class MassDataRepository : IMassDataRepository
return await _db.Massdata.AsNoTracking().CountAsync(cancellationToken); 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) public async Task<Massdata?> GetByCustomerNameAsync(string customerName, CancellationToken cancellationToken = default)
{ {
return await _db.Massdata.AsNoTracking() return await _db.Massdata.AsNoTracking()

View File

@@ -0,0 +1,28 @@
using DbFirst.Application.Repositories;
using DbFirst.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace DbFirst.Infrastructure.Repositories;
public class TimeRepository : ITimeRepository
{
private readonly ApplicationDbContext _db;
public TimeRepository(ApplicationDbContext db)
{
_db = db;
}
public async Task InsertAsync(CancellationToken cancellationToken = default)
{
await _db.Database.ExecuteSqlRawAsync("INSERT INTO [TIME] (NOW) VALUES (GETDATE())", cancellationToken);
}
public async Task<TimeRecord?> GetLastAsync(CancellationToken cancellationToken = default)
{
return await _db.Times
.AsNoTracking()
.OrderByDescending(t => t.Now)
.FirstOrDefaultAsync(cancellationToken);
}
}

View File

@@ -13,8 +13,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.Domain", "DbFirst.D
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.BlazorWebApp", "DbFirst.BlazorWebApp\DbFirst.BlazorWebApp.csproj", "{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.BlazorWebApp", "DbFirst.BlazorWebApp\DbFirst.BlazorWebApp.csproj", "{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.Contracts", "DbFirst.Contracts\DbFirst.Contracts.csproj", "{94FFCA01-9476-49B3-B7D0-5706514E42E4}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -85,18 +83,6 @@ Global
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Release|x64.Build.0 = Release|Any CPU {FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Release|x64.Build.0 = Release|Any CPU
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Release|x86.ActiveCfg = Release|Any CPU {FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Release|x86.ActiveCfg = Release|Any CPU
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Release|x86.Build.0 = Release|Any CPU {FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Release|x86.Build.0 = Release|Any CPU
{94FFCA01-9476-49B3-B7D0-5706514E42E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{94FFCA01-9476-49B3-B7D0-5706514E42E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{94FFCA01-9476-49B3-B7D0-5706514E42E4}.Debug|x64.ActiveCfg = Debug|Any CPU
{94FFCA01-9476-49B3-B7D0-5706514E42E4}.Debug|x64.Build.0 = Debug|Any CPU
{94FFCA01-9476-49B3-B7D0-5706514E42E4}.Debug|x86.ActiveCfg = Debug|Any CPU
{94FFCA01-9476-49B3-B7D0-5706514E42E4}.Debug|x86.Build.0 = Debug|Any CPU
{94FFCA01-9476-49B3-B7D0-5706514E42E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{94FFCA01-9476-49B3-B7D0-5706514E42E4}.Release|Any CPU.Build.0 = Release|Any CPU
{94FFCA01-9476-49B3-B7D0-5706514E42E4}.Release|x64.ActiveCfg = Release|Any CPU
{94FFCA01-9476-49B3-B7D0-5706514E42E4}.Release|x64.Build.0 = Release|Any CPU
{94FFCA01-9476-49B3-B7D0-5706514E42E4}.Release|x86.ActiveCfg = Release|Any CPU
{94FFCA01-9476-49B3-B7D0-5706514E42E4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

File diff suppressed because it is too large Load Diff