Compare commits

..

8 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
32 changed files with 493 additions and 183 deletions

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

@@ -54,6 +54,7 @@ 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();

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

@@ -25,33 +25,42 @@ else if (items.Count == 0)
else
{
<div class="band-editor">
<div class="band-controls">
<DxButton Text="Band hinzufügen" Click="AddBand" />
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
</div>
@foreach (var band in bandLayout.Bands)
<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-row">
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
<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>
}
<DxFormLayout CssClass="band-columns" ColCount="2">
@foreach (var column in columnDefinitions)
{
<DxFormLayoutItem Caption="@column.Caption">
<DxComboBox Data="@bandOptions"
TData="BandOption"
TValue="string"
TextFieldName="Caption"
ValueFieldName="Id"
Value="@GetColumnBand(column.FieldName)"
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
Width="100%" />
</DxFormLayoutItem>
}
</DxFormLayout>
</div>
<div class="grid-section">
@@ -150,6 +159,7 @@ else
private List<BandOption> bandOptions = new();
private Dictionary<string, ColumnDefinition> columnLookup = new();
private bool gridLayoutApplied;
private bool bandEditorExpanded;
private List<ColumnDefinition> columnDefinitions = new()
{

View File

@@ -1,49 +1,3 @@
.action-panel {
margin-bottom: 16px;
}
.grid-section {
margin-top: 12px;
}
.catalog-edit-popup {
.catalog-edit-popup {
min-width: 720px;
}
.band-editor {
display: grid;
gap: 12px;
margin-bottom: 16px;
}
.band-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.band-row {
display: flex;
gap: 8px;
align-items: center;
}
.band-columns {
max-width: 720px;
}
.filter-row-cell {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
.filter-operator {
width: 52px;
min-width: 52px;
flex: 0 0 52px;
}
.filter-value {
min-width: 160px;
flex: 1 1 160px;
}
.loading-container {
min-height: 160px;
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -31,6 +31,12 @@
<span class="bi bi-table-nav-menu" aria-hidden="true"></span> MassData
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="clock">
<span class="bi bi-clock-nav-menu" aria-hidden="true"></span> Clock
</NavLink>
</div>
</nav>
</div>

View File

@@ -24,6 +24,46 @@ else if (items.Count == 0)
}
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"
@@ -36,36 +76,6 @@ else
CssClass="page-size-combo" />
</div>
<div class="band-editor">
<div class="band-controls">
<DxButton Text="Band hinzufügen" Click="AddBand" />
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
<DxButton Text="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 class="grid-section">
<DxGrid Data="@items"
TItem="MassDataReadDto"
@@ -178,6 +188,7 @@ else
private List<BandOption> bandOptions = new();
private Dictionary<string, ColumnDefinition> columnLookup = new();
private bool gridLayoutApplied;
private bool bandEditorExpanded;
private List<ColumnDefinition> columnDefinitions = new()
{
@@ -281,7 +292,6 @@ else
{
if (string.IsNullOrWhiteSpace(layoutUser))
return;
try
{
CaptureColumnLayoutFromGrid();
@@ -299,41 +309,31 @@ else
{
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;
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))
@@ -407,15 +407,12 @@ else
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 =>
@@ -458,14 +455,12 @@ else
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)));
@@ -483,7 +478,6 @@ else
SetPopupHeaderText(true);
return;
}
var item = (MassDataReadDto)e.DataItem;
e.EditModel = new MassDataEditModel
{
@@ -505,7 +499,6 @@ else
infoMessage = null;
validationMessageStore?.Clear();
editContext?.NotifyValidationStateChanged();
var editModel = (MassDataEditModel)e.EditModel;
if (!decimal.TryParse(editModel.AmountText, out var amount))
{
@@ -513,7 +506,6 @@ else
e.Cancel = true;
return;
}
if (editModel.IsNew)
{
var existing = await Api.GetByCustomerNameAsync(editModel.CustomerName);
@@ -524,7 +516,6 @@ else
return;
}
}
var dto = new MassDataWriteDto
{
CustomerName = editModel.CustomerName,
@@ -532,7 +523,6 @@ else
Category = editModel.Category,
StatusFlag = editModel.StatusFlag
};
try
{
var saved = await Api.UpsertAsync(dto);

View File

@@ -1,57 +1,3 @@
.action-panel {
margin-bottom: 16px;
}
.grid-section {
margin-top: 12px;
}
.pager-container {
display: flex;
justify-content: center;
margin-top: 12px;
margin-bottom: 16px;
}
.page-size-selector {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
}
.page-size-label {
white-space: nowrap;
}
.page-size-combo {
width: 13ch;
min-width: 13ch;
max-width: 13ch;
}
.page-size-combo input {
text-align: left;
}
.massdata-edit-popup {
.massdata-edit-popup {
min-width: 720px;
}
.band-editor {
display: grid;
gap: 12px;
margin-bottom: 16px;
}
.band-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.band-row {
display: flex;
gap: 8px;
align-items: center;
}
.band-columns {
max-width: 720px;
}
.loading-container {
min-height: 160px;
display: flex;
align-items: center;
justify-content: center;
}

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,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

@@ -14,4 +14,8 @@
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.22" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\images\" />
</ItemGroup>
</Project>

View File

@@ -31,6 +31,10 @@ if (!string.IsNullOrWhiteSpace(apiBaseUrl))
{
client.BaseAddress = new Uri(apiBaseUrl);
});
builder.Services.AddHttpClient<TimeApiClient>(client =>
{
client.BaseAddress = new Uri(apiBaseUrl);
});
}
else
{
@@ -38,6 +42,7 @@ else
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,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

@@ -7,9 +7,21 @@ html, body {
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 {
@@ -27,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 {
@@ -51,7 +63,7 @@ h1:focus {
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA9NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDg2IDY2LjAxODMgMjYzLjU4NiA2Ni4wMTgzWk0yNjMuNTc2IDg2LjA1NDdDMjYxLjA0OSA4Ni4wNTQ3IDI1OS43ODUgODcuMzAwNSAxNTEuMDIyIDg5Ljc5MjEgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) 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;
}
@@ -63,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,6 @@
namespace DbFirst.Domain.Entities;
public class TimeRecord
{
public DateTime? Now { get; set; }
}

View File

@@ -16,6 +16,7 @@ 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)
{
@@ -83,6 +84,16 @@ public partial class ApplicationDbContext : DbContext
.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

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