Compare commits
7 Commits
4ac8e94334
...
feat/timer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86feec930b | ||
|
|
f5224e20f2 | ||
|
|
36ade1b26b | ||
|
|
d422d841ff | ||
|
|
ea1b2ea6e4 | ||
|
|
6101561e72 | ||
|
|
64fb76b9e6 |
28
DbFirst.API/Controllers/TimeController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ builder.Services.AddApplication();
|
|||||||
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
|
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
|
||||||
builder.Services.AddScoped<IMassDataRepository, MassDataRepository>();
|
builder.Services.AddScoped<IMassDataRepository, MassDataRepository>();
|
||||||
builder.Services.AddScoped<ILayoutRepository, LayoutRepository>();
|
builder.Services.AddScoped<ILayoutRepository, LayoutRepository>();
|
||||||
|
builder.Services.AddScoped<ITimeRepository, TimeRepository>();
|
||||||
|
|
||||||
builder.Services.AddDevExpressControls();
|
builder.Services.AddDevExpressControls();
|
||||||
builder.Services.AddSignalR();
|
builder.Services.AddSignalR();
|
||||||
|
|||||||
9
DbFirst.Application/Repositories/ITimeRepository.cs
Normal 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);
|
||||||
|
}
|
||||||
6
DbFirst.Application/Time/Commands/InsertTimeCommand.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
using DbFirst.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace DbFirst.Application.Time.Commands;
|
||||||
|
|
||||||
|
public record InsertTimeCommand : IRequest<TimeRecord?>;
|
||||||
21
DbFirst.Application/Time/Commands/InsertTimeHandler.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,33 +25,42 @@ else if (items.Count == 0)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="band-editor">
|
<div class="band-editor">
|
||||||
<div class="band-controls">
|
<button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded">
|
||||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
<span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">►</span>
|
||||||
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
<span>Layout</span>
|
||||||
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
|
</button>
|
||||||
</div>
|
@if (bandEditorExpanded)
|
||||||
@foreach (var band in bandLayout.Bands)
|
|
||||||
{
|
{
|
||||||
<div class="band-row">
|
<div class="band-editor-body">
|
||||||
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
|
<div class="band-controls">
|
||||||
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
|
<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>
|
||||||
}
|
}
|
||||||
<DxFormLayout CssClass="band-columns" ColCount="2">
|
|
||||||
@foreach (var column in columnDefinitions)
|
|
||||||
{
|
|
||||||
<DxFormLayoutItem Caption="@column.Caption">
|
|
||||||
<DxComboBox Data="@bandOptions"
|
|
||||||
TData="BandOption"
|
|
||||||
TValue="string"
|
|
||||||
TextFieldName="Caption"
|
|
||||||
ValueFieldName="Id"
|
|
||||||
Value="@GetColumnBand(column.FieldName)"
|
|
||||||
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
|
|
||||||
Width="100%" />
|
|
||||||
</DxFormLayoutItem>
|
|
||||||
}
|
|
||||||
</DxFormLayout>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid-section">
|
<div class="grid-section">
|
||||||
@@ -150,6 +159,7 @@ else
|
|||||||
private List<BandOption> bandOptions = new();
|
private List<BandOption> bandOptions = new();
|
||||||
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
||||||
private bool gridLayoutApplied;
|
private bool gridLayoutApplied;
|
||||||
|
private bool bandEditorExpanded;
|
||||||
|
|
||||||
private List<ColumnDefinition> columnDefinitions = new()
|
private List<ColumnDefinition> columnDefinitions = new()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,6 +31,12 @@
|
|||||||
<span class="bi bi-table-nav-menu" aria-hidden="true"></span> MassData
|
<span class="bi bi-table-nav-menu" aria-hidden="true"></span> MassData
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</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>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,46 @@ else if (items.Count == 0)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
||||||
|
<div class="band-editor">
|
||||||
|
<button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded">
|
||||||
|
<span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">►</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">
|
<div class="mb-3 page-size-selector">
|
||||||
<span class="page-size-label">Datensätze je Seite:</span>
|
<span class="page-size-label">Datensätze je Seite:</span>
|
||||||
<DxComboBox Data="@pageSizeOptions"
|
<DxComboBox Data="@pageSizeOptions"
|
||||||
@@ -36,36 +76,6 @@ else
|
|||||||
CssClass="page-size-combo" />
|
CssClass="page-size-combo" />
|
||||||
</div>
|
</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">
|
<div class="grid-section">
|
||||||
<DxGrid Data="@items"
|
<DxGrid Data="@items"
|
||||||
TItem="MassDataReadDto"
|
TItem="MassDataReadDto"
|
||||||
@@ -178,6 +188,7 @@ else
|
|||||||
private List<BandOption> bandOptions = new();
|
private List<BandOption> bandOptions = new();
|
||||||
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
||||||
private bool gridLayoutApplied;
|
private bool gridLayoutApplied;
|
||||||
|
private bool bandEditorExpanded;
|
||||||
|
|
||||||
private List<ColumnDefinition> columnDefinitions = new()
|
private List<ColumnDefinition> columnDefinitions = new()
|
||||||
{
|
{
|
||||||
@@ -281,7 +292,6 @@ else
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CaptureColumnLayoutFromGrid();
|
CaptureColumnLayoutFromGrid();
|
||||||
@@ -299,41 +309,31 @@ else
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
|
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
|
||||||
|
|
||||||
bandLayout = new BandLayout();
|
bandLayout = new BandLayout();
|
||||||
columnBandAssignments.Clear();
|
columnBandAssignments.Clear();
|
||||||
UpdateBandOptions();
|
UpdateBandOptions();
|
||||||
|
|
||||||
foreach (var column in columnDefinitions)
|
foreach (var column in columnDefinitions)
|
||||||
column.Width = null;
|
column.Width = null;
|
||||||
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
_sizeMode = SizeMode.Medium;
|
_sizeMode = SizeMode.Medium;
|
||||||
|
|
||||||
if (gridRef != null)
|
if (gridRef != null)
|
||||||
gridRef.LoadLayout(new GridPersistentLayout());
|
gridRef.LoadLayout(new GridPersistentLayout());
|
||||||
gridLayoutApplied = false;
|
gridLayoutApplied = false;
|
||||||
|
|
||||||
infoMessage = "Layout zurückgesetzt.";
|
infoMessage = "Layout zurückgesetzt.";
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CaptureColumnLayoutFromGrid()
|
private void CaptureColumnLayoutFromGrid()
|
||||||
{
|
{
|
||||||
if (gridRef == null)
|
if (gridRef == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
var layout = gridRef.SaveLayout();
|
var layout = gridRef.SaveLayout();
|
||||||
bandLayout.GridLayout = layout;
|
bandLayout.GridLayout = layout;
|
||||||
bandLayout.SizeMode = _sizeMode;
|
bandLayout.SizeMode = _sizeMode;
|
||||||
|
|
||||||
var orderedColumns = layout.Columns
|
var orderedColumns = layout.Columns
|
||||||
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
|
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
|
||||||
.OrderBy(c => c.VisibleIndex)
|
.OrderBy(c => c.VisibleIndex)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
|
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
|
||||||
bandLayout.ColumnWidths = orderedColumns
|
bandLayout.ColumnWidths = orderedColumns
|
||||||
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
|
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
|
||||||
@@ -407,15 +407,12 @@ else
|
|||||||
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
||||||
builder.AddAttribute(seq++, "Width", "120px");
|
builder.AddAttribute(seq++, "Width", "120px");
|
||||||
builder.CloseComponent();
|
builder.CloseComponent();
|
||||||
|
|
||||||
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
|
foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
|
||||||
BuildDataColumn(builder, ref seq, column);
|
BuildDataColumn(builder, ref seq, column);
|
||||||
|
|
||||||
foreach (var band in bandLayout.Bands)
|
foreach (var band in bandLayout.Bands)
|
||||||
{
|
{
|
||||||
if (band.Columns.Count == 0) continue;
|
if (band.Columns.Count == 0) continue;
|
||||||
|
|
||||||
builder.OpenComponent<DxGridBandColumn>(seq++);
|
builder.OpenComponent<DxGridBandColumn>(seq++);
|
||||||
builder.AddAttribute(seq++, "Caption", band.Caption);
|
builder.AddAttribute(seq++, "Caption", band.Caption);
|
||||||
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
||||||
@@ -458,14 +455,12 @@ else
|
|||||||
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (validationMessageStore == null || editContext == null) return;
|
if (validationMessageStore == null || editContext == null) return;
|
||||||
|
|
||||||
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure))
|
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure))
|
||||||
{
|
{
|
||||||
validationMessageStore.Clear();
|
validationMessageStore.Clear();
|
||||||
editContext.NotifyValidationStateChanged();
|
editContext.NotifyValidationStateChanged();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.CustomerName))
|
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.CustomerName))
|
||||||
{
|
{
|
||||||
validationMessageStore.Clear(new FieldIdentifier(editContext.Model, nameof(MassDataEditModel.CustomerName)));
|
validationMessageStore.Clear(new FieldIdentifier(editContext.Model, nameof(MassDataEditModel.CustomerName)));
|
||||||
@@ -483,7 +478,6 @@ else
|
|||||||
SetPopupHeaderText(true);
|
SetPopupHeaderText(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var item = (MassDataReadDto)e.DataItem;
|
var item = (MassDataReadDto)e.DataItem;
|
||||||
e.EditModel = new MassDataEditModel
|
e.EditModel = new MassDataEditModel
|
||||||
{
|
{
|
||||||
@@ -505,7 +499,6 @@ else
|
|||||||
infoMessage = null;
|
infoMessage = null;
|
||||||
validationMessageStore?.Clear();
|
validationMessageStore?.Clear();
|
||||||
editContext?.NotifyValidationStateChanged();
|
editContext?.NotifyValidationStateChanged();
|
||||||
|
|
||||||
var editModel = (MassDataEditModel)e.EditModel;
|
var editModel = (MassDataEditModel)e.EditModel;
|
||||||
if (!decimal.TryParse(editModel.AmountText, out var amount))
|
if (!decimal.TryParse(editModel.AmountText, out var amount))
|
||||||
{
|
{
|
||||||
@@ -513,7 +506,6 @@ else
|
|||||||
e.Cancel = true;
|
e.Cancel = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editModel.IsNew)
|
if (editModel.IsNew)
|
||||||
{
|
{
|
||||||
var existing = await Api.GetByCustomerNameAsync(editModel.CustomerName);
|
var existing = await Api.GetByCustomerNameAsync(editModel.CustomerName);
|
||||||
@@ -524,7 +516,6 @@ else
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var dto = new MassDataWriteDto
|
var dto = new MassDataWriteDto
|
||||||
{
|
{
|
||||||
CustomerName = editModel.CustomerName,
|
CustomerName = editModel.CustomerName,
|
||||||
@@ -532,7 +523,6 @@ else
|
|||||||
Category = editModel.Category,
|
Category = editModel.Category,
|
||||||
StatusFlag = editModel.StatusFlag
|
StatusFlag = editModel.StatusFlag
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var saved = await Api.UpsertAsync(dto);
|
var saved = await Api.UpsertAsync(dto);
|
||||||
|
|||||||
100
DbFirst.BlazorWebApp/Components/Pages/Clock.razor
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,43 @@
|
|||||||
@page "/"
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
@page "/"
|
||||||
|
|
||||||
<PageTitle>Home</PageTitle>
|
<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,4 +14,8 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.22" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.22" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="wwwroot\images\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ if (!string.IsNullOrWhiteSpace(apiBaseUrl))
|
|||||||
{
|
{
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
});
|
});
|
||||||
|
builder.Services.AddHttpClient<TimeApiClient>(client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(apiBaseUrl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -38,6 +42,7 @@ else
|
|||||||
builder.Services.AddHttpClient<DashboardApiClient>();
|
builder.Services.AddHttpClient<DashboardApiClient>();
|
||||||
builder.Services.AddHttpClient<MassDataApiClient>();
|
builder.Services.AddHttpClient<MassDataApiClient>();
|
||||||
builder.Services.AddHttpClient<LayoutApiClient>();
|
builder.Services.AddHttpClient<LayoutApiClient>();
|
||||||
|
builder.Services.AddHttpClient<TimeApiClient>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|||||||
27
DbFirst.BlazorWebApp/Services/TimeApiClient.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,21 @@ html, body {
|
|||||||
font-size: var(--global-size);
|
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 {
|
.app-dark {
|
||||||
background-color: #1b1b1b;
|
background-color: #1b1b1b;
|
||||||
color: #f1f1f1;
|
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 {
|
a, .btn-link {
|
||||||
@@ -66,15 +78,47 @@ h1:focus {
|
|||||||
|
|
||||||
/* Grid Band-Editor */
|
/* Grid Band-Editor */
|
||||||
.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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px;
|
padding: 0 12px 12px 12px;
|
||||||
margin-top: 4px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.band-controls {
|
.band-controls {
|
||||||
@@ -99,6 +143,11 @@ h1:focus {
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Grid Zebra-Striping */
|
||||||
|
dxbl-grid tbody tr:nth-child(even) td {
|
||||||
|
background-color: var(--grid-stripe-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* MassData-spezifisch */
|
/* MassData-spezifisch */
|
||||||
.page-size-selector {
|
.page-size-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
BIN
DbFirst.BlazorWebApp/wwwroot/images/Blazor Server Lifecycle.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
BIN
DbFirst.BlazorWebApp/wwwroot/images/Datenfluss Catalogs.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
DbFirst.BlazorWebApp/wwwroot/images/Datenfluss Massdata.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
DbFirst.BlazorWebApp/wwwroot/images/DbFirstBefehl.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
DbFirst.BlazorWebApp/wwwroot/images/Fehlerbehandlung.png
Normal file
|
After Width: | Height: | Size: 742 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
BIN
DbFirst.BlazorWebApp/wwwroot/images/Übersicht Architektur.png
Normal file
|
After Width: | Height: | Size: 201 KiB |
6
DbFirst.Domain/Entities/Time.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace DbFirst.Domain.Entities;
|
||||||
|
|
||||||
|
public class TimeRecord
|
||||||
|
{
|
||||||
|
public DateTime? Now { get; set; }
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ public partial class ApplicationDbContext : DbContext
|
|||||||
|
|
||||||
public virtual DbSet<VwmyCatalog> VwmyCatalogs { get; set; }
|
public virtual DbSet<VwmyCatalog> VwmyCatalogs { get; set; }
|
||||||
public virtual DbSet<SmfLayout> SmfLayouts { get; set; }
|
public virtual DbSet<SmfLayout> SmfLayouts { get; set; }
|
||||||
|
public virtual DbSet<TimeRecord> Times { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -83,6 +84,16 @@ public partial class ApplicationDbContext : DbContext
|
|||||||
.HasColumnName("CHANGED_WHEN");
|
.HasColumnName("CHANGED_WHEN");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<TimeRecord>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasNoKey();
|
||||||
|
entity.ToTable("TIME");
|
||||||
|
|
||||||
|
entity.Property(e => e.Now)
|
||||||
|
.HasColumnType("datetime")
|
||||||
|
.HasColumnName("NOW");
|
||||||
|
});
|
||||||
|
|
||||||
OnModelCreatingPartial(modelBuilder);
|
OnModelCreatingPartial(modelBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
DbFirst.Infrastructure/Repositories/TimeRepository.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||