Compare commits

...

44 Commits

Author SHA1 Message Date
OlgunR
86feec930b Add Clock page with live DB time and TimeApiClient service
Introduced a new Clock page that displays and updates the current database server time every second by calling a backend API. Added the TimeApiClient service to handle API requests for the server time. Registered TimeApiClient in Program.cs and updated the navigation menu to include a link to the new Clock page. Includes error handling and custom UI styling for the clock display.
2026-03-30 15:16:33 +02:00
OlgunR
f5224e20f2 Add time record API endpoint and supporting infrastructure
Introduced a new TimeController with a POST endpoint to insert and retrieve the latest time record. Added ITimeRepository, TimeRepository, and TimeRecord entity. Implemented MediatR command and handler for time insertion. Updated ApplicationDbContext and DI configuration to support the new feature.
2026-03-30 15:16:03 +02:00
OlgunR
36ade1b26b Carousel - Home 2026-03-30 14:56:51 +02:00
OlgunR
d422d841ff Add zebra-striping to grid rows with theme support
Introduced --grid-stripe-bg CSS variable for both light and dark themes to enable zebra-striping in dxbl-grid components. Applied background color to even-numbered grid rows for improved readability.
2026-03-27 14:13:16 +01:00
OlgunR
ea1b2ea6e4 Update band editor label and move page size selector
Changed band editor toggle label to "Layout" in both CatalogsGrid.razor and MassDataGrid.razor. Relocated the page size selector in MassDataGrid.razor to appear after the band editor section for improved layout consistency.
2026-03-27 09:54:23 +01:00
OlgunR
6101561e72 Add collapsible band editor to grid components
Introduce a toggleable "Band-Layout konfigurieren" section in CatalogsGrid.razor and MassDataGrid.razor, allowing users to expand or collapse the band editor UI. Added bandEditorExpanded state to control visibility. Updated CSS to style the new toggle button and its expanded/collapsed states, improving usability and reducing UI clutter.
2026-03-27 09:46:15 +01:00
OlgunR
64fb76b9e6 Add theme variables for band editor background and border
Introduce CSS variables for band editor colors in light and dark themes. Update .band-editor to use these variables, enabling automatic adaptation to the active theme.
2026-03-27 09:11:56 +01:00
OlgunR
4ac8e94334 Refactor: centralize grid/editor CSS in app.css
Consolidate shared grid and band-editor styles from CatalogsGrid.razor.css and MassDataGrid.razor.css into app.css for improved maintainability and consistency. Only component-specific min-width rules remain in the respective files. Also includes minor formatting cleanups in app.css.
2026-03-25 17:15:55 +01:00
OlgunR
dc74d21426 Refactor: centralize grid band layout logic in service
Introduce BandLayoutService and shared models to manage grid band layouts across components. Refactor CatalogsGrid and MassDataGrid to use the new service, removing duplicated layout logic. Update _Imports.razor and register the service in Program.cs for improved maintainability and code reuse.
2026-03-25 17:04:19 +01:00
OlgunR
566c3b3276 Move dashboard styles to Dashboard.razor.css file
Separated CSS from Dashboard.razor by moving all dashboard-related styles into a new Dashboard.razor.css file. This improves maintainability and keeps styling concerns separate from markup and logic.
2026-03-25 13:34:52 +01:00
OlgunR
ac84866abe Extract inline styles to .razor.css files for grids
Moved CSS from inline <style> blocks in CatalogsGrid.razor and MassDataGrid.razor to dedicated CatalogsGrid.razor.css and MassDataGrid.razor.css files. This improves maintainability and keeps styling concerns separate from component logic. No changes to the actual styles were made.
2026-03-25 12:02:30 +01:00
OlgunR
d9ce4a5dca Move common usings to _Imports.razor for global access
Removed redundant using directives from CatalogsGrid.razor and MassDataGrid.razor. Added them to _Imports.razor to ensure global availability across components. Also added @using DbFirst.BlazorWebApp to _Imports.razor for consistency.
2026-03-25 10:49:51 +01:00
OlgunR
13617dde87 Remove duplicate DevExpress.Blazor package reference
Cleaned up the project file by removing a redundant <ItemGroup>
that contained a duplicate DevExpress.Blazor package reference.
Ensured that each package is referenced only once and that
Microsoft.AspNetCore.SignalR.Client remains in its own group.
2026-03-25 10:45:57 +01:00
OlgunR
789066a214 Replace HTML entities with Latin-1 bytes in UI strings
Replaced HTML-encoded German special characters with their Windows-1252 (Latin-1) encoded byte equivalents in user-facing strings in CatalogsGrid.razor and MassDataGrid.razor. Renamed and expanded the reset layout method for improved layout reset behavior. Updated button labels and info messages for consistency. These changes address encoding and rendering issues by shifting from HTML entities to direct byte encoding.
2026-02-23 09:13:12 +01:00
OlgunR
964d508630 Persist grid SizeMode in BandLayout for layout restore
Added SizeMode property to BandLayout in both CatalogsGrid.razor and MassDataGrid.razor. The grid's current SizeMode is now saved and restored with the layout, ensuring user preferences for grid sizing persist across sessions. This improves the consistency of the user experience when reloading or switching layouts.
2026-02-23 08:55:53 +01:00
OlgunR
c5ca9f0048 Add grid font size selector and fix German character display
Introduced font size adjustment for grids via dropdown toolbar and CSS variable. Added JavaScript for dynamic font size changes. Replaced German umlauts with HTML entities for proper rendering. Refactored grid code and improved error/info message display. Enhances accessibility and user experience.
2026-02-19 17:01:33 +01:00
OlgunR
e7aa41aa4d Add dark mode toggle with ThemeState service
Implement dark mode support using a new ThemeState service that manages theme switching and integrates with DevExpress Blazor theming. Update App.razor to apply the theme globally, enhance MainLayout with a toggle button and dynamic CSS classes, and add dark mode styles to CSS files. Register ThemeState as a scoped service in Program.cs.
2026-02-18 10:07:14 +01:00
OlgunR
9a4f189e4e Add row focusing support to CatalogsGrid and MassDataGrid
Enable row focusing in both grid components by introducing the focusedRowKey property and binding it to the grid. After create or update operations, set focusedRowKey to the affected item's key, ensuring the grid automatically focuses the relevant row. Improves user experience by highlighting newly created or updated items.
2026-02-17 12:40:37 +01:00
OlgunR
797dd44f25 Remove BlazorWasm project and all related frontend assets
This commit deletes the entire Blazor WebAssembly frontend project, including all source code, components, DTOs, services, CSS, configuration files, static assets (such as favicon and icons), and the main HTML entry point. All references to `DbFirst.BlazorWasm` have been removed from the solution file, and associated dependencies like Bootstrap CSS and its source map are also deleted. This effectively removes the BlazorWasm application from the repository.
2026-02-11 08:15:47 +01:00
OlgunR
0e313b2e07 Remove custom grid sort/filter CSS from MassDataGrid
Deleted styles for custom sort icons, filter row layout, and search input in MassDataGrid. Retained only general layout and popup/editor styles, reverting to default grid visuals.
2026-02-10 13:36:08 +01:00
OlgunR
db232914a9 Switch grids to built-in filter menu, remove custom filters
Removed custom filter row templates and operator logic for date and number columns in CatalogsGrid.razor and MassDataGrid.razor. Grids now use DevExpress's built-in filter menu (FilterMenuButtonDisplayMode.Always), simplifying code and UI. All related filter fields and methods were deleted.
2026-02-10 13:32:21 +01:00
OlgunR
f49db97147 Remove DevExpress theme CSS links from App.razor
Removed references to fluent-light.bs5.min.css and bootstrap-external.bs5.min.css from App.razor. These DevExpress Blazor theme stylesheets are no longer included in the application. All other stylesheets remain unchanged.
2026-02-10 13:17:29 +01:00
OlgunR
0876ebe999 Update DevExpress Blazor to use Bootstrap 5 theme
Replaced Fluent theme CSS with Bootstrap 5 Fluent Light theme. Updated DevExpress Blazor service registration to explicitly use BootstrapVersion.v5 for consistent styling. Cleaned up and reordered CSS includes.
2026-02-10 09:54:39 +01:00
OlgunR
74504c583e Improve loading UX with spinner and wider filter fields
Replaced plain loading text with a centered Bootstrap spinner in CatalogsGrid and MassDataGrid. Introduced hasLoaded flag for more accurate loading state handling. Increased min-width of filter input fields for better usability. Added .loading-container CSS for consistent spinner placement.
2026-02-09 16:40:09 +01:00
OlgunR
8387b71676 Add operator selection to grid filter rows for dates/numbers
Introduce custom filter row cell templates for date and numeric columns in CatalogsGrid and MassDataGrid. Users can now select a filter operator (e.g., =, <, >, <=, >=) alongside the filter value for columns like AddedWhen, ChangedWhen, and Amount. Updated filter criteria logic to support these operators using DevExpress expressions. Added new CSS for filter row alignment and appearance. Removed obsolete methods and improved layout logic. This enhances filtering flexibility and user experience in both grids.
2026-02-09 16:10:16 +01:00
OlgunR
8824492057 Enable grouping features in DxGrid components
Added ShowGroupPanel, ShowGroupedColumns, and AllowGroup to DxGrid in CatalogsGrid.razor and MassDataGrid.razor for enhanced data grouping. Also set PagerVisible="false" in MassDataGrid.razor to hide pager controls.
2026-02-09 14:03:47 +01:00
OlgunR
59f22be405 Update nav menu with new custom SVG icons
Replaced icons for "Catalogs", "Dashboards", and "MassData" in NavMenu.razor with new custom Bootstrap-style SVG icons. Added corresponding CSS classes using data URIs to embed the new white icons, ensuring consistent appearance in the navigation menu.
2026-02-09 13:41:11 +01:00
OlgunR
23865aefb6 Remove Counter and Weather pages and nav links
Removed the Counter and Weather pages by deleting their component files. Also updated the navigation menu to remove links to these pages, leaving only Home and Catalogs. This simplifies the app's navigation and available features.
2026-02-09 13:22:04 +01:00
OlgunR
2a730ddfcc Refactor grid and band layout persistence logic
Unify band and grid layout saving into a single "Layout speichern" action and method. Store the full grid layout in BandLayout, and simplify column/band rendering logic. Apply saved layouts after render for consistency. Remove obsolete ordering logic and update messages for clarity. These changes improve robustness and maintainability of layout persistence in CatalogsGrid and MassDataGrid.
2026-02-09 11:36:56 +01:00
OlgunR
d78fd5e3d1 Add user-specific persistent grid/band layouts support
Implemented user-customizable, persistent grid and band layouts for CatalogsGrid and MassDataGrid. Added backend API, database entity, and repository for storing layouts per user. Refactored grids to support dynamic band/column rendering, layout management UI, and per-user storage via localStorage and the new API. Registered all necessary services and updated data context. Enables flexible, user-specific grid experiences with saved layouts.
2026-02-06 14:07:52 +01:00
OlgunR
52b2cf9a5b Add user-selectable page size to MassData grid
Users can now choose how many records to display per page in the MassData grid (100, 1,000, 10,000, 100,000, or all). The backend and API client are updated to support nullable skip/take parameters, allowing "all" records to be fetched when desired. The pager and page count calculations are updated to reflect the selected page size, and the pager is hidden if only one page exists. Additional UI and CSS changes provide a combo box for page size selection. The API controller now treats take <= 0 as "no limit."
2026-02-05 15:45:36 +01:00
OlgunR
9d7b3591cc Update grid column captions to German
Changed "Added" to "Angelegt am" and "Changed" to "Geändert am" in the data grid column headers for improved localization. No other column settings were modified.
2026-02-05 14:17:29 +01:00
OlgunR
006ee78422 Refactor MassDataGrid editing and validation logic
- Switch to custom edit model (MassDataEditModel) for popup editing, enabling granular field validation and UI control
- Replace default editors with explicit DxTextBox/DxCheckBox bindings
- Add AmountText field for string input and validation; validate and convert in OnEditModelSaving
- Implement duplicate customer check via new GetByCustomerNameAsync API method
- Show ValidationSummary in popup; manage validation state with ValidationMessageStore and EditContext
- Make popup header and width dynamic; show procedure ComboBox only for existing records
- Restore "New" button in grid command column
- Refactor CatalogsGrid to handle validation clearing in OnEditFieldChanged instead of OnTitleChanged
- General improvements to real-time validation feedback
2026-02-05 14:11:27 +01:00
OlgunR
a52d615750 Update grid popup headers and edit form logic
CatalogsGrid now sets popup edit form header dynamically ("Neu" for new, "Edit" for existing) and only shows the "Update-Prozedur" field when editing. Added IsNew property to CatalogEditModel. MassDataGrid sets popup header to "Bearbeiten". Also standardized DateChanged event handler style in both components.
2026-02-05 12:52:44 +01:00
OlgunR
9bbe34dece Enhance grid filter UI with custom editors for each type
Replaced default filter row editors with custom templates in CatalogsGrid and MassDataGrid. Added DxTextBox for text fields, DxDateEdit for date fields, and a DxComboBox for boolean status filtering. Introduced BoolFilterOption class to support the status dropdown. These changes improve filter usability and data type handling.
2026-02-05 11:37:06 +01:00
OlgunR
05ea47f42c Improve title field validation feedback in catalog edit form
Title field now validates on input, not just on blur. Added OnTitleChanged handler to clear validation messages for CatTitle as the user types. OnFieldChanged now only clears messages for UpdateProcedure. This enhances real-time validation feedback for users.
2026-02-05 11:21:49 +01:00
OlgunR
945c8aaf4a Refactor grids to use DxGrid popup editing
Modernize CatalogsGrid.razor and MassDataGrid.razor to use DevExpress DxGrid's built-in popup editing with EditFormTemplate. Remove custom EditForm panels and manual editing state logic. Move CRUD operations and validation to grid event handlers. Add field-level validation and error display for catalogs. Update grid columns, add command columns, and set audit fields to read-only. Only editing is allowed in MassDataGrid; deletion is disabled. Streamline code and UI for improved maintainability and user experience.
2026-02-05 10:43:40 +01:00
OlgunR
4ef80ce875 Improve MassDataGrid UI: custom sort icons & filter inputs
Replaced default sort icons with custom SVGs for clarity.
Updated filter row to use styled DxTextBox inputs with search icons.
Ensured consistent and intuitive filtering experience across columns.
2026-02-05 08:54:21 +01:00
OlgunR
88c34ef94b Add MassData feature with API, paging, and Blazor grid
Introduces MassData management to backend and Blazor frontend:
- Adds API endpoint for MassData count and paging
- Updates repository and controller for count support
- Implements MediatR query/handler for count
- Adds Blazor page and grid for viewing/editing MassData
- Registers MassDataApiClient and integrates with DI
- Supports paging, upsert, and UI feedback in grid
2026-02-04 13:00:45 +01:00
OlgunR
85b9b0b51a Add MassData API with CQRS, repository, and DbContext
Introduce MassData feature with new API endpoints for querying and upserting records by customer name. Add DTOs, AutoMapper profile, MediatR CQRS handlers, repository pattern, and MassDataDbContext. Register new services in DI and add MassDataConnection to configuration. Upsert uses stored procedure. Enables full CRUD for Massdata via dedicated API.
2026-02-04 11:39:58 +01:00
OlgunR
013088a25f Add real-time dashboard updates with SignalR
Integrate SignalR to provide real-time dashboard update notifications.
- Added DashboardsHub and DashboardChangeNotifier on the backend.
- Modified SqlDashboardStorage to trigger notifications on changes.
- Registered SignalR services and mapped the hub endpoint.
- Updated Blazor clients to connect to the hub and refresh dashboards on change.
- Added SignalR client packages and necessary DI/configuration.
2026-02-04 09:01:28 +01:00
OlgunR
dbe1d9d206 Add mode param to dashboard links for designer/viewer mode
Dashboard navigation links now include a mode query parameter, set to "designer" or "viewer" based on the IsDesigner flag. This enables the app to distinguish between designer and viewer modes when navigating to dashboards.
2026-02-04 08:45:27 +01:00
OlgunR
dc2cccac1f Update dashboard navigation and dynamic loading
- Changed NavMenu to link to /dashboards instead of /dashboards/default
- Refactored Dashboard.razor to list dashboards from API
- Dashboard viewer/designer now loads by selected dashboard ID
- Mode toggle preserves selected dashboard and mode
- Added DashboardApiClient and DashboardInfoDto for API integration
- Registered DashboardApiClient for DI and HTTP client setup in Program.cs
2026-02-03 17:23:04 +01:00
OlgunR
32b6d30ba1 Add toggle for Designer/Viewer modes on dashboard page
Added a button to switch between Designer and Viewer modes for the dashboard. The mode is controlled via a query parameter and updates the dashboard's WorkingMode accordingly. The dashboard component is re-rendered when the mode changes. Also updated the navigation link label to remove the "(Designer)" suffix.
2026-02-03 16:43:16 +01:00
104 changed files with 2689 additions and 1409 deletions

View File

@@ -0,0 +1,94 @@
using System.Text;
using DbFirst.Application.Repositories;
using DbFirst.Domain.Entities;
using Microsoft.AspNetCore.Mvc;
namespace DbFirst.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class LayoutsController : ControllerBase
{
private readonly ILayoutRepository _repository;
public LayoutsController(ILayoutRepository repository)
{
_repository = repository;
}
[HttpGet]
public async Task<ActionResult<LayoutDto>> Get([FromQuery] string layoutType, [FromQuery] string layoutKey, [FromQuery] string userName, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(layoutType) || string.IsNullOrWhiteSpace(layoutKey) || string.IsNullOrWhiteSpace(userName))
{
return BadRequest("layoutType, layoutKey und userName sind erforderlich.");
}
var entity = await _repository.GetAsync(layoutType, layoutKey, userName, cancellationToken);
if (entity == null)
{
return NotFound();
}
return Ok(Map(entity));
}
[HttpPost]
public async Task<ActionResult<LayoutDto>> Upsert(LayoutDto dto, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(dto.LayoutType) || string.IsNullOrWhiteSpace(dto.LayoutKey) || string.IsNullOrWhiteSpace(dto.UserName))
{
return BadRequest("LayoutType, LayoutKey und UserName sind erforderlich.");
}
var data = string.IsNullOrWhiteSpace(dto.LayoutData)
? Array.Empty<byte>()
: Encoding.UTF8.GetBytes(dto.LayoutData);
try
{
var entity = await _repository.UpsertAsync(dto.LayoutType, dto.LayoutKey, dto.UserName, data, cancellationToken);
return Ok(Map(entity));
}
catch (Exception ex)
{
var detail = ex.InnerException?.Message ?? ex.Message;
return Problem(detail: detail, statusCode: StatusCodes.Status500InternalServerError);
}
}
[HttpDelete]
public async Task<IActionResult> Delete([FromQuery] string layoutType, [FromQuery] string layoutKey, [FromQuery] string userName, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(layoutType) || string.IsNullOrWhiteSpace(layoutKey) || string.IsNullOrWhiteSpace(userName))
{
return BadRequest("layoutType, layoutKey und userName sind erforderlich.");
}
var deleted = await _repository.DeleteAsync(layoutType, layoutKey, userName, cancellationToken);
return deleted ? NoContent() : NotFound();
}
private static LayoutDto Map(SmfLayout entity)
{
var layoutData = entity.LayoutData.Length == 0
? string.Empty
: Encoding.UTF8.GetString(entity.LayoutData);
return new LayoutDto
{
LayoutType = entity.LayoutType,
LayoutKey = entity.LayoutKey,
UserName = entity.UserName,
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

@@ -0,0 +1,58 @@
using DbFirst.Application.MassData;
using DbFirst.Application.MassData.Commands;
using DbFirst.Application.MassData.Queries;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace DbFirst.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class MassDataController : ControllerBase
{
private readonly IMediator _mediator;
public MassDataController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet("count")]
public async Task<ActionResult<int>> GetCount(CancellationToken cancellationToken)
{
var count = await _mediator.Send(new GetMassDataCountQuery(), cancellationToken);
return Ok(count);
}
[HttpGet]
public async Task<ActionResult<IEnumerable<MassDataReadDto>>> GetAll([FromQuery] int? skip, [FromQuery] int? take, CancellationToken cancellationToken)
{
int? resolvedTake = take;
if (resolvedTake is <= 0)
{
resolvedTake = null;
}
var result = await _mediator.Send(new GetAllMassDataQuery(skip, resolvedTake), cancellationToken);
return Ok(result);
}
[HttpGet("{customerName}")]
public async Task<ActionResult<MassDataReadDto>> GetByCustomerName(string customerName, CancellationToken cancellationToken)
{
var result = await _mediator.Send(new GetMassDataByCustomerNameQuery(customerName), cancellationToken);
if (result == null)
{
return NotFound();
}
return Ok(result);
}
[HttpPost("upsert")]
public async Task<ActionResult<MassDataReadDto>> Upsert(MassDataWriteDto dto, CancellationToken cancellationToken)
{
var result = await _mediator.Send(new UpsertMassDataByCustomerNameCommand(dto), cancellationToken);
return Ok(result);
}
}

View File

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

View File

@@ -0,0 +1,19 @@
using DbFirst.API.Hubs;
using Microsoft.AspNetCore.SignalR;
namespace DbFirst.API.Dashboards;
public class DashboardChangeNotifier : IDashboardChangeNotifier
{
private readonly IHubContext<DashboardsHub> _hubContext;
public DashboardChangeNotifier(IHubContext<DashboardsHub> hubContext)
{
_hubContext = hubContext;
}
public void NotifyChanged()
{
_ = _hubContext.Clients.All.SendAsync("DashboardsChanged");
}
}

View File

@@ -0,0 +1,6 @@
namespace DbFirst.API.Dashboards;
public interface IDashboardChangeNotifier
{
void NotifyChanged();
}

View File

@@ -11,12 +11,14 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
private readonly string _connectionString;
private readonly string _tableName;
private readonly Func<string?>? _userProvider;
private readonly IDashboardChangeNotifier? _notifier;
public SqlDashboardStorage(string connectionString, string tableName = "TBDD_SMF_CONFIG", Func<string?>? userProvider = null)
public SqlDashboardStorage(string connectionString, string tableName = "TBDD_SMF_CONFIG", Func<string?>? userProvider = null, IDashboardChangeNotifier? notifier = null)
{
_connectionString = connectionString;
_tableName = tableName;
_userProvider = userProvider;
_notifier = notifier;
}
public IEnumerable<DashboardInfo> GetAvailableDashboardsInfo()
@@ -98,6 +100,7 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
connection.Open();
command.ExecuteNonQuery();
_notifier?.NotifyChanged();
return id;
}
@@ -118,6 +121,8 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
{
throw new ArgumentException($"Dashboard '{dashboardId}' not found.");
}
_notifier?.NotifyChanged();
}
public void DeleteDashboard(string dashboardId)
@@ -128,5 +133,6 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
connection.Open();
command.ExecuteNonQuery();
_notifier?.NotifyChanged();
}
}

View File

@@ -0,0 +1,7 @@
using Microsoft.AspNetCore.SignalR;
namespace DbFirst.API.Hubs;
public class DashboardsHub : Hub
{
}

View File

@@ -1,5 +1,6 @@
using DbFirst.API.Middleware;
using DbFirst.API.Dashboards;
using DbFirst.API.Hubs;
using DbFirst.Application;
using DbFirst.Application.Repositories;
using DbFirst.Domain;
@@ -51,8 +52,13 @@ 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.AddScoped<ITimeRepository, TimeRepository>();
builder.Services.AddDevExpressControls();
builder.Services.AddSignalR();
builder.Services.AddSingleton<IDashboardChangeNotifier, DashboardChangeNotifier>();
builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvider) => {
var dashboardsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "Dashboards");
Directory.CreateDirectory(dashboardsPath);
@@ -112,7 +118,8 @@ builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvi
DashboardConfigurator configurator = new DashboardConfigurator();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? string.Empty;
var dashboardStorage = new SqlDashboardStorage(connectionString, "TBDD_SMF_CONFIG");
var notifier = serviceProvider.GetRequiredService<IDashboardChangeNotifier>();
var dashboardStorage = new SqlDashboardStorage(connectionString, "TBDD_SMF_CONFIG", notifier: notifier);
configurator.SetDashboardStorage(dashboardStorage);
DataSourceInMemoryStorage dataSourceStorage = new DataSourceInMemoryStorage();
@@ -155,7 +162,7 @@ app.UseCors();
app.UseAuthorization();
app.MapDashboardRoute("api/dashboard", "DefaultDashboard");
app.MapHub<DashboardsHub>("/hubs/dashboards");
app.MapControllers();
app.Run();

View File

@@ -1,6 +1,7 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;"
"DefaultConnection": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;",
"MassDataConnection": "Server=SDD-VMP04-SQL19\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;"
},
"Dashboard": {
"BaseUrl": "https://localhost:7204"

View File

@@ -0,0 +1,5 @@
using MediatR;
namespace DbFirst.Application.MassData.Commands;
public record UpsertMassDataByCustomerNameCommand(MassDataWriteDto Dto) : IRequest<MassDataReadDto>;

View File

@@ -0,0 +1,24 @@
using AutoMapper;
using DbFirst.Application.Repositories;
using MediatR;
namespace DbFirst.Application.MassData.Commands;
public class UpsertMassDataByCustomerNameHandler : IRequestHandler<UpsertMassDataByCustomerNameCommand, MassDataReadDto>
{
private readonly IMassDataRepository _repository;
private readonly IMapper _mapper;
public UpsertMassDataByCustomerNameHandler(IMassDataRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<MassDataReadDto> Handle(UpsertMassDataByCustomerNameCommand request, CancellationToken cancellationToken)
{
var dto = request.Dto;
var updated = await _repository.UpsertByCustomerNameAsync(dto.CustomerName, dto.Amount, dto.StatusFlag, dto.Category, cancellationToken);
return _mapper.Map<MassDataReadDto>(updated);
}
}

View File

@@ -0,0 +1,12 @@
using AutoMapper;
using DbFirst.Domain.Entities;
namespace DbFirst.Application.MassData;
public class MassDataProfile : Profile
{
public MassDataProfile()
{
CreateMap<Massdata, MassDataReadDto>();
}
}

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
using AutoMapper;
using DbFirst.Application.Repositories;
using MediatR;
namespace DbFirst.Application.MassData.Queries;
public class GetAllMassDataHandler : IRequestHandler<GetAllMassDataQuery, List<MassDataReadDto>>
{
private readonly IMassDataRepository _repository;
private readonly IMapper _mapper;
public GetAllMassDataHandler(IMassDataRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<List<MassDataReadDto>> Handle(GetAllMassDataQuery request, CancellationToken cancellationToken)
{
var items = await _repository.GetAllAsync(request.Skip, request.Take, cancellationToken);
return _mapper.Map<List<MassDataReadDto>>(items);
}
}

View File

@@ -0,0 +1,5 @@
using MediatR;
namespace DbFirst.Application.MassData.Queries;
public record GetAllMassDataQuery(int? Skip, int? Take) : IRequest<List<MassDataReadDto>>;

View File

@@ -0,0 +1,23 @@
using AutoMapper;
using DbFirst.Application.Repositories;
using MediatR;
namespace DbFirst.Application.MassData.Queries;
public class GetMassDataByCustomerNameHandler : IRequestHandler<GetMassDataByCustomerNameQuery, MassDataReadDto?>
{
private readonly IMassDataRepository _repository;
private readonly IMapper _mapper;
public GetMassDataByCustomerNameHandler(IMassDataRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<MassDataReadDto?> Handle(GetMassDataByCustomerNameQuery request, CancellationToken cancellationToken)
{
var item = await _repository.GetByCustomerNameAsync(request.CustomerName, cancellationToken);
return item == null ? null : _mapper.Map<MassDataReadDto>(item);
}
}

View File

@@ -0,0 +1,5 @@
using MediatR;
namespace DbFirst.Application.MassData.Queries;
public record GetMassDataByCustomerNameQuery(string CustomerName) : IRequest<MassDataReadDto?>;

View File

@@ -0,0 +1,19 @@
using DbFirst.Application.Repositories;
using MediatR;
namespace DbFirst.Application.MassData.Queries;
public class GetMassDataCountHandler : IRequestHandler<GetMassDataCountQuery, int>
{
private readonly IMassDataRepository _repository;
public GetMassDataCountHandler(IMassDataRepository repository)
{
_repository = repository;
}
public async Task<int> Handle(GetMassDataCountQuery request, CancellationToken cancellationToken)
{
return await _repository.GetCountAsync(cancellationToken);
}
}

View File

@@ -0,0 +1,5 @@
using MediatR;
namespace DbFirst.Application.MassData.Queries;
public record GetMassDataCountQuery : IRequest<int>;

View File

@@ -0,0 +1,10 @@
using DbFirst.Domain.Entities;
namespace DbFirst.Application.Repositories;
public interface ILayoutRepository
{
Task<SmfLayout?> GetAsync(string layoutType, string layoutKey, string userName, CancellationToken cancellationToken = default);
Task<SmfLayout> UpsertAsync(string layoutType, string layoutKey, string userName, byte[] layoutData, CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(string layoutType, string layoutKey, string userName, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,11 @@
using DbFirst.Domain.Entities;
namespace DbFirst.Application.Repositories;
public interface IMassDataRepository
{
Task<int> GetCountAsync(CancellationToken cancellationToken = default);
Task<List<Massdata>> GetAllAsync(int? skip = null, int? take = null, CancellationToken cancellationToken = default);
Task<Massdata?> GetByCustomerNameAsync(string customerName, CancellationToken cancellationToken = default);
Task<Massdata> UpsertByCustomerNameAsync(string customerName, decimal amount, bool statusFlag, string category, CancellationToken cancellationToken = default);
}

View File

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

View File

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

View File

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

View File

@@ -1,60 +0,0 @@
/*
Ablauf und die Rolle jeder Datei in der Blazor WebAssembly-Anwendung:
1.index.html
• Reihenfolge: Wird als erstes geladen, wenn die Anwendung im Browser geöffnet wird.
• Purpose:
• Lädt die Blazor WebAssembly-Umgebung (blazor.webassembly.js).
• Definiert den Platzhalter <div id="app">, in dem die Blazor-Komponenten gerendert werden.
• Bindet Stylesheets und Skripte ein, die für das Styling und die Funktionalität der Anwendung benötigt werden.
2. Program.cs
• Reihenfolge: Wird nach index.html ausgeführt, sobald die Blazor-Umgebung initialisiert ist.
• Purpose:
• Initialisiert die Blazor WebAssembly-Anwendung.
• Registriert Root-Komponenten (App) und Abhängigkeiten (z. B. HttpClient, CatalogApiClient).
• Konfiguriert die Basis-URL für API-Aufrufe.
3. App.razor
• Reihenfolge: Wird als nächstes geladen, nachdem die Anwendung initialisiert wurde.
• Purpose:
• Definiert die Routing-Logik der Anwendung.
• Entscheidet, welche Komponente basierend auf der URL gerendert wird.
• Stellt sicher, dass ein Standardlayout (MainLayout) verwendet wird.
4.MainLayout.razor
• Reihenfolge: Wird geladen, wenn eine Seite gerendert wird, da es das Standardlayout ist.
• Purpose:
• Definiert das Hauptlayout der Anwendung.
• Enthält die Navigationsleiste (NavMenu) und den Platzhalter für den Seiteninhalt (@Body).
5. NavMenu.razor
• Reihenfolge: Wird als Teil des Layouts (MainLayout) geladen.
• Purpose:
• Stellt die Navigationsleiste bereit.
• Enthält Links zu verschiedenen Seiten der Anwendung (z. B. Home, Catalogs).
• Ermöglicht das Ein- und Ausklappen des Menüs.
6. Catalogs.razor
• Reihenfolge: Wird geladen, wenn der Benutzer die URL /catalogs aufruft.
• Purpose:
• Stellt die Benutzeroberfläche für die Verwaltung von Katalogen bereit.
• Nutzt CatalogApiClient, um Daten von der API zu laden, zu erstellen, zu aktualisieren oder zu löschen.
• Verwendet DevExpress-Komponenten für ein modernes UI.
7. CatalogApiClient.cs
• Reihenfolge: Wird verwendet, wenn Catalogs.razor API-Aufrufe ausführt.
• Purpose:
• Kapselt die Kommunikation mit der API.
• Bietet Methoden für CRUD-Operationen (Create, Read, Update, Delete) auf Katalog-Daten.
• Behandelt Fehler und gibt benutzerfreundliche Fehlermeldungen zurück.
Zusammenfassung des Ablaufs:
1.index.html: Lädt die Blazor-Umgebung und startet die Anwendung.
2. Program.cs: Initialisiert die Anwendung und registriert Abhängigkeiten.
3. App.razor: Definiert die Routing-Logik und lädt das Standardlayout.
4. MainLayout.razor: Stellt das Hauptlayout bereit.
5. NavMenu.razor: Lädt die Navigationsleiste.
6. Seiten wie Catalogs.razor: Werden basierend auf der URL gerendert.
7. CatalogApiClient.cs: Führt API-Aufrufe aus, wenn die Seite Daten benötigt.
*/

View File

@@ -1,20 +0,0 @@
@*
• Ist der logische Einstiegspunkt der Blazor-Anwendung.
• Sie definiert die Routing-Logik und das Standardlayout der Anwendung.
• Der Router-Komponent in App.razor entscheidet, welche Blazor-Komponente basierend auf der URL geladen wird.
kurz: Steuert die Navigation und das Rendering der Blazor-Komponenten.
*@
@DxResourceManager.RegisterTheme(Themes.Fluent)
@DxResourceManager.RegisterScripts()
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@@ -1,296 +0,0 @@
@inject CatalogApiClient Api
<style>
.action-panel { margin-bottom: 16px; }
.grid-section { margin-top: 12px; }
.catalog-grid .dxbl-grid-sort-asc,
.catalog-grid .dxbl-grid-sort-desc {
display: none;
}
.catalog-grid th.dxbl-grid-header-sortable {
position: relative;
padding-right: 1.5rem;
}
.catalog-grid th.dxbl-grid-header-sortable::before,
.catalog-grid th.dxbl-grid-header-sortable::after {
content: "";
position: absolute;
right: 0.45rem;
width: 0.7rem;
height: 0.7rem;
background-repeat: no-repeat;
background-size: 0.7rem 0.7rem;
opacity: 0.35;
pointer-events: none;
}
.catalog-grid th.dxbl-grid-header-sortable::before {
top: 38%;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.957 10.999a1 1 0 0 1-.821-1.571l2.633-3.785a1.5 1.5 0 0 1 2.462 0l2.633 3.785a1 1 0 0 1-.821 1.57H4.957Z' fill='%23888888'/%3E%3C/svg%3E");
}
.catalog-grid th.dxbl-grid-header-sortable::after {
top: 58%;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.957 5a1 1 0 0 0-.821 1.571l2.633 3.784a1.5 1.5 0 0 0 2.462 0l2.633-3.784A1 1 0 0 0 11.043 5H4.957Z' fill='%23888888'/%3E%3C/svg%3E");
}
.catalog-grid th.dxbl-grid-header-sortable[aria-sort="ascending"]::after {
opacity: 0;
}
.catalog-grid th.dxbl-grid-header-sortable[aria-sort="descending"]::before {
opacity: 0;
}
.catalog-grid .filter-search-input input {
padding-right: 1.75rem;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M9.309 10.016a4.5 4.5 0 1 1 .707-.707l3.838 3.837a.5.5 0 0 1-.708.708L9.31 10.016ZM10 6.5a3.5 3.5 0 1 0-7 0 3.5 3.5 0 0 0 7 0Z' fill='%23666666'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.5rem center;
background-size: 0.9rem;
}
</style>
@if (!string.IsNullOrWhiteSpace(errorMessage))
{
<div class="alert alert-danger" role="alert">@errorMessage</div>
}
else if (!string.IsNullOrWhiteSpace(infoMessage))
{
<div class="alert alert-success" role="alert">@infoMessage</div>
}
<div class="mb-3">
<DxButton RenderStyle="ButtonRenderStyle.Primary" Click="@StartCreate">Neuen Eintrag anlegen</DxButton>
</div>
@if (showForm)
{
<div class="action-panel">
<EditForm Model="formModel" OnValidSubmit="HandleSubmit" Context="editCtx">
<DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="Titel" Context="itemCtx">
<DxTextBox @bind-Text="formModel.CatTitle" Enabled="@(isEditing ? formModel.UpdateProcedure != 0 : true)" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Kennung" Context="itemCtx">
<DxTextBox @bind-Text="formModel.CatString" />
</DxFormLayoutItem>
@if (isEditing)
{
<DxFormLayoutItem Caption="Update-Prozedur" Context="itemCtx">
<DxComboBox Data="@procedureOptions"
TextFieldName="Text"
ValueFieldName="Value"
@bind-Value="formModel.UpdateProcedure" />
</DxFormLayoutItem>
}
<DxFormLayoutItem Caption=" " Context="itemCtx">
<DxStack Orientation="Orientation.Horizontal" Spacing="8">
<DxButton RenderStyle="ButtonRenderStyle.Success" ButtonType="ButtonType.Submit" SubmitFormOnClick="true" Context="btnCtx">@((isEditing ? "Speichern" : "Anlegen"))</DxButton>
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Click="@CancelEdit" Context="btnCtx">Abbrechen</DxButton>
</DxStack>
</DxFormLayoutItem>
</DxFormLayout>
</EditForm>
</div>
}
@if (isLoading)
{
<p><em>Lade Daten...</em></p>
}
else if (items.Count == 0)
{
<p>Keine Einträge vorhanden.</p>
}
else
{
<div class="grid-section">
<DxGrid Data="@items" TItem="CatalogReadDto" KeyFieldName="@nameof(CatalogReadDto.Guid)" ShowFilterRow="true" PageSize="10" CssClass="mb-4 catalog-grid">
<Columns>
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.Guid)" Caption="Id" Width="140px" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
TextChanged="@(value => filter.FilterRowValue = value)"
CssClass="filter-search-input" />
</FilterRowCellTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatTitle)" Caption="Titel">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue as string)"
TextChanged="@(value => filter.FilterRowValue = value)"
CssClass="filter-search-input" />
</FilterRowCellTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatString)" Caption="String">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue as string)"
TextChanged="@(value => filter.FilterRowValue = value)"
CssClass="filter-search-input" />
</FilterRowCellTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue as string)"
TextChanged="@(value => filter.FilterRowValue = value)"
CssClass="filter-search-input" />
</FilterRowCellTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue as string)"
TextChanged="@(value => filter.FilterRowValue = value)"
CssClass="filter-search-input" />
</FilterRowCellTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" />
<DxGridDataColumn Caption="" Width="220px" AllowSort="false">
<CellDisplayTemplate Context="cell">
@{ var item = (CatalogReadDto)cell.DataItem; }
<div style="white-space: nowrap;">
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Size="ButtonSize.Small" Click="@(() => StartEdit(item))">Bearbeiten</DxButton>
<DxButton RenderStyle="ButtonRenderStyle.Danger" Size="ButtonSize.Small" Click="@(() => DeleteCatalog(item.Guid))">Löschen</DxButton>
</div>
</CellDisplayTemplate>
</DxGridDataColumn>
</Columns>
</DxGrid>
</div>
}
@code {
private List<CatalogReadDto> items = new();
private CatalogWriteDto formModel = new();
private int editingId;
private bool isLoading;
private bool isEditing;
private bool showForm;
private string? errorMessage;
private string? infoMessage;
private readonly List<ProcedureOption> procedureOptions = new()
{
new() { Value = 0, Text = "PRTBMY_CATALOG_UPDATE" },
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
};
protected override async Task OnInitializedAsync()
{
await LoadCatalogs();
}
private async Task LoadCatalogs()
{
isLoading = true;
errorMessage = null;
try
{
items = await Api.GetAllAsync();
}
catch (Exception ex)
{
errorMessage = $"Kataloge konnten nicht geladen werden: {ex.Message}";
}
finally
{
isLoading = false;
StateHasChanged();
}
}
private void StartCreate()
{
formModel = new CatalogWriteDto();
editingId = 0;
isEditing = false;
showForm = true;
infoMessage = null;
errorMessage = null;
}
private void StartEdit(CatalogReadDto item)
{
formModel = new CatalogWriteDto
{
CatTitle = item.CatTitle,
CatString = item.CatString,
UpdateProcedure = 0
};
editingId = item.Guid;
isEditing = true;
showForm = true;
infoMessage = null;
errorMessage = null;
}
private async Task HandleSubmit()
{
errorMessage = null;
infoMessage = null;
try
{
if (isEditing)
{
var updated = await Api.UpdateAsync(editingId, formModel);
if (!updated.Success)
{
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
return;
}
infoMessage = "Katalog aktualisiert.";
}
else
{
var created = await Api.CreateAsync(formModel);
if (!created.Success || created.Value == null)
{
errorMessage = created.Error ?? "Anlegen fehlgeschlagen.";
return;
}
infoMessage = "Katalog angelegt.";
}
showForm = false;
await LoadCatalogs();
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Speichern: {ex.Message}";
}
}
private void CancelEdit()
{
showForm = false;
infoMessage = null;
errorMessage = null;
}
private async Task DeleteCatalog(int id)
{
errorMessage = null;
infoMessage = null;
try
{
var deleted = await Api.DeleteAsync(id);
if (!deleted.Success)
{
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
return;
}
infoMessage = "Katalog gelöscht.";
await LoadCatalogs();
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Löschen: {ex.Message}";
}
}
private sealed class ProcedureOption
{
public int Value { get; set; }
public string Text { get; set; } = string.Empty;
}
}

View File

@@ -1,22 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DevExpress.Blazor" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Dashboard" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Themes" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.22" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.22" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\sample-data\" />
</ItemGroup>
</Project>

View File

@@ -1,19 +0,0 @@
@* Definiert das Hauptlayout der Anwendung.
Enthält die Navigationsleiste und den Hauptinhalt. *@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu /> <!-- Einbindung der Navigationsleiste -->
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body <!-- Platzhalter für den Seiteninhalt -->
</article>
</main>
</div>

View File

@@ -1,77 +0,0 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}

View File

@@ -1,43 +0,0 @@
@* Definiert die Navigationsleiste, die Links zu verschiedenen Seiten der Anwendung enthält. *@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">DbFirst.BlazorWasm</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="catalogs">
<span class="bi bi-collection-nav-menu" aria-hidden="true"></span> Catalogs
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="dashboards/default">
<span class="oi oi-list-rich" aria-hidden="true"></span> Dashboards
</NavLink>
</div>
</nav>
</div>
@code {
private bool collapseNavMenu = true;
// CSS-Klasse für die Navigation, die den Zustand (eingeklappt/ausgeklappt) steuert.
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
// Methode zum Umschalten des Navigationsmenüs.
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}

View File

@@ -1,83 +0,0 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
.nav-scrollable {
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
@page "/catalogs"
<PageTitle>Catalogs</PageTitle>
<h1>Catalogs</h1>
<CatalogsGrid />

View File

@@ -1,71 +0,0 @@
@page "/dashboard"
@page "/dashboards/{DashboardId?}"
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
@inject NavigationManager Navigation
<style>
.dashboard-shell {
display: flex;
gap: 0;
min-height: 800px;
border: 1px solid #e6e6e6;
border-radius: 6px;
overflow: hidden;
background: #fff;
}
.dashboard-nav {
width: 220px;
border-right: 1px solid #e6e6e6;
background: #fafafa;
}
.dashboard-nav-title {
padding: 0.75rem 1rem 0.5rem;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #6c757d;
font-weight: 600;
}
.dashboard-nav-link {
display: block;
padding: 0.55rem 1rem;
color: inherit;
text-decoration: none;
}
.dashboard-nav-link.active {
background: #e9ecef;
font-weight: 600;
}
.dashboard-content {
flex: 1;
min-width: 0;
padding: 1rem;
}
</style>
<PageTitle>Dashboards</PageTitle>
<div class="dashboard-shell">
<aside class="dashboard-nav">
<div class="dashboard-nav-title">Dashboards</div>
<NavLink class="dashboard-nav-link" href="dashboards/default">Default Dashboard (Designer)</NavLink>
</aside>
<section class="dashboard-content">
<DxDashboard Endpoint="@DashboardEndpoint" InitialDashboardId="DefaultDashboard" WorkingMode="WorkingMode.Designer" style="width: 100%; height: 800px;">
</DxDashboard>
</section>
</div>
@code {
[Parameter] public string? DashboardId { get; set; }
private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
protected override void OnParametersSet()
{
if (!string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase))
{
Navigation.NavigateTo("dashboards/default", replace: true);
}
}
}

View File

@@ -1,7 +0,0 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Db First approach</h1>
This is a Blazor WebAssembly application demonstrating the Database First approach using DevExpress components.

View File

@@ -1,21 +0,0 @@
/* Initialisiert die Blazor WebAssembly-Anwendung.
Registriert Root-Komponenten
Konfiguriert Abhängigkeiten */
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using DbFirst.BlazorWasm;
using DbFirst.BlazorWasm.Services;
using DevExpress.Blazor;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddDevExpressBlazor();
var apiBaseUrl = builder.Configuration["ApiBaseUrl"] ?? builder.HostEnvironment.BaseAddress;
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiBaseUrl) });
builder.Services.AddScoped<CatalogApiClient>();
await builder.Build().RunAsync();

View File

@@ -1,41 +0,0 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:12804",
"sslPort": 44394
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5101",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7276;http://localhost:5101",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -1,148 +0,0 @@
/* Kapselt die Kommunikation mit der API für den Catalog-Endpunkt.
Bietet Methoden für CRUD-Operationen auf Catalog-Daten */
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using DbFirst.BlazorWasm.Models;
namespace DbFirst.BlazorWasm.Services;
public class CatalogApiClient
{
private readonly HttpClient _httpClient;
private const string Endpoint = "api/catalogs";
public CatalogApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<List<CatalogReadDto>> GetAllAsync()
{
var result = await _httpClient.GetFromJsonAsync<List<CatalogReadDto>>(Endpoint);
return result ?? new List<CatalogReadDto>();
}
public async Task<CatalogReadDto?> GetByIdAsync(int id)
{
return await _httpClient.GetFromJsonAsync<CatalogReadDto>($"{Endpoint}/{id}");
}
public async Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto)
{
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
if (response.IsSuccessStatusCode)
{
var payload = await response.Content.ReadFromJsonAsync<CatalogReadDto>();
return ApiResult<CatalogReadDto?>.Ok(payload);
}
var error = await ReadErrorAsync(response);
return ApiResult<CatalogReadDto?>.Fail(error);
}
public async Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto)
{
var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto);
if (response.IsSuccessStatusCode)
{
return ApiResult<bool>.Ok(true);
}
var error = await ReadErrorAsync(response);
return ApiResult<bool>.Fail(error);
}
public async Task<ApiResult<bool>> DeleteAsync(int id)
{
var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}");
if (response.IsSuccessStatusCode)
{
return ApiResult<bool>.Ok(true);
}
var error = await ReadErrorAsync(response);
return ApiResult<bool>.Fail(error);
}
private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
{
// Liest und analysiert Fehlerdetails aus der API-Antwort.
// Gibt eine benutzerfreundliche Fehlermeldung zurück.
string? problemTitle = null;
string? problemDetail = null;
try
{
var problem = await response.Content.ReadFromJsonAsync<ProblemDetailsDto>();
if (problem != null)
{
problemTitle = problem.Title;
problemDetail = problem.Detail ?? problem.Type;
}
}
catch
{
// ignore parse errors
}
var status = response.StatusCode;
var reason = response.ReasonPhrase;
var body = await response.Content.ReadAsStringAsync();
string detail = problemDetail;
if (string.IsNullOrWhiteSpace(detail) && !string.IsNullOrWhiteSpace(body))
{
detail = body;
}
// Friendly overrides
if (status == HttpStatusCode.Conflict)
{
return "Datensatz existiert bereits. Bitte wählen Sie einen anderen Titel.";
}
if (status == HttpStatusCode.BadRequest && (detail?.Contains("CatTitle cannot be changed", StringComparison.OrdinalIgnoreCase) ?? false))
{
return "Titel kann nicht geändert werden.";
}
return status switch
{
HttpStatusCode.BadRequest => $"Eingabe ungültig{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.NotFound => $"Nicht gefunden{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Conflict => $"Konflikt{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Unauthorized => $"Nicht autorisiert{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Forbidden => $"Nicht erlaubt{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.InternalServerError => $"Serverfehler{FormatSuffix(problemTitle, detail, reason)}",
_ => $"Fehler {(int)status} {reason ?? string.Empty}{FormatSuffix(problemTitle, detail, reason)}"
};
}
private static string FormatSuffix(string? title, string? detail, string? reason)
{
// Formatiert zusätzliche Informationen für Fehlermeldungen.
// Kombiniert Titel, Details und Grund in einer lesbaren Form.
var parts = new List<string>();
if (!string.IsNullOrWhiteSpace(title)) parts.Add(title);
if (!string.IsNullOrWhiteSpace(detail)) parts.Add(detail);
if (parts.Count == 0 && !string.IsNullOrWhiteSpace(reason)) parts.Add(reason);
if (parts.Count == 0) return string.Empty;
return ": " + string.Join(" | ", parts);
}
}
public record ApiResult<T>(bool Success, T? Value, string? Error)
{
public static ApiResult<T> Ok(T? value) => new(true, value, null);
public static ApiResult<T> Fail(string? error) => new(false, default, error);
}
internal sealed class ProblemDetailsDto
{
public string? Type { get; set; }
public string? Title { get; set; }
public string? Detail { get; set; }
}

View File

@@ -1,16 +0,0 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using DbFirst.BlazorWasm
@using DbFirst.BlazorWasm.Layout
@using DbFirst.BlazorWasm.Models
@using DbFirst.BlazorWasm.Services
@using DbFirst.BlazorWasm.Components
@using DevExpress.Blazor
@using DevExpress.DashboardBlazor
@using DevExpress.DashboardWeb

View File

@@ -1,3 +0,0 @@
{
"ApiBaseUrl": "https://localhost:7204/"
}

View File

@@ -1,103 +0,0 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
h1:focus {
outline: none;
}
a, .btn-link {
color: #0071c1;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid red;
}
.validation-message {
color: red;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.loading-progress {
position: relative;
display: block;
width: 8rem;
height: 8rem;
margin: 20vh auto 1rem auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}
code {
color: #c02d76;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,58 +0,0 @@
<!--
• Ist der technische Einstiegspunkt der Blazor WebAssembly-Anwendung.
• Sie lädt die notwendigen Ressourcen (z. B. das Blazor-Skript blazor.webassembly.js)
und definiert den Platzhalter <div id="app">, in dem die Blazor-Komponenten gerendert werden.
• Ohne diese Datei könnte die Blazor-Anwendung nicht starten, da sie die Verbindung
zwischen der statischen HTML-Welt und der Blazor-Welt herstellt.
kurz: Startet die Anwendung und lädt die Blazor-Umgebung.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DbFirst.BlazorWasm</title>
<base href="/" />
<!-- Stylesheets für DevExpress und Bootstrap -->
<link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/ace-theme-dreamweaver.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/ace-theme-ambiance.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/dx.light.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/dx-analytics.common.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/dx-analytics.light.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/dx-querybuilder.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/dx-dashboard.light.min.css" rel="stylesheet" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<link href="DbFirst.BlazorWasm.styles.css" rel="stylesheet" />
</head>
<body>
<!-- Einstiegspunkt der Blazor-Anwendung -->
<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
</div>
<!-- Fehler-UI für unvorhergesehene Fehler -->
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<!-- Blazor WebAssembly-Skript -->
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@@ -5,7 +5,10 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
@DxResourceManager.RegisterTheme(Themes.Fluent)
@DxResourceManager.RegisterTheme(Themes.Fluent.Clone(properties =>
{
properties.ApplyToPageElements = true;
}))
@DxResourceManager.RegisterScripts()
<link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" />
@@ -17,17 +20,12 @@
<link href="_content/DevExpress.Blazor.Dashboard/dx-querybuilder.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/dx-dashboard.light.min.css" rel="stylesheet" />
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes/bootstrap-external.bs5.min.css" />
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes.Fluent/core.min.css" />
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes.Fluent/global.min.css" />
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes.Fluent/modes/light.min.css" />
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes.Fluent/accents/blue.min.css" />
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes.Fluent/bootstrap/fluent-light.bs5.min.css" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="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>
<HeadOutlet />
</head>

View File

@@ -1,9 +1,5 @@
@inject CatalogApiClient Api
<style>
.action-panel { margin-bottom: 16px; }
.grid-section { margin-top: 12px; }
</style>
@inject BandLayoutService BandLayoutService
@if (!string.IsNullOrWhiteSpace(errorMessage))
{
@@ -14,84 +10,167 @@ else if (!string.IsNullOrWhiteSpace(infoMessage))
<div class="alert alert-success" role="alert">@infoMessage</div>
}
<div class="mb-3">
<DxButton RenderStyle="ButtonRenderStyle.Primary" Click="@StartCreate">Neuen Eintrag anlegen</DxButton>
</div>
@if (showForm)
@if (!hasLoaded || isLoading)
{
<div class="action-panel">
<EditForm Model="formModel" OnValidSubmit="HandleSubmit" Context="editCtx">
<DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="Titel" Context="itemCtx">
<DxTextBox @bind-Text="formModel.CatTitle" Enabled="@(isEditing ? formModel.UpdateProcedure != 0 : true)" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Kennung" Context="itemCtx">
<DxTextBox @bind-Text="formModel.CatString" />
</DxFormLayoutItem>
@if (isEditing)
{
<DxFormLayoutItem Caption="Update-Prozedur" Context="itemCtx">
<DxComboBox Data="@procedureOptions"
TextFieldName="Text"
ValueFieldName="Value"
@bind-Value="formModel.UpdateProcedure" />
</DxFormLayoutItem>
}
<DxFormLayoutItem Caption=" " Context="itemCtx">
<DxStack Orientation="Orientation.Horizontal" Spacing="8">
<DxButton RenderStyle="ButtonRenderStyle.Success" ButtonType="ButtonType.Submit" SubmitFormOnClick="true" Context="btnCtx">@((isEditing ? "Speichern" : "Anlegen"))</DxButton>
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Click="@CancelEdit" Context="btnCtx">Abbrechen</DxButton>
</DxStack>
</DxFormLayoutItem>
</DxFormLayout>
</EditForm>
<div class="loading-container">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Lade...</span>
</div>
</div>
}
@if (isLoading)
{
<p><em>Lade Daten...</em></p>
}
else if (items.Count == 0)
{
<p>Keine Einträge vorhanden.</p>
}
else
{
<div class="band-editor">
<button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded">
<span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">&#9658;</span>
<span>Layout</span>
</button>
@if (bandEditorExpanded)
{
<div class="band-editor-body">
<div class="band-controls">
<DxButton Text="Band hinzufügen" Click="AddBand" />
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
</div>
@foreach (var band in bandLayout.Bands)
{
<div class="band-row">
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
</div>
}
<DxFormLayout CssClass="band-columns" ColCount="2">
@foreach (var column in columnDefinitions)
{
<DxFormLayoutItem Caption="@column.Caption">
<DxComboBox Data="@bandOptions"
TData="BandOption"
TValue="string"
TextFieldName="Caption"
ValueFieldName="Id"
Value="@GetColumnBand(column.FieldName)"
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
Width="100%" />
</DxFormLayoutItem>
}
</DxFormLayout>
</div>
}
</div>
<div class="grid-section">
<DxGrid Data="@items" TItem="CatalogReadDto" KeyFieldName="@nameof(CatalogReadDto.Guid)" ShowFilterRow="true" PageSize="10" CssClass="mb-4 catalog-grid">
<DxGrid Data="@items"
TItem="CatalogReadDto"
KeyFieldName="@nameof(CatalogReadDto.Guid)"
SizeMode="@_sizeMode"
ShowGroupPanel="true"
ShowGroupedColumns="true"
AllowGroup="true"
FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always"
AllowColumnResize="true"
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
AllowColumnReorder="true"
PageSize="10"
CssClass="mb-4 catalog-grid"
EditMode="GridEditMode.PopupEditForm"
PopupEditFormCssClass="catalog-edit-popup"
PopupEditFormHeaderText="@popupHeaderText"
CustomizeEditModel="OnCustomizeEditModel"
EditModelSaving="OnEditModelSaving"
DataItemDeleting="OnDataItemDeleting"
FocusedRowEnabled="true"
@bind-FocusedRowKey="focusedRowKey"
@ref="gridRef">
<ToolbarTemplate>
<DxToolbar>
<DxToolbarItem Alignment="ToolbarItemAlignment.Right">
<Template Context="_">
<DxDropDownButton Text="@FormatSizeText(_sizeMode)"
RenderStyle="ButtonRenderStyle.Secondary"
RenderStyleMode="ButtonRenderStyleMode.Text"
ItemClick="OnSizeChange">
<Items>
@foreach (var size in _sizeModes)
{
<DxDropDownButtonItem Text="@FormatSizeText(size)" Id="@size.ToString()" />
}
</Items>
</DxDropDownButton>
</Template>
</DxToolbarItem>
</DxToolbar>
</ToolbarTemplate>
<Columns>
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.Guid)" Caption="Id" Width="140px" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatTitle)" Caption="Titel" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatString)" Caption="String" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" />
<DxGridDataColumn Caption="" Width="220px" AllowSort="false">
<CellDisplayTemplate Context="cell">
@{ var item = (CatalogReadDto)cell.DataItem; }
<div style="white-space: nowrap;">
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Size="ButtonSize.Small" Click="@(() => StartEdit(item))">Bearbeiten</DxButton>
<DxButton RenderStyle="ButtonRenderStyle.Danger" Size="ButtonSize.Small" Click="@(() => DeleteCatalog(item.Guid))">Löschen</DxButton>
</div>
</CellDisplayTemplate>
</DxGridDataColumn>
@RenderColumns()
</Columns>
<EditFormTemplate Context="editFormContext">
@{
SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew);
}
<DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="Titel">
<DxTextBox @bind-Text="editModel.CatTitle" Width="100%" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Kennung">
<DxTextBox @bind-Text="editModel.CatString" Width="100%" />
</DxFormLayoutItem>
@if (!editModel.IsNew)
{
<DxFormLayoutItem Caption="Update-Prozedur">
<DxComboBox Data="@procedureOptions"
TData="ProcedureOption"
TValue="int"
TextFieldName="Text"
ValueFieldName="Value"
@bind-Value="editModel.UpdateProcedure"
Width="100%" />
</DxFormLayoutItem>
}
<DxFormLayoutItem ColSpanMd="12">
<ValidationSummary />
</DxFormLayoutItem>
</DxFormLayout>
</EditFormTemplate>
</DxGrid>
</div>
}
@code {
private List<CatalogReadDto> items = new();
private CatalogWriteDto formModel = new();
private int editingId;
private bool isLoading;
private bool isEditing;
private bool showForm;
private bool hasLoaded;
private string? errorMessage;
private string? infoMessage;
private EditContext? editContext;
private ValidationMessageStore? validationMessageStore;
private IGrid? gridRef;
private int? focusedRowKey;
private string popupHeaderText = "Edit";
private const string LayoutType = "GRID_BANDS";
private const string LayoutKey = "CatalogsGrid";
private string? layoutUser;
private BandLayout bandLayout = new();
private Dictionary<string, string> columnBandAssignments = new();
private List<BandOption> bandOptions = new();
private Dictionary<string, ColumnDefinition> columnLookup = new();
private bool gridLayoutApplied;
private bool bandEditorExpanded;
private List<ColumnDefinition> columnDefinitions = new()
{
new() { FieldName = nameof(CatalogReadDto.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.CatTitle), Caption = "Titel", FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.CatString), Caption = "String", FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.AddedWho), Caption = "Angelegt von", ReadOnly = true, FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.AddedWhen), Caption = "Angelegt am", ReadOnly = true, FilterType = ColumnFilterType.Date },
new() { FieldName = nameof(CatalogReadDto.ChangedWho), Caption = "Geändert von", ReadOnly = true, FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.ChangedWhen), Caption = "Geändert am", ReadOnly = true, FilterType = ColumnFilterType.Date }
};
private readonly List<ProcedureOption> procedureOptions = new()
{
@@ -99,11 +178,46 @@ else
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
};
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
private SizeMode _sizeMode = SizeMode.Medium;
private static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
private string FormatSizeText(SizeMode size) => size switch
{
SizeMode.Small => "Klein",
SizeMode.Medium => "Mittel",
SizeMode.Large => "Groß",
_ => size.ToString()
};
private void OnSizeChange(DropDownButtonItemClickEventArgs args)
{
_sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
}
protected override async Task OnInitializedAsync()
{
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
ApplyColumnLayoutFromStorage();
_sizeMode = bandLayout.SizeMode;
UpdateBandOptions();
await LoadCatalogs();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
{
gridRef.LoadLayout(bandLayout.GridLayout);
gridLayoutApplied = true;
await InvokeAsync(StateHasChanged);
}
}
private async Task LoadCatalogs()
{
isLoading = true;
@@ -119,107 +233,346 @@ else
finally
{
isLoading = false;
hasLoaded = true;
StateHasChanged();
}
}
private void StartCreate()
private async Task SaveLayoutAsync()
{
formModel = new CatalogWriteDto();
editingId = 0;
isEditing = false;
showForm = true;
infoMessage = null;
errorMessage = null;
}
private void StartEdit(CatalogReadDto item)
{
formModel = new CatalogWriteDto
{
CatTitle = item.CatTitle,
CatString = item.CatString,
UpdateProcedure = 0
};
editingId = item.Guid;
isEditing = true;
showForm = true;
infoMessage = null;
errorMessage = null;
}
private async Task HandleSubmit()
{
errorMessage = null;
infoMessage = null;
if (string.IsNullOrWhiteSpace(layoutUser))
return;
try
{
if (isEditing)
CaptureColumnLayoutFromGrid();
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
infoMessage = "Layout gespeichert.";
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
}
}
private async Task ResetLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
return;
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
bandLayout = new BandLayout();
columnBandAssignments.Clear();
UpdateBandOptions();
foreach (var column in columnDefinitions)
column.Width = null;
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
_sizeMode = SizeMode.Medium;
if (gridRef != null)
gridRef.LoadLayout(new GridPersistentLayout());
gridLayoutApplied = false;
infoMessage = "Layout zurückgesetzt.";
errorMessage = null;
}
private void CaptureColumnLayoutFromGrid()
{
if (gridRef == null)
return;
var layout = gridRef.SaveLayout();
bandLayout.GridLayout = layout;
bandLayout.SizeMode = _sizeMode;
var orderedColumns = layout.Columns
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
.OrderBy(c => c.VisibleIndex)
.ToList();
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
bandLayout.ColumnWidths = orderedColumns
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
}
private void ApplyColumnLayoutFromStorage()
{
foreach (var column in columnDefinitions)
{
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
column.Width = width;
}
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
}
private void AddBand()
{
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
UpdateBandOptions();
}
private void RemoveBand(BandDefinition band)
{
bandLayout.Bands.Remove(band);
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
columnBandAssignments.Remove(key);
UpdateBandOptions();
SyncBandsFromAssignments();
}
private void UpdateBandCaption(BandDefinition band, string value)
{
band.Caption = value;
UpdateBandOptions();
}
private void UpdateColumnBand(string fieldName, string? bandId)
{
if (string.IsNullOrWhiteSpace(bandId))
columnBandAssignments.Remove(fieldName);
else
columnBandAssignments[fieldName] = bandId;
SyncBandsFromAssignments();
}
private string GetColumnBand(string fieldName)
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
private void SyncBandsFromAssignments()
{
foreach (var band in bandLayout.Bands)
{
band.Columns = columnDefinitions
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
.Select(c => c.FieldName)
.ToList();
}
StateHasChanged();
}
private void UpdateBandOptions()
{
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
}
private RenderFragment RenderColumns() => builder =>
{
var seq = 0;
builder.OpenComponent<DxGridCommandColumn>(seq++);
builder.AddAttribute(seq++, "Width", "120px");
builder.CloseComponent();
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
BuildDataColumn(builder, ref seq, column);
foreach (var band in bandLayout.Bands)
{
if (band.Columns.Count == 0) continue;
builder.OpenComponent<DxGridBandColumn>(seq++);
builder.AddAttribute(seq++, "Caption", band.Caption);
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
{
var updated = await Api.UpdateAsync(editingId, formModel);
if (!updated.Success)
var bandSeq = 0;
foreach (var columnName in band.Columns)
{
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
if (columnLookup.TryGetValue(columnName, out var column))
BuildDataColumn(bandBuilder, ref bandSeq, column);
}
}));
builder.CloseComponent();
}
};
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
{
builder.OpenComponent<DxGridDataColumn>(seq++);
builder.AddAttribute(seq++, "FieldName", column.FieldName);
builder.AddAttribute(seq++, "Caption", column.Caption);
if (!string.IsNullOrWhiteSpace(column.Width))
builder.AddAttribute(seq++, "Width", column.Width);
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
if (column.ReadOnly)
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
}
private void SetEditContext(EditContext context)
{
if (editContext == context) return;
if (editContext != null)
editContext.OnFieldChanged -= OnEditFieldChanged;
editContext = context;
validationMessageStore = new ValidationMessageStore(editContext);
editContext.OnFieldChanged += OnEditFieldChanged;
}
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
{
if (validationMessageStore == null || editContext == null) return;
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.UpdateProcedure))
{
validationMessageStore.Clear();
editContext.NotifyValidationStateChanged();
return;
}
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.CatTitle))
{
validationMessageStore.Clear(new FieldIdentifier(editContext.Model, nameof(CatalogEditModel.CatTitle)));
editContext.NotifyValidationStateChanged();
}
}
private void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{
popupHeaderText = e.IsNew ? "Neu" : "Edit";
if (e.IsNew)
{
e.EditModel = new CatalogEditModel { IsNew = true };
return;
}
var item = (CatalogReadDto)e.DataItem;
e.EditModel = new CatalogEditModel
{
Guid = item.Guid,
CatTitle = item.CatTitle,
CatString = item.CatString,
UpdateProcedure = 0,
OriginalCatTitle = item.CatTitle,
IsNew = false
};
}
private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
{
errorMessage = null;
infoMessage = null;
validationMessageStore?.Clear();
editContext?.NotifyValidationStateChanged();
var editModel = (CatalogEditModel)e.EditModel;
if (!ValidateEditModel(editModel, e.IsNew))
{
e.Cancel = true;
return;
}
var dto = new CatalogWriteDto
{
CatTitle = editModel.CatTitle,
CatString = editModel.CatString,
UpdateProcedure = editModel.UpdateProcedure
};
try
{
if (e.IsNew)
{
var created = await Api.CreateAsync(dto);
if (!created.Success || created.Value == null)
{
if (!string.IsNullOrWhiteSpace(created.Error))
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), created.Error);
else
errorMessage = "Anlegen fehlgeschlagen.";
e.Cancel = true;
return;
}
infoMessage = "Katalog aktualisiert.";
infoMessage = "Katalog angelegt.";
focusedRowKey = created.Value.Guid;
}
else
{
var created = await Api.CreateAsync(formModel);
if (!created.Success || created.Value == null)
var updated = await Api.UpdateAsync(editModel.Guid, dto);
if (!updated.Success)
{
errorMessage = created.Error ?? "Anlegen fehlgeschlagen.";
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
e.Cancel = true;
return;
}
infoMessage = "Katalog angelegt.";
infoMessage = "Katalog aktualisiert.";
focusedRowKey = editModel.Guid;
}
showForm = false;
await LoadCatalogs();
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Speichern: {ex.Message}";
e.Cancel = true;
}
}
private void CancelEdit()
private void AddValidationError(CatalogEditModel editModel, string fieldName, string message)
{
showForm = false;
infoMessage = null;
errorMessage = null;
if (editContext == null || validationMessageStore == null) return;
validationMessageStore.Add(new FieldIdentifier(editModel, fieldName), message);
editContext.NotifyValidationStateChanged();
}
private async Task DeleteCatalog(int id)
private bool ValidateEditModel(CatalogEditModel editModel, bool isNew)
{
if (isNew) return true;
if (editModel.UpdateProcedure == 0 &&
!string.Equals(editModel.CatTitle, editModel.OriginalCatTitle, StringComparison.OrdinalIgnoreCase))
{
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), "Titel kann nicht geändert werden.");
return false;
}
return true;
}
private async Task OnDataItemDeleting(GridDataItemDeletingEventArgs e)
{
errorMessage = null;
infoMessage = null;
var item = (CatalogReadDto)e.DataItem;
try
{
var deleted = await Api.DeleteAsync(id);
var deleted = await Api.DeleteAsync(item.Guid);
if (!deleted.Success)
{
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
e.Cancel = true;
return;
}
infoMessage = "Katalog gelöscht.";
await LoadCatalogs();
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Löschen: {ex.Message}";
e.Cancel = true;
}
}
private sealed class CatalogEditModel
{
public int Guid { get; set; }
public string CatTitle { get; set; } = string.Empty;
public string CatString { get; set; } = string.Empty;
public int UpdateProcedure { get; set; }
public string OriginalCatTitle { get; set; } = string.Empty;
public bool IsNew { get; set; }
}
private sealed class ProcedureOption
{
public int Value { get; set; }
public string Text { get; set; } = string.Empty;
}
}
}

View File

@@ -0,0 +1,3 @@
.catalog-edit-popup {
min-width: 720px;
}

View File

@@ -1,12 +1,15 @@
@inherits LayoutComponentBase
@implements IDisposable
@inject ThemeState ThemeState
<div class="page">
<div class="page @(ThemeState.IsDarkMode ? "app-dark" : "app-light")">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<DxButton Text="@(ThemeState.IsDarkMode ? "Dark Mode aus" : "Dark Mode an")" Click="ToggleTheme" />
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
@@ -21,3 +24,20 @@
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
@code {
protected override void OnInitialized()
{
ThemeState.OnChange += StateHasChanged;
}
private void ToggleTheme()
{
ThemeState.SetDarkMode(!ThemeState.IsDarkMode);
}
public void Dispose()
{
ThemeState.OnChange -= StateHasChanged;
}
}

View File

@@ -4,14 +4,27 @@
flex-direction: column;
}
.page.app-dark {
background-color: #1b1b1b;
color: #f1f1f1;
}
main {
flex: 1;
}
.page.app-dark main {
background-color: #1b1b1b;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.page.app-dark .sidebar {
background-image: linear-gradient(180deg, #171717 0%, #0f2a46 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
@@ -21,6 +34,11 @@ main {
align-items: center;
}
.page.app-dark .top-row {
background-color: #222;
border-bottom-color: #3d3d3d;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;

View File

@@ -14,17 +14,6 @@
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="catalogs">
<span class="bi bi-collection-nav-menu" aria-hidden="true"></span> Catalogs
@@ -32,8 +21,20 @@
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="dashboards/default">
<span class="oi oi-list-rich" aria-hidden="true"></span> Dashboards
<NavLink class="nav-link" href="dashboards">
<span class="bi bi-speedometer-nav-menu" aria-hidden="true"></span> Dashboards
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="massdata">
<span class="bi bi-table-nav-menu" aria-hidden="true"></span> MassData
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="clock">
<span class="bi bi-clock-nav-menu" aria-hidden="true"></span> Clock
</NavLink>
</div>
</nav>

View File

@@ -46,6 +46,18 @@
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.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;

View File

@@ -0,0 +1,578 @@
@inject MassDataApiClient Api
@inject BandLayoutService BandLayoutService
@if (!string.IsNullOrWhiteSpace(errorMessage))
{
<div class="alert alert-danger" role="alert">@errorMessage</div>
}
else if (!string.IsNullOrWhiteSpace(infoMessage))
{
<div class="alert alert-success" role="alert">@infoMessage</div>
}
@if (!hasLoaded || isLoading)
{
<div class="loading-container">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Lade...</span>
</div>
</div>
}
else if (items.Count == 0)
{
<p>Keine Einträge vorhanden.</p>
}
else
{
<div class="band-editor">
<button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded">
<span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">&#9658;</span>
<span>Layout</span>
</button>
@if (bandEditorExpanded)
{
<div class="band-editor-body">
<div class="band-controls">
<DxButton Text="Band hinzufügen" Click="AddBand" />
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
</div>
@foreach (var band in bandLayout.Bands)
{
<div class="band-row">
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
</div>
}
<DxFormLayout CssClass="band-columns" ColCount="2">
@foreach (var column in columnDefinitions)
{
<DxFormLayoutItem Caption="@column.Caption">
<DxComboBox Data="@bandOptions"
TData="BandOption"
TValue="string"
TextFieldName="Caption"
ValueFieldName="Id"
Value="@GetColumnBand(column.FieldName)"
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
Width="100%" />
</DxFormLayoutItem>
}
</DxFormLayout>
</div>
}
</div>
<div class="mb-3 page-size-selector">
<span class="page-size-label">Datensätze je Seite:</span>
<DxComboBox Data="@pageSizeOptions"
TData="PageSizeOption"
TValue="int?"
TextFieldName="Text"
ValueFieldName="Value"
Value="@pageSize"
ValueChanged="OnPageSizeChanged"
CssClass="page-size-combo" />
</div>
<div class="grid-section">
<DxGrid Data="@items"
TItem="MassDataReadDto"
KeyFieldName="@nameof(MassDataReadDto.Id)"
SizeMode="@_sizeMode"
ShowGroupPanel="true"
ShowGroupedColumns="true"
AllowGroup="true"
FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always"
AllowColumnResize="true"
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
AllowColumnReorder="true"
PagerVisible="false"
PageSize="@(pageSize ?? 100)"
CssClass="mb-3 massdata-grid"
EditMode="GridEditMode.PopupEditForm"
PopupEditFormHeaderText="@popupHeaderText"
CustomizeEditModel="OnCustomizeEditModel"
EditModelSaving="OnEditModelSaving"
DataItemDeleting="OnDataItemDeleting"
FocusedRowEnabled="true"
@bind-FocusedRowKey="focusedRowKey"
@ref="gridRef">
<ToolbarTemplate>
<DxToolbar>
<DxToolbarItem Alignment="ToolbarItemAlignment.Right">
<Template Context="_">
<DxDropDownButton Text="@FormatSizeText(_sizeMode)"
RenderStyle="ButtonRenderStyle.Secondary"
RenderStyleMode="ButtonRenderStyleMode.Text"
ItemClick="OnSizeChange">
<Items>
@foreach (var size in _sizeModes)
{
<DxDropDownButtonItem Text="@FormatSizeText(size)" Id="@size.ToString()" />
}
</Items>
</DxDropDownButton>
</Template>
</DxToolbarItem>
</DxToolbar>
</ToolbarTemplate>
<Columns>
@RenderColumns()
</Columns>
<EditFormTemplate Context="editFormContext">
@{
SetEditContext(editFormContext.EditContext); var editModel = (MassDataEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew);
}
<DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="CustomerName">
<DxTextBox @bind-Text="editModel.CustomerName" Width="100%" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Amount">
<DxTextBox @bind-Text="editModel.AmountText" Width="100%" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Category">
<DxTextBox @bind-Text="editModel.Category" Width="100%" ReadOnly="@(!editModel.IsNew)" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Status">
<DxCheckBox @bind-Checked="editModel.StatusFlag" ReadOnly="@(!editModel.IsNew)" />
</DxFormLayoutItem>
@if (!editModel.IsNew)
{
<DxFormLayoutItem Caption="Prozedur">
<DxComboBox Data="@procedureOptions"
TData="ProcedureOption"
TValue="int"
TextFieldName="Text"
ValueFieldName="Value"
@bind-Value="editModel.UpdateProcedure"
Width="100%" />
</DxFormLayoutItem>
}
<DxFormLayoutItem ColSpanMd="12">
<ValidationSummary />
</DxFormLayoutItem>
</DxFormLayout>
</EditFormTemplate>
</DxGrid>
@if (pageCount > 1)
{
<div class="pager-container">
<DxPager PageCount="@pageCount" ActivePageIndex="@pageIndex" ActivePageIndexChanged="OnPageChanged" />
</div>
}
</div>
}
@code {
private List<MassDataReadDto> items = new();
private bool isLoading;
private 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 IGrid? gridRef;
private int? focusedRowKey;
private const string LayoutType = "GRID_BANDS";
private const string LayoutKey = "MassDataGrid";
private string? layoutUser;
private BandLayout bandLayout = new();
private Dictionary<string, string> columnBandAssignments = new();
private List<BandOption> bandOptions = new();
private Dictionary<string, ColumnDefinition> columnLookup = new();
private bool gridLayoutApplied;
private bool bandEditorExpanded;
private List<ColumnDefinition> columnDefinitions = new()
{
new() { FieldName = nameof(MassDataReadDto.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(MassDataReadDto.CustomerName), Caption = "CustomerName", FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(MassDataReadDto.Amount), Caption = "Amount", DisplayFormat = "c2", FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(MassDataReadDto.Category), Caption = "Category", ReadOnly = true, FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(MassDataReadDto.StatusFlag), Caption = "Status", ReadOnly = true, FilterType = ColumnFilterType.Bool },
new() { FieldName = nameof(MassDataReadDto.AddedWhen), Caption = "Added", ReadOnly = true, FilterType = ColumnFilterType.Date },
new() { FieldName = nameof(MassDataReadDto.ChangedWhen), Caption = "Changed", ReadOnly = true, FilterType = ColumnFilterType.Date }
};
private readonly List<PageSizeOption> pageSizeOptions = new()
{
new() { Value = 100, Text = "100" },
new() { Value = 1000, Text = "1.000" },
new() { Value = 10000, Text = "10.000" },
new() { Value = 100000, Text = "100.000" },
new() { Value = null, Text = "Alle" }
};
private readonly List<ProcedureOption> procedureOptions = new()
{
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
};
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
private SizeMode _sizeMode = SizeMode.Medium;
private static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
private string FormatSizeText(SizeMode size) => size switch
{
SizeMode.Small => "Klein",
SizeMode.Medium => "Mittel",
SizeMode.Large => "Groß",
_ => size.ToString()
};
private void OnSizeChange(DropDownButtonItemClickEventArgs args)
{
_sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
}
protected override async Task OnInitializedAsync()
{
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
ApplyColumnLayoutFromStorage();
_sizeMode = bandLayout.SizeMode;
UpdateBandOptions();
await LoadPage(0);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
{
gridRef.LoadLayout(bandLayout.GridLayout);
gridLayoutApplied = true;
await InvokeAsync(StateHasChanged);
}
}
private async Task LoadPage(int page)
{
isLoading = true;
errorMessage = null;
try
{
var total = await Api.GetCountAsync();
var effectivePageSize = pageSize ?? (total == 0 ? 1 : total);
pageCount = Math.Max(1, (int)Math.Ceiling(total / (double)effectivePageSize));
pageIndex = Math.Clamp(page, 0, pageCount - 1);
var skip = pageSize.HasValue ? pageIndex * pageSize.Value : 0;
items = await Api.GetAllAsync(skip, pageSize);
}
catch (Exception ex)
{
errorMessage = $"MassData konnten nicht geladen werden: {ex.Message}";
}
finally
{
isLoading = false;
hasLoaded = true;
StateHasChanged();
}
}
private async Task OnPageChanged(int index) => await LoadPage(index);
private async Task OnPageSizeChanged(int? size)
{
pageSize = size;
await LoadPage(0);
}
private async Task SaveLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
return;
try
{
CaptureColumnLayoutFromGrid();
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
infoMessage = "Layout gespeichert.";
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
}
}
private async Task ResetLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
return;
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
bandLayout = new BandLayout();
columnBandAssignments.Clear();
UpdateBandOptions();
foreach (var column in columnDefinitions)
column.Width = null;
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
_sizeMode = SizeMode.Medium;
if (gridRef != null)
gridRef.LoadLayout(new GridPersistentLayout());
gridLayoutApplied = false;
infoMessage = "Layout zurückgesetzt.";
errorMessage = null;
}
private void CaptureColumnLayoutFromGrid()
{
if (gridRef == null) return;
var layout = gridRef.SaveLayout();
bandLayout.GridLayout = layout;
bandLayout.SizeMode = _sizeMode;
var orderedColumns = layout.Columns
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
.OrderBy(c => c.VisibleIndex)
.ToList();
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
bandLayout.ColumnWidths = orderedColumns
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
}
private void ApplyColumnLayoutFromStorage()
{
foreach (var column in columnDefinitions)
{
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
column.Width = width;
}
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
}
private void AddBand()
{
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
UpdateBandOptions();
}
private void RemoveBand(BandDefinition band)
{
bandLayout.Bands.Remove(band);
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
columnBandAssignments.Remove(key);
UpdateBandOptions();
SyncBandsFromAssignments();
}
private void UpdateBandCaption(BandDefinition band, string value)
{
band.Caption = value;
UpdateBandOptions();
}
private void UpdateColumnBand(string fieldName, string? bandId)
{
if (string.IsNullOrWhiteSpace(bandId))
columnBandAssignments.Remove(fieldName);
else
columnBandAssignments[fieldName] = bandId;
SyncBandsFromAssignments();
}
private string GetColumnBand(string fieldName)
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
private void SyncBandsFromAssignments()
{
foreach (var band in bandLayout.Bands)
{
band.Columns = columnDefinitions
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
.Select(c => c.FieldName)
.ToList();
}
StateHasChanged();
}
private void UpdateBandOptions()
{
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
}
private RenderFragment RenderColumns() => builder =>
{
var seq = 0;
builder.OpenComponent<DxGridCommandColumn>(seq++);
builder.AddAttribute(seq++, "Width", "120px");
builder.CloseComponent();
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
BuildDataColumn(builder, ref seq, column);
foreach (var band in bandLayout.Bands)
{
if (band.Columns.Count == 0) continue;
builder.OpenComponent<DxGridBandColumn>(seq++);
builder.AddAttribute(seq++, "Caption", band.Caption);
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
{
var bandSeq = 0;
foreach (var columnName in band.Columns)
{
if (columnLookup.TryGetValue(columnName, out var column))
BuildDataColumn(bandBuilder, ref bandSeq, column);
}
}));
builder.CloseComponent();
}
};
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
{
builder.OpenComponent<DxGridDataColumn>(seq++);
builder.AddAttribute(seq++, "FieldName", column.FieldName);
builder.AddAttribute(seq++, "Caption", column.Caption);
if (!string.IsNullOrWhiteSpace(column.Width))
builder.AddAttribute(seq++, "Width", column.Width);
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
if (column.ReadOnly)
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
}
private void SetEditContext(EditContext context)
{
if (editContext == context) return;
if (editContext != null)
editContext.OnFieldChanged -= OnEditFieldChanged;
editContext = context;
validationMessageStore = new ValidationMessageStore(editContext);
editContext.OnFieldChanged += OnEditFieldChanged;
}
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
{
if (validationMessageStore == null || editContext == null) return;
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure))
{
validationMessageStore.Clear();
editContext.NotifyValidationStateChanged();
return;
}
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.CustomerName))
{
validationMessageStore.Clear(new FieldIdentifier(editContext.Model, nameof(MassDataEditModel.CustomerName)));
editContext.NotifyValidationStateChanged();
}
}
private void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
private async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{
if (e.IsNew)
{
e.EditModel = new MassDataEditModel { IsNew = true, UpdateProcedure = procedureOptions[0].Value };
SetPopupHeaderText(true);
return;
}
var item = (MassDataReadDto)e.DataItem;
e.EditModel = new MassDataEditModel
{
Id = item.Id,
CustomerName = item.CustomerName,
AmountText = item.Amount.ToString("0.00"),
Category = item.Category,
StatusFlag = item.StatusFlag,
UpdateProcedure = procedureOptions[0].Value,
IsNew = false,
OriginalCustomerName = item.CustomerName
};
SetPopupHeaderText(false);
}
private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
{
errorMessage = null;
infoMessage = null;
validationMessageStore?.Clear();
editContext?.NotifyValidationStateChanged();
var editModel = (MassDataEditModel)e.EditModel;
if (!decimal.TryParse(editModel.AmountText, out var amount))
{
AddValidationError(editModel, nameof(MassDataEditModel.AmountText), "Amount ist ungültig.");
e.Cancel = true;
return;
}
if (editModel.IsNew)
{
var existing = await Api.GetByCustomerNameAsync(editModel.CustomerName);
if (existing != null)
{
AddValidationError(editModel, nameof(MassDataEditModel.CustomerName), "Kunde existiert bereits.");
e.Cancel = true;
return;
}
}
var dto = new MassDataWriteDto
{
CustomerName = editModel.CustomerName,
Amount = amount,
Category = editModel.Category,
StatusFlag = editModel.StatusFlag
};
try
{
var saved = await Api.UpsertAsync(dto);
infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert.";
focusedRowKey = saved.Id;
await LoadPage(pageIndex);
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Speichern: {ex.Message}";
e.Cancel = true;
}
}
private void AddValidationError(MassDataEditModel editModel, string fieldName, string message)
{
if (editContext == null || validationMessageStore == null) return;
validationMessageStore.Add(new FieldIdentifier(editModel, fieldName), message);
editContext.NotifyValidationStateChanged();
}
private Task OnDataItemDeleting(GridDataItemDeletingEventArgs e)
{
errorMessage = null;
infoMessage = "Löschen ist aktuell noch nicht verfügbar.";
e.Cancel = true;
return Task.CompletedTask;
}
private sealed class MassDataEditModel
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string AmountText { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public bool StatusFlag { get; set; }
public int UpdateProcedure { get; set; }
public bool IsNew { get; set; }
public string OriginalCustomerName { get; set; } = string.Empty;
}
private sealed class ProcedureOption
{
public int Value { get; set; }
public string Text { get; set; } = string.Empty;
}
private sealed class PageSizeOption
{
public int? Value { get; set; }
public string Text { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,3 @@
.massdata-edit-popup {
min-width: 720px;
}

View File

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

View File

@@ -1,19 +0,0 @@
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -1,71 +1,123 @@
@page "/dashboard"
@page "/dashboards/{DashboardId?}"
@implements IAsyncDisposable
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
@inject NavigationManager Navigation
<style>
.dashboard-shell {
display: flex;
gap: 0;
min-height: 800px;
border: 1px solid #e6e6e6;
border-radius: 6px;
overflow: hidden;
background: #fff;
}
.dashboard-nav {
width: 220px;
border-right: 1px solid #e6e6e6;
background: #fafafa;
}
.dashboard-nav-title {
padding: 0.75rem 1rem 0.5rem;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #6c757d;
font-weight: 600;
}
.dashboard-nav-link {
display: block;
padding: 0.55rem 1rem;
color: inherit;
text-decoration: none;
}
.dashboard-nav-link.active {
background: #e9ecef;
font-weight: 600;
}
.dashboard-content {
flex: 1;
min-width: 0;
padding: 1rem;
}
</style>
@inject DashboardApiClient DashboardApi
<PageTitle>Dashboards</PageTitle>
<div class="dashboard-shell">
<aside class="dashboard-nav">
<div class="dashboard-nav-title">Dashboards</div>
<NavLink class="dashboard-nav-link" href="dashboards/default">Default Dashboard (Designer)</NavLink>
@if (dashboards.Count == 0)
{
<div class="px-3 py-2 text-muted">Keine Dashboards vorhanden.</div>
}
else
{
@foreach (var dashboard in dashboards)
{
<NavLink class="dashboard-nav-link" href="@($"dashboards/{dashboard.Id}?mode={(IsDesigner ? "designer" : "viewer")}")">@dashboard.Name</NavLink>
}
}
</aside>
<section class="dashboard-content">
<DxDashboard Endpoint="@DashboardEndpoint" InitialDashboardId="DefaultDashboard" WorkingMode="WorkingMode.Designer" style="width: 100%; height: 800px;">
<div class="mb-3">
<DxButton RenderStyle="ButtonRenderStyle.Primary" Click="@ToggleMode">
@(IsDesigner ? "Zum Viewer wechseln" : "Zum Designer wechseln")
</DxButton>
</div>
<DxDashboard @key="DashboardKey" Endpoint="@DashboardEndpoint" InitialDashboardId="@SelectedDashboardId" WorkingMode="@CurrentMode" style="width: 100%; height: 800px;">
</DxDashboard>
</section>
</div>
@code {
[Parameter] public string? DashboardId { get; set; }
[SupplyParameterFromQuery] public string? Mode { get; set; }
private readonly List<DashboardInfoDto> dashboards = new();
private HubConnection? _hubConnection;
private bool IsDesigner => !string.Equals(Mode, "viewer", StringComparison.OrdinalIgnoreCase);
private WorkingMode CurrentMode => IsDesigner ? WorkingMode.Designer : WorkingMode.ViewerOnly;
private string SelectedDashboardId { get; set; } = string.Empty;
private string DashboardKey => $"{SelectedDashboardId}-{(IsDesigner ? "designer" : "viewer")}";
private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
private string HubEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/hubs/dashboards";
protected override void OnParametersSet()
protected override async Task OnInitializedAsync()
{
if (!string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase))
await RefreshDashboards();
_hubConnection = new HubConnectionBuilder()
.WithUrl(HubEndpoint)
.WithAutomaticReconnect()
.Build();
_hubConnection.On("DashboardsChanged", async () =>
{
Navigation.NavigateTo("dashboards/default", replace: true);
await RefreshDashboards();
});
await _hubConnection.StartAsync();
}
protected override async Task OnParametersSetAsync()
{
if (dashboards.Count == 0)
{
await RefreshDashboards();
}
var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase)
? null
: DashboardId;
var resolved = !string.IsNullOrWhiteSpace(requestedId)
? dashboards.FirstOrDefault(d => string.Equals(d.Id, requestedId, StringComparison.OrdinalIgnoreCase))
: dashboards.FirstOrDefault(d => string.Equals(d.Id, "DefaultDashboard", StringComparison.OrdinalIgnoreCase))
?? dashboards.FirstOrDefault();
if (resolved == null)
{
return;
}
SelectedDashboardId = resolved.Id;
if (!string.Equals(DashboardId, resolved.Id, StringComparison.OrdinalIgnoreCase))
{
Navigation.NavigateTo($"dashboards/{resolved.Id}?mode={(IsDesigner ? "designer" : "viewer")}", replace: true);
}
}
private void ToggleMode()
{
var targetMode = IsDesigner ? "viewer" : "designer";
Navigation.NavigateTo($"dashboards/{SelectedDashboardId}?mode={targetMode}", replace: true);
}
private async Task RefreshDashboards()
{
var latest = await DashboardApi.GetAllAsync();
if (latest.Count == dashboards.Count && latest.All(d => dashboards.Any(x => x.Id == d.Id && x.Name == d.Name)))
{
return;
}
dashboards.Clear();
dashboards.AddRange(latest);
await InvokeAsync(StateHasChanged);
}
public async ValueTask DisposeAsync()
{
if (_hubConnection != null)
{
await _hubConnection.DisposeAsync();
}
}
}

View File

@@ -0,0 +1,42 @@
.dashboard-shell {
display: flex;
gap: 0;
min-height: 800px;
border: 1px solid #e6e6e6;
border-radius: 6px;
overflow: hidden;
background: #fff;
}
.dashboard-nav {
width: 220px;
border-right: 1px solid #e6e6e6;
background: #fafafa;
}
.dashboard-nav-title {
padding: 0.75rem 1rem 0.5rem;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #6c757d;
font-weight: 600;
}
.dashboard-nav-link {
display: block;
padding: 0.55rem 1rem;
color: inherit;
text-decoration: none;
}
.dashboard-nav-link.active {
background: #e9ecef;
font-weight: 600;
}
.dashboard-content {
flex: 1;
min-width: 0;
padding: 1rem;
}

View File

@@ -1,7 +1,43 @@
@page "/"
@rendermode InteractiveServer
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
<h1>Database first</h1>
Welcome to your new app.
<DxCarousel Width="100%"
Height="calc(100vh - 9rem)"
Data="@GetCarouselData()"
ImageSrcField="Source"
ImageAltField="AlternateText"
LoopNavigationEnabled="true"
SlideShowEnabled="true"
SlideShowDelay="3000"
PauseSlideShowOnHover="true"
ImageSizeMode="CarouselImageSizeMode.FitProportional">
</DxCarousel>
@code {
List<CarouselData> GetCarouselData()
{
return new List<CarouselData>
{
new CarouselData("/images/DbFirstBefehl.png", "DbFirstBefehl"),
new CarouselData("/images/CQRS - Katalog-Datenfluss.png", "CQRS - Katalog-Datenfluss"),
new CarouselData("/images/CQRS - Catalog Create, Update, Delete.png", "CQRS - Catalog Create, Update, Delete"),
};
}
public class CarouselData
{
public string Source { get; set; }
public string AlternateText { get; set; }
public CarouselData(string source, string alt)
{
Source = source;
AlternateText = alt;
}
}
}

View File

@@ -0,0 +1,7 @@
@page "/massdata"
<PageTitle>MassData</PageTitle>
<h1>MassData</h1>
<MassDataGrid />

View File

@@ -1,64 +0,0 @@
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@@ -1,16 +1,20 @@
@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 DbFirst.BlazorWebApp
@using DbFirst.BlazorWebApp.Components
@using DbFirst.BlazorWebApp.Models
@using DbFirst.BlazorWebApp.Models.Grid
@using DbFirst.BlazorWebApp.Services
@using DevExpress.Blazor
@using DevExpress.DashboardBlazor
@using DevExpress.DashboardWeb
@using DbFirst.BlazorWebApp
@using DevExpress.Data.Filtering

View File

@@ -11,6 +11,11 @@
<PackageReference Include="DevExpress.Blazor.Dashboard" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Themes" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.22" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\images\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,7 @@
namespace DbFirst.BlazorWebApp.Models;
public class DashboardInfoDto
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,43 @@
using DevExpress.Blazor;
namespace DbFirst.BlazorWebApp.Models.Grid
{
public class BandLayout
{
public List<BandDefinition> Bands { get; set; } = new();
public List<string> ColumnOrder { get; set; } = new();
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public GridPersistentLayout? GridLayout { get; set; }
public SizeMode SizeMode { get; set; } = SizeMode.Medium;
}
public class BandDefinition
{
public string Id { get; set; } = string.Empty;
public string Caption { get; set; } = string.Empty;
public List<string> Columns { get; set; } = new();
}
public class BandOption
{
public string Id { get; set; } = string.Empty;
public string Caption { get; set; } = string.Empty;
}
public class ColumnDefinition
{
public string FieldName { get; init; } = string.Empty;
public string Caption { get; init; } = string.Empty;
public string? Width { get; set; }
public string? DisplayFormat { get; init; }
public bool ReadOnly { get; init; }
public ColumnFilterType FilterType { get; init; }
}
public enum ColumnFilterType
{
Text,
Bool,
Date
}
}

View File

@@ -0,0 +1,9 @@
namespace DbFirst.BlazorWebApp.Models;
public class LayoutDto
{
public string LayoutType { get; set; } = string.Empty;
public string LayoutKey { get; set; } = string.Empty;
public string UserName { get; set; } = string.Empty;
public string LayoutData { get; set; } = string.Empty;
}

View File

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

View File

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

View File

@@ -8,7 +8,9 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddDevExpressBlazor();
builder.Services.AddDevExpressBlazor(options => options.BootstrapVersion = BootstrapVersion.v5);
builder.Services.AddScoped<ThemeState>();
builder.Services.AddScoped<BandLayoutService>();
var apiBaseUrl = builder.Configuration["ApiBaseUrl"];
if (!string.IsNullOrWhiteSpace(apiBaseUrl))
@@ -17,10 +19,30 @@ 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);
});
builder.Services.AddHttpClient<TimeApiClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
});
}
else
{
builder.Services.AddHttpClient<CatalogApiClient>();
builder.Services.AddHttpClient<DashboardApiClient>();
builder.Services.AddHttpClient<MassDataApiClient>();
builder.Services.AddHttpClient<LayoutApiClient>();
builder.Services.AddHttpClient<TimeApiClient>();
}
var app = builder.Build();

View File

@@ -0,0 +1,103 @@
using DbFirst.BlazorWebApp.Models;
using DbFirst.BlazorWebApp.Models.Grid;
using Microsoft.JSInterop;
using System.Text.Json;
namespace DbFirst.BlazorWebApp.Services
{
public class BandLayoutService(LayoutApiClient layoutApi, IJSRuntime jsRuntime)
{
private const string LayoutUserStorageKey = "layoutUser";
private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
public async Task<string> EnsureLayoutUserAsync()
{
var layoutUser = await jsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
if (string.IsNullOrWhiteSpace(layoutUser))
{
layoutUser = Guid.NewGuid().ToString("N");
await jsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
}
return layoutUser;
}
public async Task<BandLayout> LoadBandLayoutAsync(
string layoutType,
string layoutKey,
string layoutUser,
Dictionary<string, ColumnDefinition> columnLookup)
{
if (string.IsNullOrWhiteSpace(layoutUser))
return new BandLayout();
var stored = await layoutApi.GetAsync(layoutType, layoutKey, layoutUser);
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
{
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, _jsonOptions);
return NormalizeBandLayout(parsed, columnLookup);
}
return new BandLayout();
}
public async Task SaveBandLayoutAsync(
string layoutType,
string layoutKey,
string layoutUser,
BandLayout bandLayout)
{
var layoutData = JsonSerializer.Serialize(bandLayout, _jsonOptions);
await layoutApi.UpsertAsync(new LayoutDto
{
LayoutType = layoutType,
LayoutKey = layoutKey,
UserName = layoutUser,
LayoutData = layoutData
});
}
public async Task ResetBandLayoutAsync(
string layoutType,
string layoutKey,
string layoutUser)
{
await layoutApi.DeleteAsync(layoutType, layoutKey, layoutUser);
}
public Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
{
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var band in layout.Bands)
{
foreach (var column in band.Columns)
{
assignments[column] = band.Id;
}
}
return assignments;
}
public BandLayout NormalizeBandLayout(
BandLayout? layout,
Dictionary<string, ColumnDefinition> columnLookup)
{
layout ??= new BandLayout();
layout.Bands ??= new List<BandDefinition>();
layout.ColumnOrder ??= new List<string>();
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
foreach (var band in layout.Bands)
{
if (string.IsNullOrWhiteSpace(band.Id))
band.Id = Guid.NewGuid().ToString("N");
if (string.IsNullOrWhiteSpace(band.Caption))
band.Caption = "Band";
band.Columns = band.Columns?.Where(columnLookup.ContainsKey).ToList() ?? new List<string>();
}
return layout;
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Net.Http.Json;
using DbFirst.BlazorWebApp.Models;
namespace DbFirst.BlazorWebApp.Services;
public class DashboardApiClient
{
private readonly HttpClient _httpClient;
private const string Endpoint = "api/dashboard/dashboards";
public DashboardApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<List<DashboardInfoDto>> GetAllAsync()
{
var result = await _httpClient.GetFromJsonAsync<List<DashboardInfoDto>>(Endpoint);
return result ?? new List<DashboardInfoDto>();
}
}

View File

@@ -0,0 +1,64 @@
using System.Net.Http.Json;
using DbFirst.BlazorWebApp.Models;
namespace DbFirst.BlazorWebApp.Services;
public class LayoutApiClient
{
private readonly HttpClient _httpClient;
private const string Endpoint = "api/layouts";
public LayoutApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<LayoutDto?> GetAsync(string layoutType, string layoutKey, string userName)
{
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
var response = await _httpClient.GetAsync(url);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<LayoutDto>();
}
public async Task<LayoutDto> UpsertAsync(LayoutDto dto)
{
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
if (!response.IsSuccessStatusCode)
{
var detail = await ReadErrorAsync(response);
throw new InvalidOperationException(detail);
}
var payload = await response.Content.ReadFromJsonAsync<LayoutDto>();
return payload ?? dto;
}
private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
{
var body = await response.Content.ReadAsStringAsync();
if (!string.IsNullOrWhiteSpace(body))
{
return body;
}
return $"{(int)response.StatusCode} {response.ReasonPhrase}".Trim();
}
public async Task DeleteAsync(string layoutType, string layoutKey, string userName)
{
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
var response = await _httpClient.DeleteAsync(url);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return;
}
response.EnsureSuccessStatusCode();
}
}

View File

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

View File

@@ -0,0 +1,35 @@
using DevExpress.Blazor;
namespace DbFirst.BlazorWebApp.Services;
public class ThemeState
{
private readonly IThemeChangeService themeChangeService;
public ThemeState(IThemeChangeService themeChangeService)
{
this.themeChangeService = themeChangeService;
}
public bool IsDarkMode { get; private set; }
public event Action? OnChange;
public void SetDarkMode(bool isDarkMode)
{
if (IsDarkMode == isDarkMode)
{
return;
}
IsDarkMode = isDarkMode;
var theme = Themes.Fluent.Clone(properties =>
{
properties.Mode = isDarkMode ? ThemeMode.Dark : ThemeMode.Light;
properties.ApplyToPageElements = true;
});
themeChangeService.SetTheme(theme);
OnChange?.Invoke();
}
}

View File

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

View File

@@ -1,11 +1,37 @@
:root {
--global-size: 16px;
}
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: var(--global-size);
}
/* Theme-Variablen */
.app-light {
--band-editor-bg: #f8f9fa;
--band-editor-border: #dee2e6;
--band-toggle-hover-bg: #e9ecef;
--grid-stripe-bg: rgba(0, 0, 0, 0.03);
}
.app-dark {
background-color: #1b1b1b;
color: #f1f1f1;
--band-editor-bg: #2d2d2d;
--band-editor-border: #444444;
--band-toggle-hover-bg: #3a3a3a;
--grid-stripe-bg: rgba(255, 255, 255, 0.04);
}
a, .btn-link {
color: #006bb7;
}
.app-dark a, .app-dark .btn-link {
color: #6cb6ff;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
@@ -13,7 +39,7 @@ a, .btn-link {
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
@@ -37,7 +63,7 @@ h1:focus {
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDg2IDY2LjAxODMgMjYzLjU4NiA2Ni4wMTgzWk0yNjMuNTc2IDg2LjA1NDdDMjYxLjA0OSA4Ni4wNTQ3IDI1OS43ODUgODcuMzAwNSAyNTkuNzg2IDg5Ljc5MjEgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
@@ -49,3 +75,113 @@ h1:focus {
.darker-border-checkbox.form-check-input {
border-color: #929292;
}
/* Grid Band-Editor */
.band-editor {
margin-top: 4px;
margin-bottom: 16px;
border: 1px solid var(--band-editor-border);
border-radius: 4px;
background-color: var(--band-editor-bg);
overflow: hidden;
}
.band-editor-toggle {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 8px 12px;
background: none;
border: none;
cursor: pointer;
font-size: inherit;
color: inherit;
text-align: left;
}
.band-editor-toggle:hover {
background-color: var(--band-toggle-hover-bg);
}
.band-editor-toggle-icon {
font-size: 0.7rem;
transition: transform 0.2s ease;
display: inline-block;
}
.band-editor-toggle-icon.expanded {
transform: rotate(90deg);
}
.band-editor-body {
display: flex;
flex-direction: column;
gap: 12px;
padding: 0 12px 12px 12px;
}
.band-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.band-row {
display: flex;
gap: 8px;
align-items: center;
}
.band-columns {
max-width: 720px;
margin-top: 4px;
}
.grid-section {
margin-top: 4px;
}
/* Grid Zebra-Striping */
dxbl-grid tbody tr:nth-child(even) td {
background-color: var(--grid-stripe-bg) !important;
}
/* MassData-spezifisch */
.page-size-selector {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
margin-bottom: 12px;
}
.page-size-label {
white-space: nowrap;
}
.page-size-combo {
width: 13ch;
min-width: 13ch;
max-width: 13ch;
}
.page-size-combo input {
text-align: left;
}
.pager-container {
display: flex;
justify-content: center;
margin-top: 12px;
margin-bottom: 16px;
}
/* Lade-Spinner */
.loading-container {
min-height: 160px;
display: flex;
align-items: center;
justify-content: center;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
namespace DbFirst.Domain.Entities;
public class SmfLayout
{
public long Guid { get; set; }
public bool Active { get; set; }
public string LayoutType { get; set; } = string.Empty;
public string LayoutKey { get; set; } = string.Empty;
public string UserName { get; set; } = string.Empty;
public byte[] LayoutData { get; set; } = Array.Empty<byte>();
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,6 @@
namespace DbFirst.Domain.Entities;
public class TimeRecord
{
public DateTime? Now { get; set; }
}

View File

@@ -15,6 +15,8 @@ public partial class ApplicationDbContext : DbContext
}
public virtual DbSet<VwmyCatalog> VwmyCatalogs { get; set; }
public virtual DbSet<SmfLayout> SmfLayouts { get; set; }
public virtual DbSet<TimeRecord> Times { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -51,6 +53,47 @@ public partial class ApplicationDbContext : DbContext
.HasColumnName(catCfg.ChangedWhoColumnName);
});
modelBuilder.Entity<SmfLayout>(entity =>
{
entity.HasKey(e => e.Guid);
entity.ToTable("TBDD_SMF_LAYOUT", tb => tb.HasTrigger("TBDD_SMF_LAYOUT_AFT_UPD"));
entity.Property(e => e.Guid).HasColumnName("GUID");
entity.Property(e => e.Active).HasColumnName("ACTIVE");
entity.Property(e => e.LayoutType)
.HasMaxLength(50)
.HasColumnName("LAYOUT_TYPE");
entity.Property(e => e.LayoutKey)
.HasMaxLength(150)
.HasColumnName("LAYOUT_KEY");
entity.Property(e => e.UserName)
.HasMaxLength(50)
.HasColumnName("USER_NAME");
entity.Property(e => e.LayoutData).HasColumnName("LAYOUT_DATA");
entity.Property(e => e.AddedWho)
.HasMaxLength(50)
.HasColumnName("ADDED_WHO");
entity.Property(e => e.AddedWhen)
.HasColumnType("datetime")
.HasColumnName("ADDED_WHEN");
entity.Property(e => e.ChangedWho)
.HasMaxLength(50)
.HasColumnName("CHANGED_WHO");
entity.Property(e => e.ChangedWhen)
.HasColumnType("datetime")
.HasColumnName("CHANGED_WHEN");
});
modelBuilder.Entity<TimeRecord>(entity =>
{
entity.HasNoKey();
entity.ToTable("TIME");
entity.Property(e => e.Now)
.HasColumnType("datetime")
.HasColumnName("NOW");
});
OnModelCreatingPartial(modelBuilder);
}

View File

@@ -11,6 +11,10 @@ public static class DependencyInjection
services.Configure<TableConfigurations>(configuration.GetSection("TableConfigurations"));
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
services.AddDbContext<MassDataDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("MassDataConnection")));
return services;
}
}

View File

@@ -0,0 +1,44 @@
using DbFirst.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace DbFirst.Infrastructure;
public class MassDataDbContext : DbContext
{
public MassDataDbContext(DbContextOptions<MassDataDbContext> options)
: base(options)
{
}
public virtual DbSet<Massdata> Massdata { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Massdata>(entity =>
{
entity.HasKey(e => e.Id);
entity.ToTable("MASSDATA");
entity.Property(e => e.Id).HasColumnName("ID");
entity.Property(e => e.CustomerName)
.HasMaxLength(200)
.IsUnicode(false)
.HasColumnName("CustomerName");
entity.Property(e => e.Amount)
.HasColumnType("decimal(12,2)")
.HasColumnName("Amount");
entity.Property(e => e.Category)
.HasMaxLength(100)
.IsUnicode(false)
.HasColumnName("Category");
entity.Property(e => e.StatusFlag)
.HasColumnName("StatusFlag");
entity.Property(e => e.AddedWhen)
.HasColumnType("datetime")
.HasColumnName("ADDED_WHEN");
entity.Property(e => e.ChangedWhen)
.HasColumnType("datetime")
.HasColumnName("CHANGED_WHEN");
});
}
}

Some files were not shown because too many files have changed in this diff Show More