Convert TrafagSalesExporter from console app to Blazor Server app with MudBlazor UI
- Replaced console app with .NET 8 Blazor Server architecture - Added EF Core SQLite database (trafag_exporter.db) with auto-seed data - Models: HanaServer, Site, SharePointConfig, ExportSettings, ExportLog, SalesRecord - Services: HanaQueryService (with configurable dateFilter), ExcelExportService, SharePointUploadService, ExportOrchestrationService (with live status events), TimerBackgroundService (scheduled daily export) - MudBlazor UI pages: Dashboard (export status + manual trigger), Standorte (HANA server + site CRUD), Settings (SharePoint + timer config), Logs (filtered view) - SAP HANA queries unchanged (INV + CRN with exact SAP B1 table joins) - SharePoint upload via Microsoft Graph with app registration auth https://claude.ai/code/session_012heAXNMbbyxqYf2S2HrKLj
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
@page "/"
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using TrafagSalesExporter.Data
|
||||
@using TrafagSalesExporter.Services
|
||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
||||
@inject ExportOrchestrationService Orchestrator
|
||||
@inject TimerBackgroundService TimerService
|
||||
@inject ISnackbar Snackbar
|
||||
@implements IDisposable
|
||||
|
||||
<PageTitle>Dashboard</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Dashboard</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||
<MudStack Row AlignItems="AlignItems.Center" Spacing="4">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.PlayArrow"
|
||||
OnClick="ExportAll" Disabled="_anyRunning">
|
||||
Alle exportieren
|
||||
</MudButton>
|
||||
<MudText Typo="Typo.body1">
|
||||
@if (TimerService.NextRun < DateTime.MaxValue)
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.Schedule" Size="Size.Small" Class="mr-1" />
|
||||
@($"Nächster automatischer Lauf: {TimerService.NextRun:dd.MM.yyyy HH:mm}")
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.TimerOff" Size="Size.Small" Class="mr-1" />
|
||||
@("Timer deaktiviert")
|
||||
}
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudTable Items="_dashboardRows" Dense Hover Striped Loading="_loading">
|
||||
<HeaderContent>
|
||||
<MudTh>Land</MudTh>
|
||||
<MudTh>TSC</MudTh>
|
||||
<MudTh>Schema</MudTh>
|
||||
<MudTh>Server</MudTh>
|
||||
<MudTh>Status</MudTh>
|
||||
<MudTh>Zeilen</MudTh>
|
||||
<MudTh>Letzter Lauf</MudTh>
|
||||
<MudTh>Dauer</MudTh>
|
||||
<MudTh>Aktion</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Land</MudTd>
|
||||
<MudTd>@context.TSC</MudTd>
|
||||
<MudTd>@context.Schema</MudTd>
|
||||
<MudTd>@context.ServerName</MudTd>
|
||||
<MudTd>
|
||||
@if (Orchestrator.IsExporting(context.SiteId))
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate Color="Color.Primary" Class="mr-1" />
|
||||
<MudText Typo="Typo.caption">@Orchestrator.GetExportStatus(context.SiteId)</MudText>
|
||||
}
|
||||
else if (context.LastStatus == "OK")
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
|
||||
}
|
||||
else if (context.LastStatus == "Error")
|
||||
{
|
||||
<MudTooltip Text="@context.ErrorMessage">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Error" Color="Color.Error" Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Default">-</MudText>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd>@(context.RowCount > 0 ? context.RowCount.ToString("N0") : "-")</MudTd>
|
||||
<MudTd>@(context.LastRun.HasValue ? context.LastRun.Value.ToString("dd.MM.yyyy HH:mm:ss") : "-")</MudTd>
|
||||
<MudTd>@(context.DurationSeconds > 0 ? $"{context.DurationSeconds:F1}s" : "-")</MudTd>
|
||||
<MudTd>
|
||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload"
|
||||
OnClick="() => ExportSingle(context.SiteId)"
|
||||
Disabled="Orchestrator.IsExporting(context.SiteId)">
|
||||
Export
|
||||
</MudButton>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
|
||||
@code {
|
||||
private List<DashboardRow> _dashboardRows = new();
|
||||
private bool _loading = true;
|
||||
private bool _anyRunning;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Orchestrator.OnExportStatusChanged += HandleStatusChanged;
|
||||
await LoadDataAsync();
|
||||
}
|
||||
|
||||
private async Task LoadDataAsync()
|
||||
{
|
||||
_loading = true;
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
var sites = await db.Sites.Include(s => s.HanaServer).Where(s => s.IsActive).ToListAsync();
|
||||
var logs = await db.ExportLogs
|
||||
.GroupBy(l => l.SiteId)
|
||||
.Select(g => g.OrderByDescending(l => l.Timestamp).First())
|
||||
.ToListAsync();
|
||||
|
||||
_dashboardRows = sites.Select(s =>
|
||||
{
|
||||
var log = logs.FirstOrDefault(l => l.SiteId == s.Id);
|
||||
return new DashboardRow
|
||||
{
|
||||
SiteId = s.Id,
|
||||
Land = s.Land,
|
||||
TSC = s.TSC,
|
||||
Schema = s.Schema,
|
||||
ServerName = s.HanaServer?.Name ?? "",
|
||||
LastStatus = log?.Status ?? "",
|
||||
RowCount = log?.RowCount ?? 0,
|
||||
LastRun = log?.Timestamp,
|
||||
DurationSeconds = log?.DurationSeconds ?? 0,
|
||||
ErrorMessage = log?.ErrorMessage ?? ""
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId));
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task ExportAll()
|
||||
{
|
||||
_anyRunning = true;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Orchestrator.ExportAllAsync();
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
await LoadDataAsync();
|
||||
StateHasChanged();
|
||||
});
|
||||
});
|
||||
Snackbar.Add("Export für alle Standorte gestartet", Severity.Info);
|
||||
}
|
||||
|
||||
private void ExportSingle(int siteId)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Orchestrator.ExportSiteByIdAsync(siteId);
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
await LoadDataAsync();
|
||||
StateHasChanged();
|
||||
});
|
||||
});
|
||||
Snackbar.Add("Export gestartet", Severity.Info);
|
||||
}
|
||||
|
||||
private async void HandleStatusChanged()
|
||||
{
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId));
|
||||
StateHasChanged();
|
||||
if (!_anyRunning)
|
||||
{
|
||||
await LoadDataAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Orchestrator.OnExportStatusChanged -= HandleStatusChanged;
|
||||
}
|
||||
|
||||
private class DashboardRow
|
||||
{
|
||||
public int SiteId { get; set; }
|
||||
public string Land { get; set; } = "";
|
||||
public string TSC { get; set; } = "";
|
||||
public string Schema { get; set; } = "";
|
||||
public string ServerName { get; set; } = "";
|
||||
public string LastStatus { get; set; } = "";
|
||||
public int RowCount { get; set; }
|
||||
public DateTime? LastRun { get; set; }
|
||||
public double DurationSeconds { get; set; }
|
||||
public string ErrorMessage { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
@page "/logs"
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using TrafagSalesExporter.Data
|
||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
|
||||
<PageTitle>Logs</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Export Logs</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||
<MudStack Row AlignItems="AlignItems.Center" Spacing="3">
|
||||
<MudSelect @bind-Value="_filterLand" Label="Land" Clearable Dense Style="max-width:200px;">
|
||||
@foreach (var land in _availableLands)
|
||||
{
|
||||
<MudSelectItem Value="@land">@land</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudSelect @bind-Value="_filterStatus" Label="Status" Clearable Dense Style="max-width:150px;">
|
||||
<MudSelectItem Value="@("OK")">OK</MudSelectItem>
|
||||
<MudSelectItem Value="@("Error")">Error</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudDatePicker @bind-Date="_filterDate" Label="Datum" Clearable Dense Style="max-width:200px;" />
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ApplyFilter"
|
||||
StartIcon="@Icons.Material.Filled.FilterAlt">
|
||||
Filtern
|
||||
</MudButton>
|
||||
<MudSpacer />
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Error" OnClick="DeleteOldLogs"
|
||||
StartIcon="@Icons.Material.Filled.DeleteSweep">
|
||||
Alte Logs löschen
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudTable Items="_logs" Dense Hover Striped Loading="_loading">
|
||||
<HeaderContent>
|
||||
<MudTh>Zeitpunkt</MudTh>
|
||||
<MudTh>Land</MudTh>
|
||||
<MudTh>TSC</MudTh>
|
||||
<MudTh>Status</MudTh>
|
||||
<MudTh>Zeilen</MudTh>
|
||||
<MudTh>Dauer</MudTh>
|
||||
<MudTh>Dateiname</MudTh>
|
||||
<MudTh>Fehler</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Timestamp.ToString("dd.MM.yyyy HH:mm:ss")</MudTd>
|
||||
<MudTd>@context.Land</MudTd>
|
||||
<MudTd>@context.TSC</MudTd>
|
||||
<MudTd>
|
||||
@if (context.Status == "OK")
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Success">OK</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Error">Error</MudChip>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd>@context.RowCount.ToString("N0")</MudTd>
|
||||
<MudTd>@($"{context.DurationSeconds:F1}s")</MudTd>
|
||||
<MudTd>@context.FileName</MudTd>
|
||||
<MudTd>
|
||||
@if (!string.IsNullOrEmpty(context.ErrorMessage))
|
||||
{
|
||||
<MudTooltip Text="@context.ErrorMessage">
|
||||
<MudText Typo="Typo.caption" Color="Color.Error" Style="max-width:300px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:block;">
|
||||
@context.ErrorMessage
|
||||
</MudText>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
|
||||
@code {
|
||||
private List<ExportLog> _logs = new();
|
||||
private List<string> _availableLands = new();
|
||||
private string? _filterLand;
|
||||
private string? _filterStatus;
|
||||
private DateTime? _filterDate;
|
||||
private bool _loading = true;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
_availableLands = await db.ExportLogs.Select(l => l.Land).Distinct().OrderBy(l => l).ToListAsync();
|
||||
await LoadLogsAsync();
|
||||
}
|
||||
|
||||
private async Task LoadLogsAsync()
|
||||
{
|
||||
_loading = true;
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
IQueryable<ExportLog> query = db.ExportLogs.OrderByDescending(l => l.Timestamp);
|
||||
|
||||
if (!string.IsNullOrEmpty(_filterLand))
|
||||
query = query.Where(l => l.Land == _filterLand);
|
||||
|
||||
if (!string.IsNullOrEmpty(_filterStatus))
|
||||
query = query.Where(l => l.Status == _filterStatus);
|
||||
|
||||
if (_filterDate.HasValue)
|
||||
query = query.Where(l => l.Timestamp.Date == _filterDate.Value.Date);
|
||||
|
||||
_logs = await query.Take(500).ToListAsync();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task ApplyFilter()
|
||||
{
|
||||
await LoadLogsAsync();
|
||||
}
|
||||
|
||||
private async Task DeleteOldLogs()
|
||||
{
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"Alte Logs löschen",
|
||||
"Logs älter als 90 Tage löschen?",
|
||||
yesText: "Löschen", cancelText: "Abbrechen");
|
||||
|
||||
if (result != true) return;
|
||||
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var cutoff = DateTime.Now.AddDays(-90);
|
||||
var oldLogs = await db.ExportLogs.Where(l => l.Timestamp < cutoff).ToListAsync();
|
||||
db.ExportLogs.RemoveRange(oldLogs);
|
||||
var count = await db.SaveChangesAsync();
|
||||
await LoadLogsAsync();
|
||||
Snackbar.Add($"{oldLogs.Count} alte Logs gelöscht", Severity.Info);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
@page "/settings"
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using TrafagSalesExporter.Data
|
||||
@using TrafagSalesExporter.Services
|
||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
||||
@inject SharePointUploadService SpService
|
||||
@inject TimerBackgroundService TimerService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>Settings</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Settings</MudText>
|
||||
|
||||
@* SharePoint Config *@
|
||||
<MudText Typo="Typo.h5" Class="mb-2">SharePoint Konfiguration</MudText>
|
||||
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField @bind-Value="_spConfig.SiteUrl" Label="Site URL" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField @bind-Value="_spConfig.ExportFolder" Label="Export Folder" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudTextField @bind-Value="_spConfig.TenantId" Label="Tenant ID" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudTextField @bind-Value="_spConfig.ClientId" Label="Client ID" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudTextField @bind-Value="_spConfig.ClientSecret" Label="Client Secret" InputType="InputType.Password" />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudStack Row Spacing="2">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSharePoint"
|
||||
StartIcon="@Icons.Material.Filled.Save">
|
||||
Speichern
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick="TestSharePoint"
|
||||
StartIcon="@Icons.Material.Filled.NetworkCheck" Disabled="_testingSp">
|
||||
@if (_testingSp)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate Class="mr-2" />
|
||||
@("Teste...")
|
||||
}
|
||||
else
|
||||
{
|
||||
@("SharePoint Verbindung testen")
|
||||
}
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
@* Export Settings *@
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Export Einstellungen</MudText>
|
||||
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudTextField @bind-Value="_exportSettings.DateFilter" Label="Datum-Filter (ab)"
|
||||
HelperText="Format: yyyy-MM-dd" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudNumericField @bind-Value="_exportSettings.TimerHour" Label="Timer Stunde" Min="0" Max="23" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudNumericField @bind-Value="_exportSettings.TimerMinute" Label="Timer Minute" Min="0" Max="59" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudSwitch @bind-Value="_exportSettings.TimerEnabled" Label="Timer aktiviert" Color="Color.Primary" />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveExportSettings"
|
||||
StartIcon="@Icons.Material.Filled.Save">
|
||||
Speichern
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
@* Filename Preview *@
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Dateiname Vorschau</MudText>
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.body1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.InsertDriveFile" Size="Size.Small" Class="mr-1" />
|
||||
Sales_{"{TSC}"}_{DateTime.Now:yyyy-MM-dd}.xlsx
|
||||
</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mt-1">
|
||||
Beispiel: Sales_TRFR_@(DateTime.Now.ToString("yyyy-MM-dd")).xlsx
|
||||
</MudText>
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
private SharePointConfig _spConfig = new();
|
||||
private ExportSettings _exportSettings = new();
|
||||
private bool _testingSp;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
_spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig();
|
||||
_exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
|
||||
}
|
||||
|
||||
private async Task SaveSharePoint()
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var existing = await db.SharePointConfigs.FirstOrDefaultAsync();
|
||||
if (existing is null)
|
||||
{
|
||||
db.SharePointConfigs.Add(_spConfig);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.SiteUrl = _spConfig.SiteUrl;
|
||||
existing.ExportFolder = _spConfig.ExportFolder;
|
||||
existing.TenantId = _spConfig.TenantId;
|
||||
existing.ClientId = _spConfig.ClientId;
|
||||
existing.ClientSecret = _spConfig.ClientSecret;
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
Snackbar.Add("SharePoint Konfiguration gespeichert", Severity.Success);
|
||||
}
|
||||
|
||||
private async Task TestSharePoint()
|
||||
{
|
||||
_testingSp = true;
|
||||
try
|
||||
{
|
||||
await SpService.TestConnectionAsync(
|
||||
_spConfig.TenantId, _spConfig.ClientId, _spConfig.ClientSecret, _spConfig.SiteUrl);
|
||||
Snackbar.Add("SharePoint Verbindung erfolgreich!", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Verbindung fehlgeschlagen: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_testingSp = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveExportSettings()
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var existing = await db.ExportSettings.FirstOrDefaultAsync();
|
||||
if (existing is null)
|
||||
{
|
||||
db.ExportSettings.Add(_exportSettings);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.DateFilter = _exportSettings.DateFilter;
|
||||
existing.TimerHour = _exportSettings.TimerHour;
|
||||
existing.TimerMinute = _exportSettings.TimerMinute;
|
||||
existing.TimerEnabled = _exportSettings.TimerEnabled;
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
TimerService.Recalculate();
|
||||
Snackbar.Add("Export Einstellungen gespeichert", Severity.Success);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
@page "/standorte"
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using TrafagSalesExporter.Data
|
||||
@using TrafagSalesExporter.Services
|
||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
||||
@inject HanaQueryService HanaService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
|
||||
<PageTitle>Standorte</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Standorte</MudText>
|
||||
|
||||
@* HANA Server Section *@
|
||||
<MudText Typo="Typo.h5" Class="mb-2">HANA Server</MudText>
|
||||
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="AddServer" Class="mb-3">
|
||||
Server hinzufügen
|
||||
</MudButton>
|
||||
|
||||
<MudTable Items="_servers" Dense Hover Striped>
|
||||
<HeaderContent>
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh>Host</MudTh>
|
||||
<MudTh>Port</MudTh>
|
||||
<MudTh>Username</MudTh>
|
||||
<MudTh>Aktionen</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Name</MudTd>
|
||||
<MudTd>@context.Host</MudTd>
|
||||
<MudTd>@context.Port</MudTd>
|
||||
<MudTd>@context.Username</MudTd>
|
||||
<MudTd>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small"
|
||||
OnClick="() => EditServer(context)" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.NetworkCheck" Size="Size.Small" Color="Color.Info"
|
||||
OnClick="() => TestServerConnection(context)" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error"
|
||||
OnClick="() => DeleteServer(context)" />
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
|
||||
@* Sites Section *@
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Standorte (Sites)</MudText>
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="AddSite" Class="mb-3">
|
||||
Neuen Standort hinzufügen
|
||||
</MudButton>
|
||||
|
||||
<MudTable Items="_sites" Dense Hover Striped>
|
||||
<HeaderContent>
|
||||
<MudTh>Land</MudTh>
|
||||
<MudTh>TSC</MudTh>
|
||||
<MudTh>Schema</MudTh>
|
||||
<MudTh>Server</MudTh>
|
||||
<MudTh>Aktiv</MudTh>
|
||||
<MudTh>Aktionen</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Land</MudTd>
|
||||
<MudTd>@context.TSC</MudTd>
|
||||
<MudTd>@context.Schema</MudTd>
|
||||
<MudTd>@(context.HanaServer?.Name ?? "-")</MudTd>
|
||||
<MudTd>
|
||||
@if (context.IsActive)
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Default" Size="Size.Small" />
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small"
|
||||
OnClick="() => EditSite(context)" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error"
|
||||
OnClick="() => DeleteSite(context)" />
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
|
||||
@* Server Dialog *@
|
||||
<MudDialog @bind-Visible="_serverDialogVisible" Options="_dialogOptions">
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">@(_editingServer.Id == 0 ? "Server hinzufügen" : "Server bearbeiten")</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudTextField @bind-Value="_editingServer.Name" Label="Name" Required />
|
||||
<MudTextField @bind-Value="_editingServer.Host" Label="Host" Required />
|
||||
<MudNumericField @bind-Value="_editingServer.Port" Label="Port" />
|
||||
<MudTextField @bind-Value="_editingServer.Username" Label="Username" />
|
||||
<MudTextField @bind-Value="_editingServer.Password" Label="Password" InputType="InputType.Password" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="() => _serverDialogVisible = false">Abbrechen</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveServer">Speichern</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@* Site Dialog *@
|
||||
<MudDialog @bind-Visible="_siteDialogVisible" Options="_dialogOptions">
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">@(_editingSite.Id == 0 ? "Standort hinzufügen" : "Standort bearbeiten")</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudSelect @bind-Value="_editingSite.HanaServerId" Label="Server" Required>
|
||||
@foreach (var s in _servers)
|
||||
{
|
||||
<MudSelectItem Value="s.Id">@s.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudTextField @bind-Value="_editingSite.Schema" Label="Schema" Required />
|
||||
<MudTextField @bind-Value="_editingSite.TSC" Label="TSC" Required />
|
||||
<MudTextField @bind-Value="_editingSite.Land" Label="Land" Required />
|
||||
<MudCheckBox @bind-Value="_editingSite.IsActive" Label="Aktiv" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="() => _siteDialogVisible = false">Abbrechen</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSite">Speichern</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
private List<HanaServer> _servers = new();
|
||||
private List<Site> _sites = new();
|
||||
private HanaServer _editingServer = new();
|
||||
private Site _editingSite = new();
|
||||
private bool _serverDialogVisible;
|
||||
private bool _siteDialogVisible;
|
||||
private readonly DialogOptions _dialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadDataAsync();
|
||||
}
|
||||
|
||||
private async Task LoadDataAsync()
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
_servers = await db.HanaServers.OrderBy(s => s.Name).ToListAsync();
|
||||
_sites = await db.Sites.Include(s => s.HanaServer).OrderBy(s => s.Land).ToListAsync();
|
||||
}
|
||||
|
||||
// Server CRUD
|
||||
private void AddServer()
|
||||
{
|
||||
_editingServer = new HanaServer { Port = 30015 };
|
||||
_serverDialogVisible = true;
|
||||
}
|
||||
|
||||
private void EditServer(HanaServer server)
|
||||
{
|
||||
_editingServer = new HanaServer
|
||||
{
|
||||
Id = server.Id,
|
||||
Name = server.Name,
|
||||
Host = server.Host,
|
||||
Port = server.Port,
|
||||
Username = server.Username,
|
||||
Password = server.Password
|
||||
};
|
||||
_serverDialogVisible = true;
|
||||
}
|
||||
|
||||
private async Task SaveServer()
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
if (_editingServer.Id == 0)
|
||||
{
|
||||
db.HanaServers.Add(_editingServer);
|
||||
}
|
||||
else
|
||||
{
|
||||
var existing = await db.HanaServers.FindAsync(_editingServer.Id);
|
||||
if (existing is not null)
|
||||
{
|
||||
existing.Name = _editingServer.Name;
|
||||
existing.Host = _editingServer.Host;
|
||||
existing.Port = _editingServer.Port;
|
||||
existing.Username = _editingServer.Username;
|
||||
existing.Password = _editingServer.Password;
|
||||
}
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
_serverDialogVisible = false;
|
||||
await LoadDataAsync();
|
||||
Snackbar.Add("Server gespeichert", Severity.Success);
|
||||
}
|
||||
|
||||
private async Task DeleteServer(HanaServer server)
|
||||
{
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"Server löschen",
|
||||
$"Server '{server.Name}' wirklich löschen?",
|
||||
yesText: "Löschen", cancelText: "Abbrechen");
|
||||
|
||||
if (result != true) return;
|
||||
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var entity = await db.HanaServers.FindAsync(server.Id);
|
||||
if (entity is not null)
|
||||
{
|
||||
db.HanaServers.Remove(entity);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
await LoadDataAsync();
|
||||
Snackbar.Add("Server gelöscht", Severity.Info);
|
||||
}
|
||||
|
||||
private async Task TestServerConnection(HanaServer server)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Run(() => HanaService.TestConnection(server.Host, server.Port, server.Username, server.Password));
|
||||
Snackbar.Add($"Verbindung zu '{server.Name}' erfolgreich!", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Verbindung fehlgeschlagen: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// Site CRUD
|
||||
private void AddSite()
|
||||
{
|
||||
_editingSite = new Site
|
||||
{
|
||||
IsActive = true,
|
||||
HanaServerId = _servers.FirstOrDefault()?.Id ?? 0
|
||||
};
|
||||
_siteDialogVisible = true;
|
||||
}
|
||||
|
||||
private void EditSite(Site site)
|
||||
{
|
||||
_editingSite = new Site
|
||||
{
|
||||
Id = site.Id,
|
||||
HanaServerId = site.HanaServerId,
|
||||
Schema = site.Schema,
|
||||
TSC = site.TSC,
|
||||
Land = site.Land,
|
||||
IsActive = site.IsActive
|
||||
};
|
||||
_siteDialogVisible = true;
|
||||
}
|
||||
|
||||
private async Task SaveSite()
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
if (_editingSite.Id == 0)
|
||||
{
|
||||
db.Sites.Add(_editingSite);
|
||||
}
|
||||
else
|
||||
{
|
||||
var existing = await db.Sites.FindAsync(_editingSite.Id);
|
||||
if (existing is not null)
|
||||
{
|
||||
existing.HanaServerId = _editingSite.HanaServerId;
|
||||
existing.Schema = _editingSite.Schema;
|
||||
existing.TSC = _editingSite.TSC;
|
||||
existing.Land = _editingSite.Land;
|
||||
existing.IsActive = _editingSite.IsActive;
|
||||
}
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
_siteDialogVisible = false;
|
||||
await LoadDataAsync();
|
||||
Snackbar.Add("Standort gespeichert", Severity.Success);
|
||||
}
|
||||
|
||||
private async Task DeleteSite(Site site)
|
||||
{
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"Standort löschen",
|
||||
$"Standort '{site.Land}' wirklich löschen?",
|
||||
yesText: "Löschen", cancelText: "Abbrechen");
|
||||
|
||||
if (result != true) return;
|
||||
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var entity = await db.Sites.FindAsync(site.Id);
|
||||
if (entity is not null)
|
||||
{
|
||||
db.Sites.Remove(entity);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
await LoadDataAsync();
|
||||
Snackbar.Add("Standort gelöscht", Severity.Info);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user