Compare commits

...

44 Commits

Author SHA1 Message Date
OlgunR
8933deec96 Add dark mode override for non-native DevExpress themes
Implements a dark mode override system for DevExpress Blazor themes lacking native dark support. Adds a JS function to toggle a dx-dark class on <html>, updates ThemeState to detect native dark themes, and applies targeted CSS variable overrides for consistent dark styling. Disables prerendering to ensure JS interop, and improves theme switching logic and documentation.
2026-05-05 16:41:15 +02:00
OlgunR
2010673eba Remove unused menu state and toggle logic from NavMenu
Removed the private menuOpen field and ToggleMenu() method from NavMenu.razor, as they are no longer needed for menu state management.
2026-04-30 15:41:05 +02:00
OlgunR
b75e7d730c Modernize sidebar with DxTreeView and new responsive styles
Refactor NavMenu to use DevExpress DxTreeView for navigation, replacing the old NavLink-based menu. Update sidebar and navigation row styling to use CSS variables, remove Bootstrap-specific and SVG icon CSS, and add a responsive hamburger menu for small screens. Improve dark mode support and overall maintainability.
2026-04-30 15:37:04 +02:00
OlgunR
075433c780 Improve spacing for Dark Mode button in top row
Wrapped the Dark Mode toggle button in a span with left margin
for better separation from the theme combo box. Added a new
.btn-gap CSS class to standardize button spacing in the top row.
2026-04-23 15:59:50 +02:00
OlgunR
35e39ff979 Add theme selection dropdown and refactor theme handling
Introduce a DxComboBox in MainLayout for selecting between multiple themes. Update ThemeState to manage the current theme, provide a list of available themes, and apply the selected theme via a new SetTheme method. Refactor dark mode handling to work with the new theme system, and ensure UI updates on theme or mode changes.
2026-04-23 15:44:26 +02:00
OlgunR
292ce02370 Refactor using statements for clearer layer boundaries
Clean up and reorganize using/import statements across the solution. Remove unnecessary DTO imports from Application and Infrastructure layers, and ensure Contracts DTOs are only referenced in API and BlazorWebApp layers. No business logic is changed; these updates improve code organization, reduce coupling, and clarify architectural separation between layers.
2026-04-23 13:55:05 +02:00
OlgunR
df4de5d5f5 Refactor imports and ensure enum type safety in Catalogs
Reorganize and deduplicate using directives in _Imports.razor for clarity and maintainability. Explicitly cast UpdateProcedure to CatalogUpdateProcedure in CatalogsGrid.razor to enforce type safety. Restore necessary DevExpress and DbFirst namespaces.
2026-04-23 13:54:50 +02:00
OlgunR
e4624c92ef Refactor DTOs: make public, add properties, clean up
Refactored DTO classes by removing unnecessary using statements and internal wrappers, making them public, and adding explicit properties with default values. Clarified namespaces and improved accessibility for use in API contracts or service layers. Added CatalogUpdateProcedure to CatalogWriteDto to specify update operation type.
2026-04-23 11:48:45 +02:00
OlgunR
b0d60461b4 Standardize DTO naming and namespaces for MassData
Refactored DTO class and namespace names from Massdata* to MassData* in DbFirst.Contracts. Updated all relevant application files to use the correct DTO namespaces for Catalogs and MassData. Adjusted _Imports.razor to include new DTO namespaces and removed unused usings. These changes improve code consistency and reduce namespace-related errors.
2026-04-23 11:41:10 +02:00
OlgunR
c45c1a69a7 Remove obsolete DTO classes and add Layouts folder
Deleted DTOs for catalogs, layouts, mass data, and dashboard info from both Application and BlazorWebApp.Models namespaces. Updated DbFirst.Application.csproj to include a new Layouts folder for future organization. No functional code changes in this commit.
2026-04-23 11:25:31 +02:00
OlgunR
b268e53e1f Add project references to Contracts and Domain projects
Added DbFirst.Contracts as a project reference to both DbFirst.Application and DbFirst.BlazorWebApp, and added DbFirst.Domain to DbFirst.Application. Also, a BOM was introduced in the BlazorWebApp project file. These changes enable shared use of contracts and domain types across projects.
2026-04-23 11:23:24 +02:00
OlgunR
a1fee6f5c0 Add initial empty DTO classes for Catalogs, Dashboards, etc.
Introduced six new internal DTO classes: CatalogReadDto, CatalogWriteDto, DashboardInfoDto, LayoutDto, MassdataReadDto, and MassdataWriteDto. Each class resides in its appropriate namespace and currently contains no properties or methods, serving as placeholders for future data transfer logic.
2026-04-23 11:22:58 +02:00
OlgunR
dbb39354ab Add DbFirst.Contracts project to solution
Added new DbFirst.Contracts project targeting .NET 8.0 with nullable and implicit usings enabled. Included a project reference to DbFirst.Domain and updated the solution file to register the new project with all build configurations.
2026-04-23 11:15:53 +02:00
OlgunR
2ffa389978 Update AutoMapper to 16.1.1 and adjust registration
Upgraded AutoMapper to version 16.1.1 across API, Application, and Infrastructure projects. Removed AutoMapper.Extensions.Microsoft.DependencyInjection where no longer needed. Updated AutoMapper registration in DependencyInjection.cs to use the new API. No other changes made.
2026-04-22 09:35:50 +02:00
OlgunR
f54de87ca2 Standardize string property initialization to string.Empty
Replaced String.Empty and null! with string.Empty for string properties
in CatalogReadDto, CatalogWriteDto, and VwmyCatalog classes. This
ensures consistent initialization and helps prevent null reference
issues across the codebase.
2026-04-22 09:25:03 +02:00
OlgunR
1b4205219f Set string defaults to String.Empty in CatalogReadDto
Changed default values of CatTitle, CatString, and AddedWho from null! to String.Empty to prevent potential null reference issues.
2026-04-21 17:01:44 +02:00
OlgunR
b35c167648 Add [FromBody] to DTO params in controller actions
Explicitly annotate DTO parameters with [FromBody] in CatalogsController (Create, Update) and MassDataController (Upsert) to ensure correct model binding from the request body and improve API clarity.
2026-04-21 15:22:33 +02:00
OlgunR
087708dcf7 Use array literal [] for empty CORS origins default
Replaces Array.Empty<string>() with the C# array literal [] when defaulting the CORS allowed origins array. This is a syntactic improvement with no change in behavior.
2026-04-21 15:22:19 +02:00
OlgunR
be40c39f47 Initialize VwmyCatalog string properties with String.Empty
Changed default values of CatTitle, CatString, and AddedWho from null! to String.Empty to prevent potential null reference issues and ensure safer property initialization.
2026-04-21 13:43:33 +02:00
OlgunR
4fa3bcae3d Make dashboard change notifications async and robust
Refactored DashboardChangeNotifier and IDashboardChangeNotifier to use async notification with improved error handling and logging. Updated SqlDashboardStorage to call the new async notification method after dashboard changes, ensuring non-blocking and reliable client updates.
2026-04-21 13:42:31 +02:00
OlgunR
612d8371d3 Improve exception handling with ProblemDetails responses
ExceptionHandlingMiddleware now returns structured ProblemDetails
responses for errors, including specific handling for
InvalidOperationException (400 Bad Request) and general exceptions
(500 Internal Server Error). Responses use "application/problem+json"
content type and include trace IDs. ProblemDetails support is also
registered in Program.cs.
2026-04-21 13:18:59 +02:00
OlgunR
b7e66ab5f9 Update MediatR and DI packages; refactor registration
Upgraded to MediatR 14.1.0 and updated registration to use the new syntax. Removed MediatR.Extensions.Microsoft.DependencyInjection where no longer needed. Bumped Microsoft.Extensions.DependencyInjection.Abstractions to 10.0.0 in Application and Infrastructure projects.
2026-04-21 12:01:23 +02:00
OlgunR
95a388015a Remove GetAllAsync method from MassDataRepository
The GetAllAsync method, which returned all Massdata records, has been removed from the MassDataRepository class. No other changes were made in this commit.
2026-04-21 10:30:10 +02:00
OlgunR
c16f0483c3 Remove unused using directives from API and client files
Cleaned up unnecessary System.Net and System.Net.Http.Json usings across multiple files, including controllers and API client classes, to reduce dependencies and improve code clarity.
2026-04-21 10:16:05 +02:00
OlgunR
83292f23f3 Refactor dashboard config to factory class
Move DashboardConfigurator setup from Program.cs to a new static DashboardConfiguratorFactory. This centralizes dashboard file creation, data source registration, and storage logic, improving code organization and maintainability. Unused usings are cleaned up accordingly.
2026-04-21 10:09:50 +02:00
OlgunR
c73c7e63fe Move LayoutDto to its own file and update namespace
Separated LayoutDto from LayoutsController.cs into LayoutDto.cs and set its namespace to DbFirst.Application.Layouts for better code organization and maintainability.
2026-04-20 16:51:57 +02:00
OlgunR
3e78e2e2cf Move catalog title update validation to handler
Validation preventing CatTitle changes during updates is now enforced in UpdateCatalogHandler instead of the controller. The handler throws an exception if a title change is attempted. Also, streamlined UpdateProcedure handling and removed redundant CatTitle mapping logic.
2026-04-20 16:38:29 +02:00
OlgunR
05825b6815 Refactor DI: move repository registrations to Infrastructure
Centralize repository service registrations in Infrastructure's
DependencyInjection.cs for better maintainability and separation
of concerns. Remove direct registrations from Program.cs and
add necessary using statements.
2026-04-20 16:28:37 +02:00
OlgunR
4720c8f87b Use UTC for AddedWhen timestamps in layout repository
Changed AddedWhen to use DateTime.UtcNow instead of DateTime.Now to ensure timestamps are stored in UTC, avoiding issues with local time zones.
2026-04-20 16:22:18 +02:00
OlgunR
0c936d0bf9 Adopt C# 12 collection expressions for empty lists/dicts
Refactored code to use C# 12 collection expressions ([]) for initializing empty lists and dictionaries instead of the older constructors. This change modernizes and simplifies collection initialization across models, services, and API clients without altering any logic.
2026-04-20 14:33:08 +02:00
OlgunR
fcbc66f8f5 Add CancellationToken support to all API client methods
All API client interfaces and implementations now accept an optional CancellationToken parameter for each method. This enables consumers to cancel HTTP requests, improving responsiveness and resource management. No changes were made to core logic or return types; only method signatures and HttpClient calls were updated to support cancellation.
2026-04-20 13:50:53 +02:00
OlgunR
aab6478f9a Refactor API clients to use interface-based DI
Introduce interfaces for all API clients and update dependency injection to use these interfaces. Refactor services and components to depend on abstractions instead of concrete implementations, improving testability and maintainability.
2026-04-20 13:23:16 +02:00
OlgunR
177d418ac3 Use InvokeAsync for async state updates in BandGridBase
Replaced direct StateHasChanged() calls with InvokeAsync(StateHasChanged) to ensure asynchronous state updates. This helps prevent issues that can arise from synchronous state changes during component lifecycle events or event callbacks in Blazor.
2026-04-20 12:59:27 +02:00
OlgunR
7cc88c13f3 Cache layout user to reduce localStorage access
Add a private _cachedLayoutUser field to BandLayoutService and update EnsureLayoutUserAsync to cache the layout user value after first retrieval or generation. This avoids repeated localStorage calls and improves performance.
2026-04-20 11:48:50 +02:00
OlgunR
8bf172755b Update using directives in _Imports.razor for clarity
Added @using DbFirst.BlazorWebApp and reorganized the placement of @using Microsoft.Extensions.Options to improve code organization and maintain consistent dependency order.
2026-04-20 11:34:15 +02:00
OlgunR
27c8f92a3b Refactor API base URL config to use AppSettings class
Replaced direct IConfiguration usage with a strongly-typed AppSettings class for accessing the API base URL. Registered AppSettings with DI and updated Dashboard.razor to use IOptions<AppSettings>. Updated using statements and DI setup for improved type safety and centralized configuration management.
2026-04-20 11:32:45 +02:00
OlgunR
cd0a824064 Refactor grid logic into BandGridBase<TItem> base class
Move shared state and methods from CatalogsGrid and MassDataGrid into BandGridBase<TItem>. This centralizes edit context handling, validation, popup header logic, row editing/deleting, and layout feedback, reducing duplication and improving maintainability. Individual grid components now only override OnEditFieldChanged for custom validation.
2026-04-20 10:52:05 +02:00
OlgunR
0008fac1d2 Refactor HTTP client registration for API clients
Simplified registration of CatalogApiClient, DashboardApiClient, MassDataApiClient, and LayoutApiClient by introducing a ConfigureClient method. This method sets the BaseAddress if ApiBaseUrl is configured, removing the previous if-else logic and reducing code duplication.
2026-04-20 10:29:17 +02:00
OlgunR
4659913711 Move ApiResult<T> to ApiResult.cs with proper namespace
ApiResult<T> and its static methods were relocated from CatalogApiClient.cs to a new ApiResult.cs file. The new file now includes the DbFirst.BlazorWebApp.Models namespace for better code organization.
2026-04-20 10:24:47 +02:00
OlgunR
bb23cb6629 Refactor error handling to use ApiClientHelper
Replaced the private ReadErrorAsync method in LayoutApiClient with calls to ApiClientHelper.ReadErrorAsync, centralizing error handling logic for improved consistency and reuse.
2026-04-20 10:19:40 +02:00
OlgunR
b3f7df6801 Initialize SignalR hub in OnAfterRenderAsync after first render
Moved SignalR hub connection setup from OnInitializedAsync to OnAfterRenderAsync, ensuring initialization occurs only after the component's first render. This prevents premature connection attempts and aligns with best practices for component lifecycle management.
2026-04-20 10:08:24 +02:00
OlgunR
3a1cb68cf0 Change OnCustomizeEditModel to synchronous void method
Converted OnCustomizeEditModel in MassDataGrid.razor from async Task to synchronous void, removing asynchronous support from this method.
2026-04-17 14:28:50 +02:00
OlgunR
c93518202b Add custom toolbar actions with icons to grids
Introduce Add, Edit, and Delete buttons with Bootstrap Icons in the toolbars of CatalogsGrid and MassDataGrid. Hide the default command column by overriding ShowCommandColumn. Update BandGridBase to conditionally render the command column. Track focused row index for toolbar actions, enabling row operations via toolbar instead of the grid's built-in command column. Add Bootstrap Icons stylesheet to App.razor.
2026-04-17 12:08:39 +02:00
OlgunR
6792b426ff Enhance grid UI: column chooser button & default widths
- Set default width and hide new button for DxGridBandColumn in BandGridBase.cs
- Always show column chooser button in CatalogsGrid and MassDataGrid
- Add "Spalten" toolbar button to open column chooser dialog
- Improves accessibility and consistency of grid column customization
2026-04-16 16:02:31 +02:00
78 changed files with 832 additions and 606 deletions

View File

@@ -1,7 +1,7 @@
using DbFirst.Application.Catalogs;
using DbFirst.Application.Catalogs.Commands;
using DbFirst.Application.Catalogs.Queries;
using DbFirst.Domain;
using DbFirst.Contracts.Catalogs;
using MediatR;
using Microsoft.AspNetCore.Mvc;
@@ -37,7 +37,7 @@ public class CatalogsController : ControllerBase
}
[HttpPost]
public async Task<ActionResult<CatalogReadDto>> Create(CatalogWriteDto dto, CancellationToken cancellationToken)
public async Task<ActionResult<CatalogReadDto>> Create([FromBody]CatalogWriteDto dto, CancellationToken cancellationToken)
{
var created = await _mediator.Send(new CreateCatalogCommand(dto), cancellationToken);
if (created == null)
@@ -48,19 +48,8 @@ public class CatalogsController : ControllerBase
}
[HttpPut("{id:int}")]
public async Task<ActionResult<CatalogReadDto>> Update(int id, CatalogWriteDto dto, CancellationToken cancellationToken)
public async Task<ActionResult<CatalogReadDto>> Update(int id, [FromBody] 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);
if (updated == null)
{

View File

@@ -1,7 +1,8 @@
using System.Text;
using DbFirst.Application.Repositories;
using DbFirst.Contracts.Layouts;
using DbFirst.Domain.Entities;
using Microsoft.AspNetCore.Mvc;
using System.Text;
namespace DbFirst.API.Controllers;
@@ -83,12 +84,4 @@ public class LayoutsController : ControllerBase
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.Queries;
using DbFirst.Contracts.MassData;
using MediatR;
using Microsoft.AspNetCore.Mvc;
@@ -50,7 +50,7 @@ public class MassDataController : ControllerBase
}
[HttpPost("upsert")]
public async Task<ActionResult<MassDataReadDto>> Upsert(MassDataWriteDto dto, CancellationToken cancellationToken)
public async Task<ActionResult<MassDataReadDto>> Upsert([FromBody]MassDataWriteDto dto, CancellationToken cancellationToken)
{
var result = await _mediator.Send(new UpsertMassDataByCustomerNameCommand(dto), cancellationToken);
return Ok(result);

View File

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

View File

@@ -0,0 +1,115 @@
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
{
void NotifyChanged();
Task NotifyChangedAsync();
}

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,11 @@
using DbFirst.API.Middleware;
using DbFirst.API.Dashboards;
using DbFirst.API.Hubs;
using DbFirst.API.Middleware;
using DbFirst.Application;
using DbFirst.Application.Repositories;
using DbFirst.Domain;
using DbFirst.Domain.Entities;
using DbFirst.Infrastructure;
using DbFirst.Infrastructure.Repositories;
using DevExpress.AspNetCore;
using DevExpress.DashboardAspNetCore;
using DevExpress.DashboardCommon;
using DevExpress.DashboardWeb;
using DevExpress.DataAccess.Json;
using System.Xml.Linq;
var builder = WebApplication.CreateBuilder(args);
@@ -20,6 +13,7 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddProblemDetails();
// 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.
@@ -36,7 +30,7 @@ builder.Services.AddCors(options =>
}
else
{
var origins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
var origins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? [];
if (origins.Length > 0)
{
policy.WithOrigins(origins)
@@ -51,98 +45,11 @@ builder.Services.AddCors(options =>
builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddApplication();
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
builder.Services.AddScoped<IMassDataRepository, MassDataRepository>();
builder.Services.AddScoped<ILayoutRepository, LayoutRepository>();
builder.Services.AddDevExpressControls();
builder.Services.AddSignalR();
builder.Services.AddSingleton<IDashboardChangeNotifier, DashboardChangeNotifier>();
builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvider) => {
var dashboardsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "Dashboards");
Directory.CreateDirectory(dashboardsPath);
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;
});
builder.Services.AddScoped<DashboardConfigurator>(sp =>
DashboardConfiguratorFactory.Create(sp, builder.Configuration, builder.Environment));
var app = builder.Build();
@@ -164,16 +71,4 @@ app.MapDashboardRoute("api/dashboard", "DefaultDashboard");
app.MapHub<DashboardsHub>("/hubs/dashboards");
app.MapControllers();
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);
}
app.Run();

View File

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

View File

@@ -1,12 +0,0 @@
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

@@ -1,10 +0,0 @@
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,3 +1,4 @@
using DbFirst.Contracts.Catalogs;
using MediatR;
namespace DbFirst.Application.Catalogs.Commands;

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
using AutoMapper;
using DbFirst.Application.Repositories;
using DbFirst.Domain.Entities;
using DbFirst.Contracts.Catalogs;
using DbFirst.Domain;
using DbFirst.Domain.Entities;
using MediatR;
namespace DbFirst.Application.Catalogs.Commands;
@@ -25,18 +26,20 @@ public class UpdateCatalogHandler : IRequestHandler<UpdateCatalogCommand, Catalo
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);
entity.Guid = request.Id;
entity.CatTitle = request.Dto.UpdateProcedure == CatalogUpdateProcedure.Update
? existing.CatTitle
: request.Dto.CatTitle;
entity.AddedWho = existing.AddedWho;
entity.AddedWhen = existing.AddedWhen;
entity.ChangedWho = "system";
entity.ChangedWhen = DateTime.UtcNow;
var procedure = request.Dto.UpdateProcedure;
var updated = await _repository.UpdateAsync(request.Id, entity, procedure, cancellationToken);
var updated = await _repository.UpdateAsync(request.Id, entity, request.Dto.UpdateProcedure, cancellationToken);
return updated == null ? null : _mapper.Map<CatalogReadDto>(updated);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +0,0 @@
namespace DbFirst.Application.MassData;
public class MassDataReadDto
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public decimal Amount { get; set; }
public string Category { get; set; } = string.Empty;
public bool StatusFlag { get; set; }
public DateTime AddedWhen { get; set; }
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -1,9 +0,0 @@
namespace DbFirst.Application.MassData;
public class MassDataWriteDto
{
public string CustomerName { get; set; } = string.Empty;
public decimal Amount { get; set; }
public string Category { get; set; } = string.Empty;
public bool StatusFlag { get; set; }
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,11 +18,20 @@
<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="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="DbFirst.BlazorWebApp.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<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 />
</head>

View File

@@ -2,6 +2,7 @@
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;
@@ -27,6 +28,15 @@ public abstract class BandGridBase<TItem> : ComponentBase
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 ---
@@ -103,6 +113,8 @@ public abstract class BandGridBase<TItem> : ComponentBase
// --- 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" });
@@ -145,12 +157,12 @@ public abstract class BandGridBase<TItem> : ComponentBase
.Select(c => c.FieldName)
.ToList();
}
StateHasChanged();
_ = InvokeAsync(StateHasChanged);
}
protected void UpdateBandOptions()
{
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
bandOptions = [new() { Id = string.Empty, Caption = "Ohne Band" }];
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
}
@@ -172,9 +184,12 @@ public abstract class BandGridBase<TItem> : ComponentBase
protected RenderFragment RenderColumns() => builder =>
{
var seq = 0;
builder.OpenComponent<DxGridCommandColumn>(seq++);
builder.AddAttribute(seq++, "Width", "120px");
builder.CloseComponent();
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)))
@@ -211,4 +226,53 @@ public abstract class BandGridBase<TItem> : ComponentBase
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
@if (!string.IsNullOrWhiteSpace(errorMessage))
{
@@ -38,6 +38,7 @@ else
<div class="grid-section">
<DxGrid Data="@items"
ColumnChooserButtonDisplayMode="GridColumnChooserButtonDisplayMode.Always"
TItem="CatalogReadDto"
KeyFieldName="@nameof(CatalogReadDto.Guid)"
SizeMode="@_sizeMode"
@@ -58,9 +59,42 @@ else
DataItemDeleting="OnDataItemDeleting"
FocusedRowEnabled="true"
@bind-FocusedRowKey="focusedRowKey"
RowClick="@(args => _focusedVisibleIndex = args.VisibleIndex)"
@ref="gridRef">
<ToolbarTemplate>
<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">
<Template Context="_">
<DxDropDownButton Text="@FormatSizeText(_sizeMode)"
@@ -115,16 +149,10 @@ else
@code {
private List<CatalogReadDto> items = new();
private bool isLoading;
private bool hasLoaded;
private string? errorMessage;
private string? infoMessage;
private EditContext? editContext;
private ValidationMessageStore? validationMessageStore;
private int? focusedRowKey;
private string popupHeaderText = "Edit";
protected override string LayoutKey => "CatalogsGrid";
protected override bool ShowCommandColumn => false;
protected override List<ColumnDefinition> ColumnDefinitions { get; } = new()
{
@@ -174,17 +202,7 @@ else
}
}
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)
protected override void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
{
if (validationMessageStore == null || editContext == null) return;
@@ -202,8 +220,6 @@ else
}
}
private void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{
popupHeaderText = e.IsNew ? "Neu" : "Edit";
@@ -243,7 +259,7 @@ else
{
CatTitle = editModel.CatTitle,
CatString = editModel.CatString,
UpdateProcedure = editModel.UpdateProcedure
UpdateProcedure = (CatalogUpdateProcedure)editModel.UpdateProcedure
};
try
@@ -343,25 +359,4 @@ else
public int Value { get; set; }
public string Text { get; set; } = string.Empty;
}
private async Task SaveLayoutWithFeedbackAsync()
{
try
{
await SaveLayoutAsync();
infoMessage = "Layout gespeichert.";
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
}
}
private async Task ResetLayoutWithFeedbackAsync()
{
await ResetLayoutAsync();
infoMessage = "Layout zurückgesetzt.";
errorMessage = null;
}
}

View File

@@ -1,15 +1,23 @@
@inherits LayoutComponentBase
@implements IDisposable
@inject ThemeState ThemeState
@inject IJSRuntime JS
<div class="page @(ThemeState.IsDarkMode ? "app-dark" : "app-light")">
<div class="page @(ThemeState.IsDarkMode ? "app-dark" : "app-light") @(ThemeState.IsNativeDarkTheme ? "native-dark" : "")">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<DxButton Text="@(ThemeState.IsDarkMode ? "Dark Mode aus" : "Dark Mode an")" Click="ToggleTheme" />
<DxComboBox Data="@ThemeState.AvailableThemes"
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>
</div>
@@ -26,9 +34,43 @@
</div>
@code {
private bool _isInteractive;
protected override void OnInitialized()
{
ThemeState.OnChange += StateHasChanged;
ThemeState.OnChange += OnThemeChanged;
}
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()
@@ -38,6 +80,6 @@
public void Dispose()
{
ThemeState.OnChange -= StateHasChanged;
ThemeState.OnChange -= OnThemeChanged;
}
}

View File

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

View File

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

View File

@@ -1,117 +1,52 @@
.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 {
.nav-brand-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.navbar-brand {
font-size: 1.1rem;
.nav-brand-link {
font-size: 1.05rem;
font-weight: 600;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.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;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
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-toggle-btn {
display: block;
background: none;
border: none;
font-size: 1.4rem;
cursor: pointer;
padding: 0.25rem 0.5rem;
line-height: 1;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
.nav-scrollable.nav-open {
display: block;
}
.sidebar-tree {
width: 100%;
}
@media (min-width: 641px) {
.navbar-toggler {
.nav-toggle-btn {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -1,5 +1,5 @@
@inherits BandGridBase<MassDataReadDto>
@inject MassDataApiClient Api
@inject IMassDataApiClient Api
@if (!string.IsNullOrWhiteSpace(errorMessage))
{
@@ -51,6 +51,7 @@ else
<div class="grid-section">
<DxGrid Data="@items"
ColumnChooserButtonDisplayMode="GridColumnChooserButtonDisplayMode.Always"
TItem="MassDataReadDto"
KeyFieldName="@nameof(MassDataReadDto.Id)"
SizeMode="@_sizeMode"
@@ -71,9 +72,42 @@ else
DataItemDeleting="OnDataItemDeleting"
FocusedRowEnabled="true"
@bind-FocusedRowKey="focusedRowKey"
RowClick="@(args => _focusedVisibleIndex = args.VisibleIndex)"
@ref="gridRef">
<ToolbarTemplate>
<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">
<Template Context="_">
<DxDropDownButton Text="@FormatSizeText(_sizeMode)"
@@ -141,19 +175,13 @@ else
@code {
private List<MassDataReadDto> items = new();
private bool isLoading;
private bool hasLoaded;
private string? errorMessage;
private string? infoMessage;
private int pageIndex;
private int pageCount = 1;
private int? pageSize = 100;
private string popupHeaderText = "Edit";
private EditContext? editContext;
private ValidationMessageStore? validationMessageStore;
private int? focusedRowKey;
protected override string LayoutKey => "MassDataGrid";
protected override bool ShowCommandColumn => false;
protected override List<ColumnDefinition> ColumnDefinitions { get; } = new()
{
@@ -224,17 +252,7 @@ else
await LoadPage(0);
}
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)
protected override void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
{
if (validationMessageStore == null || editContext == null) return;
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure))
@@ -250,9 +268,7 @@ else
}
}
private void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
private async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{
if (e.IsNew)
{
@@ -356,25 +372,4 @@ else
public int? Value { get; set; }
public string Text { get; set; } = string.Empty;
}
private async Task SaveLayoutWithFeedbackAsync()
{
try
{
await SaveLayoutAsync();
infoMessage = "Layout gespeichert.";
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
}
}
private async Task ResetLayoutWithFeedbackAsync()
{
await ResetLayoutAsync();
infoMessage = "Layout zurückgesetzt.";
errorMessage = null;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
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

@@ -1,12 +0,0 @@
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

@@ -1,8 +0,0 @@
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

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

View File

@@ -1,3 +1,4 @@
using DbFirst.BlazorWebApp;
using DbFirst.BlazorWebApp.Components;
using DbFirst.BlazorWebApp.Services;
using DevExpress.Blazor;
@@ -13,33 +14,18 @@ builder.Services.AddScoped<ThemeState>();
builder.Services.AddScoped<BandLayoutService>();
var apiBaseUrl = builder.Configuration["ApiBaseUrl"];
if (!string.IsNullOrWhiteSpace(apiBaseUrl))
builder.Services.Configure<AppSettings>(builder.Configuration);
void ConfigureClient(HttpClient client)
{
builder.Services.AddHttpClient<CatalogApiClient>(client =>
{
if (!string.IsNullOrWhiteSpace(apiBaseUrl))
client.BaseAddress = new Uri(apiBaseUrl);
});
builder.Services.AddHttpClient<DashboardApiClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
});
builder.Services.AddHttpClient<MassDataApiClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
});
builder.Services.AddHttpClient<LayoutApiClient>(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<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();
// Configure the HTTP request pipeline.

View File

@@ -1,5 +1,4 @@
using System.Net;
using System.Net.Http.Json;
namespace DbFirst.BlazorWebApp.Services;

View File

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

View File

@@ -1,10 +1,9 @@
using System.Net;
using System.Net.Http.Json;
using DbFirst.BlazorWebApp.Models;
using DbFirst.Contracts.Catalogs;
namespace DbFirst.BlazorWebApp.Services;
public class CatalogApiClient
public class CatalogApiClient : ICatalogApiClient
{
private readonly HttpClient _httpClient;
private const string Endpoint = "api/catalogs";
@@ -14,20 +13,20 @@ public class CatalogApiClient
_httpClient = httpClient;
}
public async Task<List<CatalogReadDto>> GetAllAsync()
public async Task<List<CatalogReadDto>> GetAllAsync(CancellationToken ct = default)
{
var result = await _httpClient.GetFromJsonAsync<List<CatalogReadDto>>(Endpoint);
return result ?? new List<CatalogReadDto>();
var result = await _httpClient.GetFromJsonAsync<List<CatalogReadDto>>(Endpoint, ct);
return result ?? [];
}
public async Task<CatalogReadDto?> GetByIdAsync(int id)
public async Task<CatalogReadDto?> GetByIdAsync(int id, CancellationToken ct = default)
{
return await _httpClient.GetFromJsonAsync<CatalogReadDto>($"{Endpoint}/{id}");
return await _httpClient.GetFromJsonAsync<CatalogReadDto>($"{Endpoint}/{id}", ct);
}
public async Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto)
public async Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto, CancellationToken ct = default)
{
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto, ct);
if (response.IsSuccessStatusCode)
{
var payload = await response.Content.ReadFromJsonAsync<CatalogReadDto>();
@@ -38,9 +37,9 @@ public class CatalogApiClient
return ApiResult<CatalogReadDto?>.Fail(error);
}
public async Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto)
public async Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto, CancellationToken ct = default)
{
var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto);
var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto, ct);
if (response.IsSuccessStatusCode)
{
return ApiResult<bool>.Ok(true);
@@ -50,9 +49,9 @@ public class CatalogApiClient
return ApiResult<bool>.Fail(error);
}
public async Task<ApiResult<bool>> DeleteAsync(int id)
public async Task<ApiResult<bool>> DeleteAsync(int id, CancellationToken ct = default)
{
var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}");
var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}", ct);
if (response.IsSuccessStatusCode)
{
return ApiResult<bool>.Ok(true);
@@ -61,11 +60,4 @@ public class CatalogApiClient
var error = await ApiClientHelper.ReadErrorAsync(response);
return ApiResult<bool>.Fail(error);
}
}
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

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

View File

@@ -0,0 +1,13 @@
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

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

View File

@@ -0,0 +1,11 @@
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

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

View File

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

View File

@@ -12,24 +12,54 @@ public class ThemeState
}
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 void SetDarkMode(bool isDarkMode)
public void SetTheme(string themeName)
{
if (IsDarkMode == isDarkMode)
{
return;
}
IsDarkMode = isDarkMode;
var theme = Themes.Fluent.Clone(properties =>
{
properties.Mode = isDarkMode ? ThemeMode.Dark : ThemeMode.Light;
properties.ApplyToPageElements = true;
});
themeChangeService.SetTheme(theme);
if (CurrentThemeName == themeName) return;
CurrentThemeName = themeName;
ApplyTheme();
OnChange?.Invoke();
}
}
public void SetDarkMode(bool isDarkMode)
{
if (IsDarkMode == isDarkMode) return;
IsDarkMode = isDarkMode;
ApplyTheme();
OnChange?.Invoke();
}
private void ApplyTheme()
{
if (CurrentThemeName == "Fluent")
{
var theme = Themes.Fluent.Clone(properties =>
{
properties.Mode = IsDarkMode ? ThemeMode.Dark : ThemeMode.Light;
properties.ApplyToPageElements = true;
});
themeChangeService.SetTheme(theme);
}
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

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

View File

@@ -148,6 +148,105 @@ dxbl-grid tbody tr:nth-child(even) td {
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 */
.page-size-selector {
display: flex;
@@ -185,3 +284,7 @@ dxbl-grid tbody tr:nth-child(even) td {
align-items: center;
justify-content: center;
}
.top-row .btn-gap {
margin-left: 8px;
}

View File

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

View File

@@ -0,0 +1,12 @@
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

@@ -0,0 +1,10 @@
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,7 +1,7 @@
namespace DbFirst.BlazorWebApp.Models;
namespace DbFirst.Contracts.Dashboards;
public class DashboardInfoDto
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,13 @@
<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

@@ -1,4 +1,4 @@
namespace DbFirst.BlazorWebApp.Models;
namespace DbFirst.Contracts.Layouts;
public class LayoutDto
{
@@ -6,4 +6,4 @@ public class LayoutDto
public string LayoutKey { get; set; } = string.Empty;
public string UserName { get; set; } = string.Empty;
public string LayoutData { get; set; } = string.Empty;
}
}

View File

@@ -1,4 +1,4 @@
namespace DbFirst.BlazorWebApp.Models;
namespace DbFirst.Contracts.MassData;
public class MassDataReadDto
{
@@ -9,4 +9,4 @@ public class MassDataReadDto
public bool StatusFlag { get; set; }
public DateTime AddedWhen { get; set; }
public DateTime? ChangedWhen { get; set; }
}
}

View File

@@ -1,4 +1,4 @@
namespace DbFirst.BlazorWebApp.Models;
namespace DbFirst.Contracts.MassData;
public class MassDataWriteDto
{
@@ -6,4 +6,4 @@ public class MassDataWriteDto
public decimal Amount { get; set; }
public string Category { get; set; } = string.Empty;
public bool StatusFlag { get; set; }
}
}

View File

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

View File

@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper" Version="16.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.22" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" 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>
</PackageReference>
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
</ItemGroup>

View File

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

View File

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

View File

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

View File

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

View File

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