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:
Claude
2026-04-09 14:00:44 +00:00
parent 2f56082adc
commit 8524631508
23 changed files with 1327 additions and 154 deletions
@@ -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);
}
}