Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec14b838e5 |
-10
@@ -1,10 +0,0 @@
|
||||
# Ignore Visual Studio + build artifacts
|
||||
.vs/
|
||||
TrafagSalesExporter/.vs/
|
||||
TrafagSalesExporter/bin/
|
||||
TrafagSalesExporter/obj/
|
||||
TrafagSalesExporter/*.user
|
||||
TrafagSalesExporter/*.suo
|
||||
TrafagSalesExporter/*.db
|
||||
TrafagSalesExporter/*.db-shm
|
||||
TrafagSalesExporter/*.db-wal
|
||||
@@ -1,8 +0,0 @@
|
||||
# Build artifacts
|
||||
bin/
|
||||
obj/
|
||||
|
||||
# Visual Studio user/IDE files
|
||||
.vs/
|
||||
*.user
|
||||
*.suo
|
||||
@@ -3,15 +3,14 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Trafag Sales Exporter</title>
|
||||
<base href="/" />
|
||||
<link href="css/app.css" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||
<HeadOutlet @rendermode="@Microsoft.AspNetCore.Components.Web.RenderMode.InteractiveServer" />
|
||||
<link href="app.css" rel="stylesheet" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
<body>
|
||||
<Routes @rendermode="@Microsoft.AspNetCore.Components.Web.RenderMode.InteractiveServer" />
|
||||
<Routes />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,38 +1,19 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<MudThemeProvider Theme="_theme" />
|
||||
<MudPopoverProvider />
|
||||
<MudThemeProvider />
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
|
||||
<MudLayout>
|
||||
<MudAppBar Elevation="1" Color="Color.Primary">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start"
|
||||
OnClick="ToggleDrawer" />
|
||||
<MudText Typo="Typo.h6" Class="ml-3">Trafag Sales Exporter</MudText>
|
||||
<MudAppBar Elevation="1">
|
||||
<MudText Typo="Typo.h6">Trafag Sales Exporter</MudText>
|
||||
</MudAppBar>
|
||||
|
||||
<MudDrawer @bind-Open="_drawerOpen" Elevation="2" ClipMode="DrawerClipMode.Always">
|
||||
<MudDrawer Open="true" Variant="DrawerVariant.Mini" Elevation="1">
|
||||
<NavMenu />
|
||||
</MudDrawer>
|
||||
|
||||
<MudMainContent Class="pa-4">
|
||||
<MudMainContent>
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="pa-4">
|
||||
@Body
|
||||
</MudContainer>
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
|
||||
@code {
|
||||
private bool _drawerOpen = true;
|
||||
|
||||
private readonly MudTheme _theme = new()
|
||||
{
|
||||
PaletteLight = new PaletteLight
|
||||
{
|
||||
Primary = "#1565C0",
|
||||
Secondary = "#00897B",
|
||||
AppbarBackground = "#1565C0"
|
||||
}
|
||||
};
|
||||
|
||||
private void ToggleDrawer() => _drawerOpen = !_drawerOpen;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
<MudNavMenu>
|
||||
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">
|
||||
Dashboard
|
||||
</MudNavLink>
|
||||
<MudNavLink Href="/standorte" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.LocationOn">
|
||||
Standorte
|
||||
</MudNavLink>
|
||||
<MudNavLink Href="/transformations" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Transform">
|
||||
Transformationen
|
||||
</MudNavLink>
|
||||
<MudNavLink Href="/settings" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Settings">
|
||||
Settings
|
||||
</MudNavLink>
|
||||
<MudNavLink Href="/logs" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">
|
||||
Logs
|
||||
</MudNavLink>
|
||||
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">Dashboard</MudNavLink>
|
||||
<MudNavLink Href="/standorte" Icon="@Icons.Material.Filled.LocationOn">Standorte</MudNavLink>
|
||||
<MudNavLink Href="/settings" Icon="@Icons.Material.Filled.Settings">Settings</MudNavLink>
|
||||
<MudNavLink Href="/logs" Icon="@Icons.Material.Filled.ReceiptLong">Logs</MudNavLink>
|
||||
</MudNavMenu>
|
||||
|
||||
@@ -1,46 +1,25 @@
|
||||
@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
|
||||
@inject ExportOrchestrationService ExportService
|
||||
|
||||
<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 Row="true" Spacing="2" Class="mb-4">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@isRunningAll" OnClick="ExportAllAsync">Alle exportieren</MudButton>
|
||||
<MudText Typo="Typo.body1">Nächster automatischer Lauf: @nextRunText</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudTable Items="_dashboardRows" Dense Hover Striped Loading="_loading">
|
||||
<MudTable Items="sites" Hover="true" Dense="true">
|
||||
<HeaderContent>
|
||||
<MudTh>Land</MudTh>
|
||||
<MudTh>TSC</MudTh>
|
||||
<MudTh>Schema</MudTh>
|
||||
<MudTh>Server</MudTh>
|
||||
<MudTh>Status</MudTh>
|
||||
<MudTh>Zeilen</MudTh>
|
||||
<MudTh>Letzter Status</MudTh>
|
||||
<MudTh>Row Count</MudTh>
|
||||
<MudTh>Letzter Lauf</MudTh>
|
||||
<MudTh>Dauer</MudTh>
|
||||
<MudTh>Aktion</MudTh>
|
||||
@@ -49,145 +28,90 @@
|
||||
<MudTd>@context.Land</MudTd>
|
||||
<MudTd>@context.TSC</MudTd>
|
||||
<MudTd>@context.Schema</MudTd>
|
||||
<MudTd>@context.ServerName</MudTd>
|
||||
<MudTd>@context.HanaServer?.Name</MudTd>
|
||||
<MudTd>@GetStatusIcon(context.Id)</MudTd>
|
||||
<MudTd>@GetRows(context.Id)</MudTd>
|
||||
<MudTd>@GetLastRun(context.Id)</MudTd>
|
||||
<MudTd>@GetDuration(context.Id)</MudTd>
|
||||
<MudTd>
|
||||
@if (Orchestrator.IsExporting(context.SiteId))
|
||||
@if (runningSiteIds.Contains(context.Id))
|
||||
{
|
||||
<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>
|
||||
<MudProgressCircular Indeterminate="true" Size="Size.Small" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Default">-</MudText>
|
||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" OnClick="() => ExportSingleAsync(context.Id)">Einzeln exportieren</MudButton>
|
||||
}
|
||||
</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;
|
||||
private List<Site> sites = [];
|
||||
private Dictionary<int, ExportLog?> latestLogs = new();
|
||||
private HashSet<int> runningSiteIds = [];
|
||||
private bool isRunningAll;
|
||||
private string nextRunText = "-";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Orchestrator.OnExportStatusChanged += HandleStatusChanged;
|
||||
await LoadDataAsync();
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private async Task LoadDataAsync()
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
_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())
|
||||
await using var db = await DbFactory.CreateDbContextAsync();
|
||||
sites = await db.Sites
|
||||
.Include(x => x.HanaServer)
|
||||
.Where(x => x.IsActive)
|
||||
.OrderBy(x => x.Land)
|
||||
.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;
|
||||
latestLogs = await ExportService.GetLatestLogsPerSiteAsync();
|
||||
var nextRun = await ExportService.GetNextRunAsync();
|
||||
nextRunText = nextRun.HasValue ? nextRun.Value.ToString("dd.MM.yyyy HH:mm") : "Deaktiviert";
|
||||
}
|
||||
|
||||
private async Task ExportAll()
|
||||
private async Task ExportAllAsync()
|
||||
{
|
||||
_anyRunning = true;
|
||||
_ = Task.Run(async () =>
|
||||
isRunningAll = true;
|
||||
foreach (var site in sites)
|
||||
{
|
||||
await Orchestrator.ExportAllAsync();
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
await LoadDataAsync();
|
||||
runningSiteIds.Add(site.Id);
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
});
|
||||
});
|
||||
Snackbar.Add("Export für alle Standorte gestartet", Severity.Info);
|
||||
await ExportService.ExportAllActiveSitesAsync();
|
||||
runningSiteIds.Clear();
|
||||
isRunningAll = false;
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private void ExportSingle(int siteId)
|
||||
private async Task ExportSingleAsync(int siteId)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Orchestrator.ExportSiteByIdAsync(siteId);
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
await LoadDataAsync();
|
||||
runningSiteIds.Add(siteId);
|
||||
StateHasChanged();
|
||||
});
|
||||
});
|
||||
Snackbar.Add("Export gestartet", Severity.Info);
|
||||
await ExportService.ExportSiteAsync(siteId);
|
||||
runningSiteIds.Remove(siteId);
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private async void HandleStatusChanged()
|
||||
private string GetStatusIcon(int siteId)
|
||||
{
|
||||
await InvokeAsync(async () =>
|
||||
if (!latestLogs.TryGetValue(siteId, out var log) || log is null)
|
||||
{
|
||||
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId));
|
||||
StateHasChanged();
|
||||
if (!_anyRunning)
|
||||
{
|
||||
await LoadDataAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
});
|
||||
return "-";
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Orchestrator.OnExportStatusChanged -= HandleStatusChanged;
|
||||
return log.Status == "OK" ? "✅" : "❌";
|
||||
}
|
||||
|
||||
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; } = "";
|
||||
}
|
||||
private string GetRows(int siteId) =>
|
||||
latestLogs.TryGetValue(siteId, out var log) && log is not null ? log.RowCount.ToString() : "-";
|
||||
|
||||
private string GetLastRun(int siteId) =>
|
||||
latestLogs.TryGetValue(siteId, out var log) && log is not null ? log.Timestamp.ToLocalTime().ToString("dd.MM.yyyy HH:mm:ss") : "-";
|
||||
|
||||
private string GetDuration(int siteId) =>
|
||||
latestLogs.TryGetValue(siteId, out var log) && log is not null ? $"{log.DurationSeconds:F1}s" : "-";
|
||||
}
|
||||
|
||||
@@ -1,134 +1,94 @@
|
||||
@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>
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Logs</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||
<MudStack Row AlignItems="AlignItems.Center" Spacing="3">
|
||||
<MudSelect @bind-Value="_filterLand" Label="Land" Clearable Style="max-width:200px;">
|
||||
@foreach (var land in _availableLands)
|
||||
{
|
||||
<MudSelectItem Value="@land">@land</MudSelectItem>
|
||||
}
|
||||
<MudGrid Class="mb-4">
|
||||
<MudItem xs="12" md="3"><MudTextField Label="Land" @bind-Value="filterLand" /></MudItem>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudSelect T="string" Label="Status" @bind-Value="filterStatus">
|
||||
<MudSelectItem Value="">Alle</MudSelectItem>
|
||||
<MudSelectItem Value="OK">OK</MudSelectItem>
|
||||
<MudSelectItem Value="Error">Error</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudSelect @bind-Value="_filterStatus" Label="Status" Clearable Style="max-width:150px;">
|
||||
<MudSelectItem Value="@("OK")">OK</MudSelectItem>
|
||||
<MudSelectItem Value="@("Error")">Error</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudDatePicker @bind-Date="_filterDate" Label="Datum" Clearable 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>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="3"><MudDatePicker Label="Ab Datum" @bind-Date="filterFromDate" /></MudItem>
|
||||
<MudItem xs="12" md="3"><MudButton Variant="Variant.Filled" OnClick="LoadAsync">Filtern</MudButton></MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudStack Row="true" Spacing="2" Class="mb-2">
|
||||
<MudNumericField T="int" Label="Logs älter als Tage löschen" @bind-Value="deleteOlderThanDays" Min="1" />
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Error" OnClick="DeleteOlderAsync">Logs löschen</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudTable Items="_logs" Dense Hover Striped Loading="_loading">
|
||||
<MudTable Items="logs" Dense="true" Hover="true" RowClassFunc="GetRowClass">
|
||||
<HeaderContent>
|
||||
<MudTh>Zeitpunkt</MudTh>
|
||||
<MudTh>Timestamp</MudTh>
|
||||
<MudTh>Land</MudTh>
|
||||
<MudTh>TSC</MudTh>
|
||||
<MudTh>Status</MudTh>
|
||||
<MudTh>Zeilen</MudTh>
|
||||
<MudTh>Rows</MudTh>
|
||||
<MudTh>Dauer</MudTh>
|
||||
<MudTh>Dateiname</MudTh>
|
||||
<MudTh>Fehler</MudTh>
|
||||
<MudTh>Dateiname</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Timestamp.ToString("dd.MM.yyyy HH:mm:ss")</MudTd>
|
||||
<MudTd>@context.Timestamp.ToLocalTime().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.Status</MudTd>
|
||||
<MudTd>@context.RowCount</MudTd>
|
||||
<MudTd>@($"{context.DurationSeconds:F1}s")</MudTd>
|
||||
<MudTd>@context.ErrorMessage</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;
|
||||
private List<ExportLog> logs = [];
|
||||
private string filterLand = string.Empty;
|
||||
private string filterStatus = string.Empty;
|
||||
private DateTime? filterFromDate;
|
||||
private int deleteOlderThanDays = 30;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
protected override async Task OnInitializedAsync() => await LoadAsync();
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
_availableLands = await db.ExportLogs.Select(l => l.Land).Distinct().OrderBy(l => l).ToListAsync();
|
||||
await LoadLogsAsync();
|
||||
await using var db = await DbFactory.CreateDbContextAsync();
|
||||
var query = db.ExportLogs.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filterLand))
|
||||
{
|
||||
query = query.Where(x => x.Land.Contains(filterLand));
|
||||
}
|
||||
|
||||
private async Task LoadLogsAsync()
|
||||
if (!string.IsNullOrWhiteSpace(filterStatus))
|
||||
{
|
||||
_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;
|
||||
query = query.Where(x => x.Status == filterStatus);
|
||||
}
|
||||
|
||||
private async Task ApplyFilter()
|
||||
if (filterFromDate.HasValue)
|
||||
{
|
||||
await LoadLogsAsync();
|
||||
var fromUtc = filterFromDate.Value.Date.ToUniversalTime();
|
||||
query = query.Where(x => x.Timestamp >= fromUtc);
|
||||
}
|
||||
|
||||
private async Task DeleteOldLogs()
|
||||
logs = await query.OrderByDescending(x => x.Timestamp).ToListAsync();
|
||||
}
|
||||
|
||||
private string GetRowClass(ExportLog log, int _) => log.Status == "Error" ? "mud-theme-error" : string.Empty;
|
||||
|
||||
private async Task DeleteOlderAsync()
|
||||
{
|
||||
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();
|
||||
var threshold = DateTime.UtcNow.AddDays(-deleteOlderThanDays);
|
||||
await using var db = await DbFactory.CreateDbContextAsync();
|
||||
var oldLogs = await db.ExportLogs.Where(x => x.Timestamp < threshold).ToListAsync();
|
||||
db.ExportLogs.RemoveRange(oldLogs);
|
||||
var count = await db.SaveChangesAsync();
|
||||
await LoadLogsAsync();
|
||||
Snackbar.Add($"{oldLogs.Count} alte Logs gelöscht", Severity.Info);
|
||||
await db.SaveChangesAsync();
|
||||
await LoadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,164 +1,93 @@
|
||||
@page "/settings"
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using TrafagSalesExporter.Data
|
||||
@using TrafagSalesExporter.Services
|
||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
||||
@inject SharePointUploadService SpService
|
||||
@inject TimerBackgroundService TimerService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject CryptoService CryptoService
|
||||
@inject SharePointUploadService SharePointUploadService
|
||||
|
||||
<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">
|
||||
<MudPaper Class="pa-4 mb-4">
|
||||
<MudText Typo="Typo.h6">SharePoint</MudText>
|
||||
<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>
|
||||
<MudItem xs="12" md="6"><MudTextField Label="SiteUrl" @bind-Value="sharePointConfig.SiteUrl" /></MudItem>
|
||||
<MudItem xs="12" md="6"><MudTextField Label="ExportFolder" @bind-Value="sharePointConfig.ExportFolder" /></MudItem>
|
||||
<MudItem xs="12" md="4"><MudTextField Label="TenantId" @bind-Value="sharePointConfig.TenantId" /></MudItem>
|
||||
<MudItem xs="12" md="4"><MudTextField Label="ClientId" @bind-Value="sharePointConfig.ClientId" /></MudItem>
|
||||
<MudItem xs="12" md="4"><MudTextField Label="ClientSecret" InputType="InputType.Password" @bind-Value="sharePointClientSecret" /></MudItem>
|
||||
</MudGrid>
|
||||
<MudStack Row="true" Spacing="2" Class="mt-3">
|
||||
<MudButton Variant="Variant.Filled" OnClick="SaveAsync">Speichern</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" OnClick="TestSharePointAsync">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">
|
||||
<MudPaper Class="pa-4 mb-4">
|
||||
<MudText Typo="Typo.h6">Export & Timer</MudText>
|
||||
<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>
|
||||
<MudItem xs="12" md="3"><MudTextField Label="DateFilter" @bind-Value="settings.DateFilter" /></MudItem>
|
||||
<MudItem xs="12" md="2"><MudNumericField T="int" Label="TimerHour" Min="0" Max="23" @bind-Value="settings.TimerHour" /></MudItem>
|
||||
<MudItem xs="12" md="2"><MudNumericField T="int" Label="TimerMinute" Min="0" Max="59" @bind-Value="settings.TimerMinute" /></MudItem>
|
||||
<MudItem xs="12" md="2"><MudCheckBox Label="TimerEnabled" @bind-Value="settings.TimerEnabled" /></MudItem>
|
||||
</MudGrid>
|
||||
<MudText Typo="Typo.body2" Class="mt-3">Dateiname-Vorschau: @PreviewFileName</MudText>
|
||||
</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>
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined">@message</MudAlert>
|
||||
|
||||
@code {
|
||||
private SharePointConfig _spConfig = new();
|
||||
private ExportSettings _exportSettings = new();
|
||||
private bool _testingSp;
|
||||
private SharePointConfig sharePointConfig = new();
|
||||
private ExportSettings settings = new();
|
||||
private string sharePointClientSecret = string.Empty;
|
||||
private string message = "Bereit.";
|
||||
|
||||
private string PreviewFileName => $"Sales_{{TSC}}_{DateTime.UtcNow:yyyy-MM-dd}.xlsx";
|
||||
|
||||
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();
|
||||
await using var db = await DbFactory.CreateDbContextAsync();
|
||||
sharePointConfig = await db.SharePointConfigs.OrderBy(x => x.Id).FirstAsync();
|
||||
settings = await db.ExportSettings.OrderBy(x => x.Id).FirstAsync();
|
||||
sharePointClientSecret = CryptoService.Decrypt(sharePointConfig.EncryptedClientSecret);
|
||||
}
|
||||
|
||||
private async Task SaveSharePoint()
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
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 using var db = await DbFactory.CreateDbContextAsync();
|
||||
var sp = await db.SharePointConfigs.SingleAsync(x => x.Id == sharePointConfig.Id);
|
||||
var es = await db.ExportSettings.SingleAsync(x => x.Id == settings.Id);
|
||||
|
||||
sp.SiteUrl = sharePointConfig.SiteUrl;
|
||||
sp.ExportFolder = sharePointConfig.ExportFolder;
|
||||
sp.TenantId = sharePointConfig.TenantId;
|
||||
sp.ClientId = sharePointConfig.ClientId;
|
||||
sp.EncryptedClientSecret = CryptoService.Encrypt(sharePointClientSecret);
|
||||
|
||||
es.DateFilter = settings.DateFilter;
|
||||
es.TimerHour = settings.TimerHour;
|
||||
es.TimerMinute = settings.TimerMinute;
|
||||
es.TimerEnabled = settings.TimerEnabled;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
Snackbar.Add("SharePoint Konfiguration gespeichert", Severity.Success);
|
||||
message = "Settings gespeichert.";
|
||||
}
|
||||
|
||||
private async Task TestSharePoint()
|
||||
private async Task TestSharePointAsync()
|
||||
{
|
||||
_testingSp = true;
|
||||
try
|
||||
{
|
||||
await SpService.TestConnectionAsync(
|
||||
_spConfig.TenantId, _spConfig.ClientId, _spConfig.ClientSecret, _spConfig.SiteUrl);
|
||||
Snackbar.Add("SharePoint Verbindung erfolgreich!", Severity.Success);
|
||||
var ok = await SharePointUploadService.TestConnectionAsync(
|
||||
sharePointConfig.SiteUrl,
|
||||
sharePointConfig.TenantId,
|
||||
sharePointConfig.ClientId,
|
||||
sharePointClientSecret);
|
||||
|
||||
message = ok ? "SharePoint Verbindung OK." : "SharePoint Verbindung fehlgeschlagen.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Verbindung fehlgeschlagen: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_testingSp = false;
|
||||
message = $"SharePoint Test fehlgeschlagen: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,74 @@
|
||||
@page "/standorte"
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using TrafagSalesExporter.Data
|
||||
@using TrafagSalesExporter.Services
|
||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
||||
@inject HanaQueryService HanaService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
@inject HanaQueryService HanaQueryService
|
||||
@inject CryptoService CryptoService
|
||||
|
||||
<PageTitle>Standorte</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Standorte</MudText>
|
||||
|
||||
<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>
|
||||
<MudPaper Class="pa-4 mb-4">
|
||||
<MudText Typo="Typo.h6">Neuen Standort hinzufügen</MudText>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="3"><MudSelect T="int" Label="Server" @bind-Value="newSite.HanaServerId">@foreach (var srv in servers) { <MudSelectItem Value="@srv.Id">@srv.Name</MudSelectItem> }</MudSelect></MudItem>
|
||||
<MudItem xs="12" md="2"><MudTextField Label="Schema" @bind-Value="newSite.Schema" /></MudItem>
|
||||
<MudItem xs="12" md="2"><MudTextField Label="TSC" @bind-Value="newSite.TSC" /></MudItem>
|
||||
<MudItem xs="12" md="3"><MudTextField Label="Land" @bind-Value="newSite.Land" /></MudItem>
|
||||
<MudItem xs="12" md="1"><MudCheckBox Label="Aktiv" @bind-Value="newSite.IsActive" /></MudItem>
|
||||
<MudItem xs="12" md="1"><MudButton Variant="Variant.Filled" OnClick="AddSiteAsync">Speichern</MudButton></MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
<MudTable Items="_servers" Dense Hover Striped>
|
||||
<MudTable Items="sites" Dense="true" Hover="true" Class="mb-6">
|
||||
<HeaderContent>
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh>Host</MudTh>
|
||||
<MudTh>Port</MudTh>
|
||||
<MudTh>Username</MudTh>
|
||||
<MudTh>Verbindungsstatus</MudTh>
|
||||
<MudTh>Aktionen</MudTh>
|
||||
<MudTh>Land</MudTh><MudTh>TSC</MudTh><MudTh>Schema</MudTh><MudTh>Server</MudTh><MudTh>Aktiv</MudTh><MudTh>Aktion</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Land</MudTd>
|
||||
<MudTd>@context.TSC</MudTd>
|
||||
<MudTd>@context.Schema</MudTd>
|
||||
<MudTd>@context.HanaServer?.Name</MudTd>
|
||||
<MudTd>@(context.IsActive ? "Ja" : "Nein")</MudTd>
|
||||
<MudTd>
|
||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" OnClick="() => EditSite(context)">Edit</MudButton>
|
||||
<MudButton Size="Size.Small" Color="Color.Error" Variant="Variant.Text" OnClick="() => DeleteSiteAsync(context.Id)">Delete</MudButton>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
|
||||
@if (editingSite is not null)
|
||||
{
|
||||
<MudPaper Class="pa-4 mb-4">
|
||||
<MudText Typo="Typo.h6">Standort bearbeiten</MudText>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="3"><MudSelect T="int" Label="Server" @bind-Value="editingSite.HanaServerId">@foreach (var srv in servers) { <MudSelectItem Value="@srv.Id">@srv.Name</MudSelectItem> }</MudSelect></MudItem>
|
||||
<MudItem xs="12" md="2"><MudTextField Label="Schema" @bind-Value="editingSite.Schema" /></MudItem>
|
||||
<MudItem xs="12" md="2"><MudTextField Label="TSC" @bind-Value="editingSite.TSC" /></MudItem>
|
||||
<MudItem xs="12" md="3"><MudTextField Label="Land" @bind-Value="editingSite.Land" /></MudItem>
|
||||
<MudItem xs="12" md="1"><MudCheckBox Label="Aktiv" @bind-Value="editingSite.IsActive" /></MudItem>
|
||||
<MudItem xs="12" md="1"><MudButton Variant="Variant.Filled" OnClick="SaveSiteAsync">Update</MudButton></MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudDivider Class="my-4" />
|
||||
<MudText Typo="Typo.h5" Class="mb-3">HANA Server</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mb-4">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="2"><MudTextField Label="Name" @bind-Value="newServer.Name" /></MudItem>
|
||||
<MudItem xs="12" md="3"><MudTextField Label="Host" @bind-Value="newServer.Host" /></MudItem>
|
||||
<MudItem xs="12" md="1"><MudNumericField T="int" Label="Port" @bind-Value="newServer.Port" /></MudItem>
|
||||
<MudItem xs="12" md="2"><MudTextField Label="Username" @bind-Value="newServer.Username" /></MudItem>
|
||||
<MudItem xs="12" md="2"><MudTextField Label="Password" InputType="InputType.Password" @bind-Value="newServerPassword" /></MudItem>
|
||||
<MudItem xs="12" md="2"><MudButton Variant="Variant.Filled" OnClick="AddServerAsync">Server speichern</MudButton></MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
<MudTable Items="servers" Dense="true" Hover="true">
|
||||
<HeaderContent>
|
||||
<MudTh>Name</MudTh><MudTh>Host</MudTh><MudTh>Port</MudTh><MudTh>Username</MudTh><MudTh>Aktion</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Name</MudTd>
|
||||
@@ -33,321 +76,140 @@
|
||||
<MudTd>@context.Port</MudTd>
|
||||
<MudTd>@context.Username</MudTd>
|
||||
<MudTd>
|
||||
@if (_connectionStatus.TryGetValue(context.Id, out var status))
|
||||
{
|
||||
<MudTooltip Text="@BuildStatusTooltip(status)">
|
||||
<MudChip Color="@(status.Success ? Color.Success : Color.Error)" Variant="Variant.Filled" Size="Size.Small">
|
||||
@(status.Success ? "OK" : "Fehler") - @status.Stage
|
||||
</MudChip>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip Color="Color.Default" Variant="Variant.Outlined" Size="Size.Small">Nicht getestet</MudChip>
|
||||
}
|
||||
</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)" />
|
||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" OnClick="() => TestServerAsync(context)">Verbindung testen</MudButton>
|
||||
<MudButton Size="Size.Small" Color="Color.Error" Variant="Variant.Text" OnClick="() => DeleteServerAsync(context.Id)">Delete</MudButton>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
|
||||
<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>Quellsystem</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.SourceSystem</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>
|
||||
|
||||
<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
|
||||
HelperText="IP oder Hostname (ohne Protokoll)" />
|
||||
<MudNumericField @bind-Value="_editingServer.Port" Label="Port"
|
||||
HelperText="Typisch 30015 (Tenant), 30013 (SystemDB), 3xx15 für Instanz xx" />
|
||||
<MudTextField @bind-Value="_editingServer.Username" Label="Username" />
|
||||
<MudTextField @bind-Value="_editingServer.Password" Label="Password" InputType="InputType.Password" />
|
||||
<MudTextField @bind-Value="_editingServer.DatabaseName" Label="Database Name (MDC)"
|
||||
HelperText="Nur bei Multi-Tenant Setup angeben, sonst leer lassen" />
|
||||
<MudSwitch @bind-Value="_editingServer.UseSsl" Label="SSL/TLS verwenden (encrypt=true)" Color="Color.Primary" />
|
||||
<MudSwitch @bind-Value="_editingServer.ValidateCertificate" Label="SSL-Zertifikat validieren" Color="Color.Primary"
|
||||
Disabled="!_editingServer.UseSsl" />
|
||||
<MudTextField @bind-Value="_editingServer.AdditionalParams" Label="Zusätzliche Parameter"
|
||||
HelperText="Optional, z.B. sslCryptoProvider=openssl;communicationTimeout=0" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="() => _serverDialogVisible = false">Abbrechen</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveServer">Speichern</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
<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 />
|
||||
<MudSelect @bind-Value="_editingSite.SourceSystem" Label="Quellsystem" Required>
|
||||
@foreach (var system in _sourceSystems)
|
||||
{
|
||||
<MudSelectItem Value="system">@system</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<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>
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Class="mt-4">@message</MudAlert>
|
||||
|
||||
@code {
|
||||
private readonly string[] _sourceSystems = ["SAP", "BI1", "SAGE"];
|
||||
private readonly Dictionary<int, ConnectionTestResult> _connectionStatus = new();
|
||||
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 };
|
||||
private List<Site> sites = [];
|
||||
private List<HanaServer> servers = [];
|
||||
private Site newSite = new() { IsActive = true };
|
||||
private Site? editingSite;
|
||||
private HanaServer newServer = new() { Port = 30015 };
|
||||
private string newServerPassword = string.Empty;
|
||||
private string message = "Bereit.";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadDataAsync();
|
||||
}
|
||||
protected override async Task OnInitializedAsync() => await LoadAsync();
|
||||
|
||||
private async Task LoadDataAsync()
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
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();
|
||||
}
|
||||
await using var db = await DbFactory.CreateDbContextAsync();
|
||||
servers = await db.HanaServers.OrderBy(x => x.Name).ToListAsync();
|
||||
sites = await db.Sites.Include(x => x.HanaServer).OrderBy(x => x.Land).ToListAsync();
|
||||
|
||||
private void AddServer()
|
||||
if (servers.Count > 0 && newSite.HanaServerId == 0)
|
||||
{
|
||||
_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,
|
||||
DatabaseName = server.DatabaseName,
|
||||
UseSsl = server.UseSsl,
|
||||
ValidateCertificate = server.ValidateCertificate,
|
||||
AdditionalParams = server.AdditionalParams
|
||||
};
|
||||
_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;
|
||||
existing.DatabaseName = _editingServer.DatabaseName;
|
||||
existing.UseSsl = _editingServer.UseSsl;
|
||||
existing.ValidateCertificate = _editingServer.ValidateCertificate;
|
||||
existing.AdditionalParams = _editingServer.AdditionalParams;
|
||||
newSite.HanaServerId = servers[0].Id;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddSiteAsync()
|
||||
{
|
||||
await using var db = await DbFactory.CreateDbContextAsync();
|
||||
db.Sites.Add(new Site
|
||||
{
|
||||
HanaServerId = newSite.HanaServerId,
|
||||
Schema = newSite.Schema,
|
||||
TSC = newSite.TSC,
|
||||
Land = newSite.Land,
|
||||
IsActive = newSite.IsActive
|
||||
});
|
||||
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)
|
||||
{
|
||||
var result = await Task.Run(() => HanaService.TestConnectionDetailed(server));
|
||||
_connectionStatus[server.Id] = result;
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
Snackbar.Add($"Verbindung zu '{server.Name}' erfolgreich.", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add($"{server.Name}: {result.ExceptionType} - {result.ErrorMessage}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildStatusTooltip(ConnectionTestResult status)
|
||||
{
|
||||
var stamp = status.TestedAtUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
|
||||
if (status.Success)
|
||||
return $"Letzter Test: {stamp}\nStage: {status.Stage}\n{status.ConnectionStringPreview}";
|
||||
|
||||
return $"Letzter Test: {stamp}\nStage: {status.Stage}\nFehler: {status.ErrorMessage}\n{status.ConnectionStringPreview}";
|
||||
}
|
||||
|
||||
private void AddSite()
|
||||
{
|
||||
_editingSite = new Site
|
||||
{
|
||||
IsActive = true,
|
||||
SourceSystem = "SAP",
|
||||
HanaServerId = _servers.FirstOrDefault()?.Id ?? 0
|
||||
};
|
||||
_siteDialogVisible = true;
|
||||
newSite = new Site { IsActive = true, HanaServerId = servers.FirstOrDefault()?.Id ?? 0 };
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private void EditSite(Site site)
|
||||
{
|
||||
_editingSite = new Site
|
||||
editingSite = new Site
|
||||
{
|
||||
Id = site.Id,
|
||||
HanaServerId = site.HanaServerId,
|
||||
Schema = site.Schema,
|
||||
TSC = site.TSC,
|
||||
Land = site.Land,
|
||||
SourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem,
|
||||
IsActive = site.IsActive
|
||||
};
|
||||
_siteDialogVisible = true;
|
||||
}
|
||||
|
||||
private async Task SaveSite()
|
||||
private async Task SaveSiteAsync()
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
if (_editingSite.Id == 0)
|
||||
if (editingSite is null)
|
||||
{
|
||||
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.SourceSystem = _editingSite.SourceSystem;
|
||||
existing.IsActive = _editingSite.IsActive;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await using var db = await DbFactory.CreateDbContextAsync();
|
||||
var entity = await db.Sites.SingleAsync(x => x.Id == editingSite.Id);
|
||||
entity.HanaServerId = editingSite.HanaServerId;
|
||||
entity.Schema = editingSite.Schema;
|
||||
entity.TSC = editingSite.TSC;
|
||||
entity.Land = editingSite.Land;
|
||||
entity.IsActive = editingSite.IsActive;
|
||||
await db.SaveChangesAsync();
|
||||
_siteDialogVisible = false;
|
||||
await LoadDataAsync();
|
||||
Snackbar.Add("Standort gespeichert", Severity.Success);
|
||||
editingSite = null;
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private async Task DeleteSite(Site site)
|
||||
private async Task DeleteSiteAsync(int id)
|
||||
{
|
||||
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 using var db = await DbFactory.CreateDbContextAsync();
|
||||
var site = await db.Sites.SingleAsync(x => x.Id == id);
|
||||
db.Sites.Remove(site);
|
||||
await db.SaveChangesAsync();
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
await LoadDataAsync();
|
||||
Snackbar.Add("Standort gelöscht", Severity.Info);
|
||||
private async Task AddServerAsync()
|
||||
{
|
||||
await using var db = await DbFactory.CreateDbContextAsync();
|
||||
db.HanaServers.Add(new HanaServer
|
||||
{
|
||||
Name = newServer.Name,
|
||||
Host = newServer.Host,
|
||||
Port = newServer.Port,
|
||||
Username = newServer.Username,
|
||||
EncryptedPassword = CryptoService.Encrypt(newServerPassword)
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
newServer = new HanaServer { Port = 30015 };
|
||||
newServerPassword = string.Empty;
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private async Task DeleteServerAsync(int id)
|
||||
{
|
||||
await using var db = await DbFactory.CreateDbContextAsync();
|
||||
var isUsed = await db.Sites.AnyAsync(x => x.HanaServerId == id);
|
||||
if (isUsed)
|
||||
{
|
||||
message = "Server kann nicht gelöscht werden, solange Sites darauf zeigen.";
|
||||
return;
|
||||
}
|
||||
|
||||
var server = await db.HanaServers.SingleAsync(x => x.Id == id);
|
||||
db.HanaServers.Remove(server);
|
||||
await db.SaveChangesAsync();
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private async Task TestServerAsync(HanaServer server)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ok = HanaQueryService.TestConnection(server.Host, server.Port, server.Username, CryptoService.Decrypt(server.EncryptedPassword));
|
||||
message = ok ? $"Verbindung OK: {server.Name}" : $"Verbindung fehlgeschlagen: {server.Name}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
message = $"Verbindung fehlgeschlagen: {ex.Message}";
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
@page "/transformations"
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using System.Reflection
|
||||
@using TrafagSalesExporter.Data
|
||||
@using TrafagSalesExporter.Models
|
||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>Transformationen</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Transformer Ansicht</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mb-4">Definiere pro Quellsystem (SAP, BI1, SAGE) Feld-Remapping und Transformationen.</MudText>
|
||||
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudStack Row="true" Spacing="2" Class="mb-3">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="AddRule">
|
||||
Regel hinzufügen
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" StartIcon="@Icons.Material.Filled.Save" OnClick="SaveAllAsync">
|
||||
Alle speichern
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudTable Items="_rules" Dense Hover Striped>
|
||||
<HeaderContent>
|
||||
<MudTh>Aktiv</MudTh>
|
||||
<MudTh>System</MudTh>
|
||||
<MudTh>Source</MudTh>
|
||||
<MudTh>Target</MudTh>
|
||||
<MudTh>Typ</MudTh>
|
||||
<MudTh>Argument</MudTh>
|
||||
<MudTh>Sort</MudTh>
|
||||
<MudTh>Aktionen</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd><MudCheckBox @bind-Value="context.IsActive" /></MudTd>
|
||||
<MudTd>
|
||||
<MudSelect T="string" Value="@context.SourceSystem" ValueChanged="@(v => context.SourceSystem = v)" Dense>
|
||||
@foreach (var system in _systems)
|
||||
{
|
||||
<MudSelectItem Value="system">@system</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudSelect T="string" Value="@context.SourceField" ValueChanged="@(v => context.SourceField = v)" Dense>
|
||||
@foreach (var field in _recordFields)
|
||||
{
|
||||
<MudSelectItem Value="field">@field</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudSelect T="string" Value="@context.TargetField" ValueChanged="@(v => context.TargetField = v)" Dense>
|
||||
@foreach (var field in _recordFields)
|
||||
{
|
||||
<MudSelectItem Value="field">@field</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudSelect T="string" Value="@context.TransformationType" ValueChanged="@(v => context.TransformationType = v)" Dense>
|
||||
@foreach (var type in _types)
|
||||
{
|
||||
<MudSelectItem Value="type">@type</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudTextField Value="@context.Argument" ValueChanged="@(v => context.Argument = v)" Dense
|
||||
HelperText="Replace: alt=>neu" />
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudNumericField T="int" Value="@context.SortOrder" ValueChanged="@(v => context.SortOrder = v)" Dense />
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small"
|
||||
OnClick="() => RemoveRule(context)" />
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
private readonly string[] _systems = ["SAP", "BI1", "SAGE"];
|
||||
private readonly string[] _types = ["Copy", "Uppercase", "Lowercase", "Prefix", "Suffix", "Replace", "Constant"];
|
||||
private readonly string[] _recordFields = typeof(SalesRecord)
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Select(p => p.Name)
|
||||
.OrderBy(n => n)
|
||||
.ToArray();
|
||||
|
||||
private List<FieldTransformationRule> _rules = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
_rules = await db.FieldTransformationRules.OrderBy(r => r.SortOrder).ThenBy(r => r.Id).ToListAsync();
|
||||
}
|
||||
|
||||
private void AddRule()
|
||||
{
|
||||
var nextSort = _rules.Count == 0 ? 10 : _rules.Max(r => r.SortOrder) + 10;
|
||||
_rules.Add(new FieldTransformationRule
|
||||
{
|
||||
SourceSystem = "SAP",
|
||||
SourceField = nameof(SalesRecord.Material),
|
||||
TargetField = nameof(SalesRecord.Material),
|
||||
TransformationType = "Copy",
|
||||
SortOrder = nextSort,
|
||||
IsActive = true
|
||||
});
|
||||
}
|
||||
|
||||
private void RemoveRule(FieldTransformationRule rule)
|
||||
{
|
||||
_rules.Remove(rule);
|
||||
}
|
||||
|
||||
private async Task SaveAllAsync()
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
db.FieldTransformationRules.RemoveRange(db.FieldTransformationRules);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
db.FieldTransformationRules.AddRange(_rules);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Snackbar.Add("Transformationsregeln gespeichert.", Severity.Success);
|
||||
await LoadAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
@using TrafagSalesExporter.Components.Layout
|
||||
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using MudBlazor
|
||||
@using TrafagSalesExporter
|
||||
@using TrafagSalesExporter.Components
|
||||
@using TrafagSalesExporter.Components.Layout
|
||||
@using TrafagSalesExporter.Models
|
||||
@using TrafagSalesExporter.Services
|
||||
@using TrafagSalesExporter.Data
|
||||
|
||||
@@ -1,107 +1,91 @@
|
||||
using System.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Models;
|
||||
using TrafagSalesExporter.Services;
|
||||
|
||||
namespace TrafagSalesExporter.Data;
|
||||
|
||||
public class AppDbContext : DbContext
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||
{
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<HanaServer> HanaServers => Set<HanaServer>();
|
||||
public DbSet<Site> Sites => Set<Site>();
|
||||
public DbSet<SharePointConfig> SharePointConfigs => Set<SharePointConfig>();
|
||||
public DbSet<ExportSettings> ExportSettings => Set<ExportSettings>();
|
||||
public DbSet<ExportLog> ExportLogs => Set<ExportLog>();
|
||||
public DbSet<FieldTransformationRule> FieldTransformationRules => Set<FieldTransformationRule>();
|
||||
|
||||
/// <summary>
|
||||
/// Fügt Spalten zu existierenden Tabellen hinzu, die bei neueren Versionen
|
||||
/// hinzugekommen sind. EnsureCreated aktualisiert das Schema nicht automatisch.
|
||||
/// </summary>
|
||||
public static void EnsureSchema(AppDbContext db)
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
AddColumnIfMissing(db, "HanaServers", "DatabaseName", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "HanaServers", "UseSsl", "INTEGER NOT NULL DEFAULT 0");
|
||||
AddColumnIfMissing(db, "HanaServers", "ValidateCertificate", "INTEGER NOT NULL DEFAULT 0");
|
||||
AddColumnIfMissing(db, "HanaServers", "AdditionalParams", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "SourceSystem", "TEXT NOT NULL DEFAULT 'SAP'");
|
||||
EnsureTransformationTable(db);
|
||||
}
|
||||
modelBuilder.Entity<HanaServer>().HasIndex(x => x.Name).IsUnique();
|
||||
|
||||
private static void AddColumnIfMissing(AppDbContext db, string table, string column, string type)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != ConnectionState.Open) conn.Open();
|
||||
modelBuilder.Entity<Site>()
|
||||
.HasOne(x => x.HanaServer)
|
||||
.WithMany(x => x.Sites)
|
||||
.HasForeignKey(x => x.HanaServerId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
bool exists = false;
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = $"PRAGMA table_info({table})";
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
if (string.Equals(reader["name"]?.ToString(), column, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists)
|
||||
{
|
||||
using var alter = conn.CreateCommand();
|
||||
alter.CommandText = $"ALTER TABLE {table} ADD COLUMN {column} {type}";
|
||||
alter.ExecuteNonQuery();
|
||||
modelBuilder.Entity<ExportLog>()
|
||||
.HasOne(x => x.Site)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.SiteId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureTransformationTable(AppDbContext db)
|
||||
public static class DbInitializer
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != ConnectionState.Open) conn.Open();
|
||||
public static async Task SeedDefaultsAsync(AppDbContext db, CryptoService cryptoService)
|
||||
{
|
||||
if (!await db.HanaServers.AnyAsync())
|
||||
{
|
||||
db.HanaServers.AddRange(
|
||||
new HanaServer
|
||||
{
|
||||
Name = "Internal",
|
||||
Host = "travtrp0",
|
||||
Port = 30015,
|
||||
Username = string.Empty,
|
||||
EncryptedPassword = cryptoService.Encrypt(string.Empty)
|
||||
},
|
||||
new HanaServer
|
||||
{
|
||||
Name = "India",
|
||||
Host = "20.197.20.60",
|
||||
Port = 30015,
|
||||
Username = string.Empty,
|
||||
EncryptedPassword = cryptoService.Encrypt(string.Empty)
|
||||
});
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
CREATE TABLE IF NOT EXISTS FieldTransformationRules (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
SourceSystem TEXT NOT NULL DEFAULT 'SAP',
|
||||
SourceField TEXT NOT NULL,
|
||||
TargetField TEXT NOT NULL,
|
||||
TransformationType TEXT NOT NULL,
|
||||
Argument TEXT NOT NULL DEFAULT '',
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public static void SeedIfEmpty(AppDbContext db)
|
||||
if (!await db.Sites.AnyAsync())
|
||||
{
|
||||
if (db.HanaServers.Any()) return;
|
||||
|
||||
var serverInternal = new HanaServer { Name = "Internal", Host = "travtrp0", Port = 30015, Username = "", Password = "" };
|
||||
var serverIndia = new HanaServer { Name = "India", Host = "20.197.20.60", Port = 30015, Username = "", Password = "" };
|
||||
db.HanaServers.AddRange(serverInternal, serverIndia);
|
||||
db.SaveChanges();
|
||||
var internalServer = await db.HanaServers.SingleAsync(x => x.Name == "Internal");
|
||||
var indiaServer = await db.HanaServers.SingleAsync(x => x.Name == "India");
|
||||
|
||||
db.Sites.AddRange(
|
||||
new Site { HanaServerId = serverInternal.Id, Schema = "fr01_p", TSC = "TRFR", Land = "Frankreich", IsActive = true },
|
||||
new Site { HanaServerId = serverInternal.Id, Schema = "it01_p", TSC = "TRIT", Land = "Italien", IsActive = true },
|
||||
new Site { HanaServerId = serverInternal.Id, Schema = "us01_p", TSC = "TRUS", Land = "USA", IsActive = true },
|
||||
new Site { HanaServerId = serverIndia.Id, Schema = "TRAFAG_LIVE", TSC = "TRIN", Land = "Indien", IsActive = true }
|
||||
);
|
||||
new Site { HanaServerId = internalServer.Id, Schema = "fr01_p", TSC = "TRFR", Land = "Frankreich", IsActive = true },
|
||||
new Site { HanaServerId = internalServer.Id, Schema = "it01_p", TSC = "TRIT", Land = "Italien", IsActive = true },
|
||||
new Site { HanaServerId = internalServer.Id, Schema = "us01_p", TSC = "TRUS", Land = "USA", IsActive = true },
|
||||
new Site { HanaServerId = indiaServer.Id, Schema = "TRAFAG_LIVE", TSC = "TRIN", Land = "Indien", IsActive = true });
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
if (!await db.SharePointConfigs.AnyAsync())
|
||||
{
|
||||
db.SharePointConfigs.Add(new SharePointConfig
|
||||
{
|
||||
SiteUrl = "https://trafagag.sharepoint.com/sites/WorldwideBIPlatform",
|
||||
ExportFolder = "/Shared Documents/Exports/",
|
||||
TenantId = "",
|
||||
ClientId = "",
|
||||
ClientSecret = ""
|
||||
TenantId = string.Empty,
|
||||
ClientId = string.Empty,
|
||||
EncryptedClientSecret = cryptoService.Encrypt(string.Empty)
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
if (!await db.ExportSettings.AnyAsync())
|
||||
{
|
||||
db.ExportSettings.Add(new ExportSettings
|
||||
{
|
||||
DateFilter = "2025-01-01",
|
||||
@@ -109,7 +93,7 @@ CREATE TABLE IF NOT EXISTS FieldTransformationRules (
|
||||
TimerMinute = 0,
|
||||
TimerEnabled = true
|
||||
});
|
||||
|
||||
db.SaveChanges();
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace TrafagSalesExporter.Models;
|
||||
|
||||
public class ExportLog
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
public int SiteId { get; set; }
|
||||
|
||||
[ForeignKey(nameof(SiteId))]
|
||||
public int? SiteId { get; set; }
|
||||
public Site? Site { get; set; }
|
||||
|
||||
public string Land { get; set; } = string.Empty;
|
||||
public string TSC { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
@@ -5,6 +5,6 @@ public class ExportSettings
|
||||
public int Id { get; set; }
|
||||
public string DateFilter { get; set; } = "2025-01-01";
|
||||
public int TimerHour { get; set; } = 3;
|
||||
public int TimerMinute { get; set; }
|
||||
public int TimerMinute { get; set; } = 0;
|
||||
public bool TimerEnabled { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TrafagSalesExporter.Models;
|
||||
|
||||
public class FieldTransformationRule
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public string SourceSystem { get; set; } = "SAP";
|
||||
|
||||
[Required]
|
||||
public string SourceField { get; set; } = nameof(SalesRecord.Material);
|
||||
|
||||
[Required]
|
||||
public string TargetField { get; set; } = nameof(SalesRecord.Material);
|
||||
|
||||
[Required]
|
||||
public string TransformationType { get; set; } = "Copy";
|
||||
|
||||
public string Argument { get; set; } = string.Empty;
|
||||
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
@@ -12,73 +12,11 @@ public class HanaServer
|
||||
[Required]
|
||||
public string Host { get; set; } = string.Empty;
|
||||
|
||||
public int Port { get; set; } = 30015;
|
||||
public int Port { get; set; }
|
||||
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public string EncryptedPassword { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Name der Tenant-Datenbank bei Multi-Tenant Database Container (MDC) Setups.
|
||||
/// Leer lassen, wenn direkt auf einen Tenant-Port verbunden wird.
|
||||
/// </summary>
|
||||
public string DatabaseName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// SSL/TLS Verschlüsselung aktivieren (encrypt=true).
|
||||
/// </summary>
|
||||
public bool UseSsl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SSL-Zertifikat validieren. Bei self-signed Zertifikaten auf false setzen.
|
||||
/// </summary>
|
||||
public bool ValidateCertificate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Zusätzliche Verbindungsparameter (Semikolon-getrennt), z.B. "sslCryptoProvider=openssl".
|
||||
/// </summary>
|
||||
public string AdditionalParams { get; set; } = string.Empty;
|
||||
|
||||
public string BuildConnectionString()
|
||||
{
|
||||
var parts = new List<string>
|
||||
{
|
||||
$"ServerNode={Host}:{Port}",
|
||||
$"UserName={Username}",
|
||||
$"Password={Password}"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(DatabaseName))
|
||||
parts.Add($"DatabaseName={DatabaseName}");
|
||||
|
||||
if (UseSsl)
|
||||
{
|
||||
parts.Add("encrypt=true");
|
||||
parts.Add($"sslValidateCertificate={(ValidateCertificate ? "true" : "false")}");
|
||||
public List<Site> Sites { get; set; } = [];
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(AdditionalParams))
|
||||
parts.Add(AdditionalParams.Trim().Trim(';'));
|
||||
|
||||
return string.Join(";", parts);
|
||||
}
|
||||
|
||||
public string GetConnectionStringPreview()
|
||||
{
|
||||
var pwdMasked = string.IsNullOrEmpty(Password) ? "" : "***";
|
||||
var copy = new HanaServer
|
||||
{
|
||||
Host = Host,
|
||||
Port = Port,
|
||||
Username = Username,
|
||||
Password = pwdMasked,
|
||||
DatabaseName = DatabaseName,
|
||||
UseSsl = UseSsl,
|
||||
ValidateCertificate = ValidateCertificate,
|
||||
AdditionalParams = AdditionalParams
|
||||
};
|
||||
|
||||
return copy.BuildConnectionString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace TrafagSalesExporter.Models;
|
||||
public class SalesRecord
|
||||
{
|
||||
public DateTime ExtractionDate { get; set; }
|
||||
public string Tsc { get; set; } = string.Empty;
|
||||
public string TSC { get; set; } = string.Empty;
|
||||
public string InvoiceNumber { get; set; } = string.Empty;
|
||||
public int PositionOnInvoice { get; set; }
|
||||
public string Material { get; set; } = string.Empty;
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TrafagSalesExporter.Models;
|
||||
|
||||
public class SharePointConfig
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public string SiteUrl { get; set; } = string.Empty;
|
||||
public string ExportFolder { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string ExportFolder { get; set; } = "/Shared Documents/Exports/";
|
||||
|
||||
[Required]
|
||||
public string TenantId { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
public string ClientSecret { get; set; } = string.Empty;
|
||||
|
||||
public string EncryptedClientSecret { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace TrafagSalesExporter.Models;
|
||||
|
||||
public class Site
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int HanaServerId { get; set; }
|
||||
|
||||
[ForeignKey(nameof(HanaServerId))]
|
||||
public HanaServer? HanaServer { get; set; }
|
||||
|
||||
[Required]
|
||||
@@ -21,8 +18,5 @@ public class Site
|
||||
[Required]
|
||||
public string Land { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string SourceSystem { get; set; } = "SAP";
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -13,30 +13,31 @@ builder.Services.AddMudServices();
|
||||
builder.Services.AddDbContextFactory<AppDbContext>(options =>
|
||||
options.UseSqlite("Data Source=trafag_exporter.db"));
|
||||
|
||||
builder.Services.AddSingleton<HanaQueryService>();
|
||||
builder.Services.AddSingleton<ExcelExportService>();
|
||||
builder.Services.AddSingleton<SharePointUploadService>();
|
||||
builder.Services.AddSingleton<RecordTransformationService>();
|
||||
builder.Services.AddSingleton<ExportOrchestrationService>();
|
||||
builder.Services.AddSingleton<TimerBackgroundService>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<TimerBackgroundService>());
|
||||
builder.Services.AddScoped<CryptoService>();
|
||||
builder.Services.AddScoped<HanaQueryService>();
|
||||
builder.Services.AddScoped<ExcelExportService>();
|
||||
builder.Services.AddScoped<SharePointUploadService>();
|
||||
builder.Services.AddScoped<ExportOrchestrationService>();
|
||||
builder.Services.AddHostedService<TimerBackgroundService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var dbFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<AppDbContext>>();
|
||||
using var db = await dbFactory.CreateDbContextAsync();
|
||||
var cryptoService = scope.ServiceProvider.GetRequiredService<CryptoService>();
|
||||
await using var db = await dbFactory.CreateDbContextAsync();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
AppDbContext.EnsureSchema(db);
|
||||
AppDbContext.SeedIfEmpty(db);
|
||||
await DbInitializer.SeedDefaultsAsync(db, cryptoService);
|
||||
}
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseAntiforgery();
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"profiles": {
|
||||
"TrafagSalesExporter": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:55415;http://localhost:55416"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class CryptoService
|
||||
{
|
||||
public string Encrypt(string plainText)
|
||||
{
|
||||
var input = Encoding.UTF8.GetBytes(plainText ?? string.Empty);
|
||||
var protectedBytes = ProtectedData.Protect(input, null, DataProtectionScope.CurrentUser);
|
||||
return Convert.ToBase64String(protectedBytes);
|
||||
}
|
||||
|
||||
public string Decrypt(string cipherText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cipherText))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var input = Convert.FromBase64String(cipherText);
|
||||
var unprotectedBytes = ProtectedData.Unprotect(input, null, DataProtectionScope.CurrentUser);
|
||||
return Encoding.UTF8.GetString(unprotectedBytes);
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,16 @@ namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class ExcelExportService
|
||||
{
|
||||
public string CreateExcelFile(string outputDirectory, string tsc, DateTime fileDate, List<SalesRecord> records)
|
||||
public string CreateFile(string baseDirectory, string land, string tsc, List<SalesRecord> records)
|
||||
{
|
||||
var outputDirectory = Path.Combine(baseDirectory, "exports", land);
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
var fileName = $"Sales_{tsc}_{fileDate:yyyy-MM-dd}.xlsx";
|
||||
var fullPath = Path.Combine(outputDirectory, fileName);
|
||||
|
||||
var fileName = $"Sales_{tsc}_{DateTime.UtcNow:yyyy-MM-dd}.xlsx";
|
||||
var filePath = Path.Combine(outputDirectory, fileName);
|
||||
|
||||
using var workbook = new XLWorkbook();
|
||||
var ws = workbook.Worksheets.Add("Sales");
|
||||
var ws = workbook.AddWorksheet("Sales");
|
||||
|
||||
var headers = new[]
|
||||
{
|
||||
@@ -51,39 +53,39 @@ public class ExcelExportService
|
||||
}
|
||||
|
||||
var row = 2;
|
||||
foreach (var record in records)
|
||||
foreach (var r in records)
|
||||
{
|
||||
ws.Cell(row, 1).Value = record.ExtractionDate.ToString("dd.MM.yyyy HH:mm:ss");
|
||||
ws.Cell(row, 2).Value = record.Tsc;
|
||||
ws.Cell(row, 3).Value = record.InvoiceNumber;
|
||||
ws.Cell(row, 4).Value = record.PositionOnInvoice;
|
||||
ws.Cell(row, 5).Value = record.Material;
|
||||
ws.Cell(row, 6).Value = record.Name;
|
||||
ws.Cell(row, 7).Value = record.ProductGroup;
|
||||
ws.Cell(row, 8).Value = record.Quantity;
|
||||
ws.Cell(row, 9).Value = record.SupplierNumber;
|
||||
ws.Cell(row, 10).Value = record.SupplierName;
|
||||
ws.Cell(row, 11).Value = record.SupplierCountry;
|
||||
ws.Cell(row, 12).Value = record.CustomerNumber;
|
||||
ws.Cell(row, 13).Value = record.CustomerName;
|
||||
ws.Cell(row, 14).Value = record.CustomerCountry;
|
||||
ws.Cell(row, 15).Value = record.CustomerIndustry;
|
||||
ws.Cell(row, 16).Value = record.StandardCost;
|
||||
ws.Cell(row, 17).Value = record.StandardCostCurrency;
|
||||
ws.Cell(row, 18).Value = record.PurchaseOrderNumber;
|
||||
ws.Cell(row, 19).Value = record.SalesPriceValue;
|
||||
ws.Cell(row, 20).Value = record.SalesCurrency;
|
||||
ws.Cell(row, 21).Value = record.Incoterms2020;
|
||||
ws.Cell(row, 22).Value = record.SalesResponsibleEmployee;
|
||||
ws.Cell(row, 23).Value = record.InvoiceDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
||||
ws.Cell(row, 24).Value = record.OrderDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
||||
ws.Cell(row, 25).Value = record.Land;
|
||||
ws.Cell(row, 26).Value = record.DocumentType;
|
||||
ws.Cell(row, 1).Value = r.ExtractionDate.ToString("dd.MM.yyyy HH:mm:ss");
|
||||
ws.Cell(row, 2).Value = r.TSC;
|
||||
ws.Cell(row, 3).Value = r.InvoiceNumber;
|
||||
ws.Cell(row, 4).Value = r.PositionOnInvoice;
|
||||
ws.Cell(row, 5).Value = r.Material;
|
||||
ws.Cell(row, 6).Value = r.Name;
|
||||
ws.Cell(row, 7).Value = r.ProductGroup;
|
||||
ws.Cell(row, 8).Value = r.Quantity;
|
||||
ws.Cell(row, 9).Value = r.SupplierNumber;
|
||||
ws.Cell(row, 10).Value = r.SupplierName;
|
||||
ws.Cell(row, 11).Value = r.SupplierCountry;
|
||||
ws.Cell(row, 12).Value = r.CustomerNumber;
|
||||
ws.Cell(row, 13).Value = r.CustomerName;
|
||||
ws.Cell(row, 14).Value = r.CustomerCountry;
|
||||
ws.Cell(row, 15).Value = r.CustomerIndustry;
|
||||
ws.Cell(row, 16).Value = r.StandardCost;
|
||||
ws.Cell(row, 17).Value = r.StandardCostCurrency;
|
||||
ws.Cell(row, 18).Value = r.PurchaseOrderNumber;
|
||||
ws.Cell(row, 19).Value = r.SalesPriceValue;
|
||||
ws.Cell(row, 20).Value = r.SalesCurrency;
|
||||
ws.Cell(row, 21).Value = r.Incoterms2020;
|
||||
ws.Cell(row, 22).Value = r.SalesResponsibleEmployee;
|
||||
ws.Cell(row, 23).Value = r.InvoiceDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
||||
ws.Cell(row, 24).Value = r.OrderDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
||||
ws.Cell(row, 25).Value = r.Land;
|
||||
ws.Cell(row, 26).Value = r.DocumentType;
|
||||
row++;
|
||||
}
|
||||
|
||||
ws.Columns().AdjustToContents();
|
||||
workbook.SaveAs(fullPath);
|
||||
return fullPath;
|
||||
workbook.SaveAs(filePath);
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,171 +1,120 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Diagnostics;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class ExportOrchestrationService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
private readonly HanaQueryService _hanaService;
|
||||
private readonly ExcelExportService _excelService;
|
||||
private readonly SharePointUploadService _sharePointService;
|
||||
private readonly RecordTransformationService _transformationService;
|
||||
private readonly ILogger<ExportOrchestrationService> _logger;
|
||||
|
||||
public event Action? OnExportStatusChanged;
|
||||
|
||||
private readonly Dictionary<int, string> _runningExports = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
public ExportOrchestrationService(
|
||||
public class ExportOrchestrationService(
|
||||
IDbContextFactory<AppDbContext> dbFactory,
|
||||
HanaQueryService hanaService,
|
||||
ExcelExportService excelService,
|
||||
SharePointUploadService sharePointService,
|
||||
RecordTransformationService transformationService,
|
||||
ILogger<ExportOrchestrationService> logger)
|
||||
CryptoService cryptoService,
|
||||
HanaQueryService hanaQueryService,
|
||||
ExcelExportService excelExportService,
|
||||
SharePointUploadService sharePointUploadService)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_hanaService = hanaService;
|
||||
_excelService = excelService;
|
||||
_sharePointService = sharePointService;
|
||||
_transformationService = transformationService;
|
||||
_logger = logger;
|
||||
}
|
||||
public async Task ExportAllActiveSitesAsync(CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
var siteIds = await db.Sites.Where(x => x.IsActive).Select(x => x.Id).ToListAsync(ct);
|
||||
|
||||
public bool IsExporting(int siteId)
|
||||
foreach (var siteId in siteIds)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _runningExports.ContainsKey(siteId);
|
||||
await ExportSiteAsync(siteId, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetExportStatus(int siteId)
|
||||
public async Task ExportSiteAsync(int siteId, CancellationToken ct = default)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _runningExports.TryGetValue(siteId, out var status) ? status : string.Empty;
|
||||
}
|
||||
}
|
||||
var started = DateTime.UtcNow;
|
||||
|
||||
public async Task ExportAllAsync()
|
||||
{
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var sites = await db.Sites.Include(s => s.HanaServer).Where(s => s.IsActive).ToListAsync();
|
||||
foreach (var site in sites)
|
||||
{
|
||||
await ExportSiteAsync(site);
|
||||
}
|
||||
}
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
var site = await db.Sites.Include(x => x.HanaServer).SingleAsync(x => x.Id == siteId, ct);
|
||||
var settings = await db.ExportSettings.OrderBy(x => x.Id).FirstAsync(ct);
|
||||
var sp = await db.SharePointConfigs.OrderBy(x => x.Id).FirstAsync(ct);
|
||||
|
||||
public async Task ExportSiteByIdAsync(int siteId)
|
||||
{
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var site = await db.Sites.Include(s => s.HanaServer).FirstOrDefaultAsync(s => s.Id == siteId);
|
||||
if (site is null) return;
|
||||
await ExportSiteAsync(site);
|
||||
}
|
||||
|
||||
private async Task ExportSiteAsync(Site site)
|
||||
{
|
||||
if (site.HanaServer is null) return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_runningExports.ContainsKey(site.Id)) return;
|
||||
_runningExports[site.Id] = "HANA Abfrage...";
|
||||
}
|
||||
NotifyChanged();
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var log = new ExportLog
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
SiteId = site.Id,
|
||||
Land = site.Land,
|
||||
TSC = site.TSC
|
||||
TSC = site.TSC,
|
||||
Status = "Error",
|
||||
RowCount = 0,
|
||||
FileName = string.Empty,
|
||||
DurationSeconds = 0
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var settings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
|
||||
var spConfig = await db.SharePointConfigs.FirstOrDefaultAsync();
|
||||
var hanaServer = site.HanaServer ?? throw new InvalidOperationException("HANA Server fehlt.");
|
||||
var hanaPassword = cryptoService.Decrypt(hanaServer.EncryptedPassword);
|
||||
var clientSecret = cryptoService.Decrypt(sp.EncryptedClientSecret);
|
||||
|
||||
UpdateStatus(site.Id, "HANA Abfrage...");
|
||||
var records = await Task.Run(() => _hanaService.GetSalesRecords(
|
||||
site.HanaServer, site.Schema, site.TSC, site.Land, settings.DateFilter));
|
||||
var records = hanaQueryService.QuerySales(
|
||||
hanaServer.Host,
|
||||
hanaServer.Port,
|
||||
hanaServer.Username,
|
||||
hanaPassword,
|
||||
site.Schema,
|
||||
site.TSC,
|
||||
site.Land,
|
||||
settings.DateFilter);
|
||||
|
||||
UpdateStatus(site.Id, "Transformationen anwenden...");
|
||||
var rules = await db.FieldTransformationRules
|
||||
.Where(r => r.IsActive && r.SourceSystem == (string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem))
|
||||
.OrderBy(r => r.SortOrder)
|
||||
.ToListAsync();
|
||||
_transformationService.Apply(records, rules);
|
||||
var filePath = excelExportService.CreateFile(AppContext.BaseDirectory, site.Land, site.TSC, records);
|
||||
|
||||
UpdateStatus(site.Id, "Excel erstellen...");
|
||||
var outputDir = Path.Combine(AppContext.BaseDirectory, "output");
|
||||
var filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records);
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
await sharePointUploadService.UploadAsync(
|
||||
sp.SiteUrl,
|
||||
sp.ExportFolder,
|
||||
sp.TenantId,
|
||||
sp.ClientId,
|
||||
clientSecret,
|
||||
site.Land,
|
||||
filePath);
|
||||
|
||||
if (spConfig is not null &&
|
||||
!string.IsNullOrWhiteSpace(spConfig.TenantId) &&
|
||||
!string.IsNullOrWhiteSpace(spConfig.ClientId) &&
|
||||
!string.IsNullOrWhiteSpace(spConfig.ClientSecret))
|
||||
{
|
||||
UpdateStatus(site.Id, "SharePoint Upload...");
|
||||
await _sharePointService.UploadAsync(
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret,
|
||||
spConfig.SiteUrl, spConfig.ExportFolder, site.Land, filePath);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
log.Status = "OK";
|
||||
log.RowCount = records.Count;
|
||||
log.FileName = fileName;
|
||||
log.DurationSeconds = sw.Elapsed.TotalSeconds;
|
||||
|
||||
_logger.LogInformation("Export OK: {Land} ({TSC}) - {Rows} Zeilen in {Duration:F1}s",
|
||||
site.Land, site.TSC, records.Count, sw.Elapsed.TotalSeconds);
|
||||
log.FileName = Path.GetFileName(filePath);
|
||||
log.ErrorMessage = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
log.Status = "Error";
|
||||
log.ErrorMessage = ex.Message;
|
||||
log.FileName = string.Empty;
|
||||
log.DurationSeconds = sw.Elapsed.TotalSeconds;
|
||||
|
||||
_logger.LogError(ex, "Export Fehler: {Land} ({TSC})", site.Land, site.TSC);
|
||||
}
|
||||
finally
|
||||
{
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
log.DurationSeconds = (DateTime.UtcNow - started).TotalSeconds;
|
||||
db.ExportLogs.Add(log);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_runningExports.Remove(site.Id);
|
||||
}
|
||||
NotifyChanged();
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStatus(int siteId, string status)
|
||||
public async Task<DateTime?> GetNextRunAsync(CancellationToken ct = default)
|
||||
{
|
||||
lock (_lock)
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
var settings = await db.ExportSettings.OrderBy(x => x.Id).FirstOrDefaultAsync(ct);
|
||||
if (settings is null || !settings.TimerEnabled)
|
||||
{
|
||||
_runningExports[siteId] = status;
|
||||
}
|
||||
NotifyChanged();
|
||||
return null;
|
||||
}
|
||||
|
||||
private void NotifyChanged()
|
||||
var now = DateTime.Now;
|
||||
var next = new DateTime(now.Year, now.Month, now.Day, settings.TimerHour, settings.TimerMinute, 0);
|
||||
if (next <= now)
|
||||
{
|
||||
OnExportStatusChanged?.Invoke();
|
||||
next = next.AddDays(1);
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<int, ExportLog?>> GetLatestLogsPerSiteAsync(CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
var grouped = await db.ExportLogs
|
||||
.OrderByDescending(x => x.Timestamp)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return grouped
|
||||
.GroupBy(x => x.SiteId ?? 0)
|
||||
.ToDictionary(g => g.Key, g => g.FirstOrDefault());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,20 +5,19 @@ namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class HanaQueryService
|
||||
{
|
||||
public List<SalesRecord> GetSalesRecords(HanaServer server,
|
||||
string schema, string tsc, string land, string dateFilter)
|
||||
public List<SalesRecord> QuerySales(string host, int port, string username, string password, string schema, string tsc, string land, string dateFilter)
|
||||
{
|
||||
var connectionString = server.BuildConnectionString();
|
||||
var connectionString = $"ServerNode={host}:{port};UserName={username};Password={password}";
|
||||
var result = new List<SalesRecord>();
|
||||
|
||||
using var connection = new HanaConnection(connectionString);
|
||||
connection.Open();
|
||||
|
||||
var invoiceQuery = GetInvoiceQuery(schema, tsc, dateFilter);
|
||||
var creditNoteQuery = GetCreditNoteQuery(schema, tsc, dateFilter);
|
||||
var creditQuery = GetCreditNoteQuery(schema, tsc, dateFilter);
|
||||
|
||||
result.AddRange(ReadRecords(connection, invoiceQuery, land));
|
||||
result.AddRange(ReadRecords(connection, creditNoteQuery, land));
|
||||
result.AddRange(Read(connection, invoiceQuery, land));
|
||||
result.AddRange(Read(connection, creditQuery, land));
|
||||
|
||||
foreach (var record in result)
|
||||
{
|
||||
@@ -32,58 +31,26 @@ public class HanaQueryService
|
||||
return result;
|
||||
}
|
||||
|
||||
public ConnectionTestResult TestConnectionDetailed(HanaServer server)
|
||||
public bool TestConnection(string host, int port, string username, string password)
|
||||
{
|
||||
var testResult = new ConnectionTestResult
|
||||
{
|
||||
TestedAtUtc = DateTime.UtcNow,
|
||||
ConnectionStringPreview = server.GetConnectionStringPreview(),
|
||||
Stage = "Verbindungsaufbau"
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var connectionString = server.BuildConnectionString();
|
||||
var connectionString = $"ServerNode={host}:{port};UserName={username};Password={password}";
|
||||
using var connection = new HanaConnection(connectionString);
|
||||
connection.Open();
|
||||
|
||||
testResult.Stage = "Ping-Query";
|
||||
using var command = new HanaCommand("SELECT 1 FROM DUMMY", connection);
|
||||
command.ExecuteScalar();
|
||||
|
||||
testResult.Success = true;
|
||||
testResult.Stage = "OK";
|
||||
return testResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
testResult.Success = false;
|
||||
testResult.ErrorMessage = ex.Message;
|
||||
testResult.ExceptionType = ex.GetType().Name;
|
||||
return testResult;
|
||||
}
|
||||
return connection.State == System.Data.ConnectionState.Open;
|
||||
}
|
||||
|
||||
public void TestConnection(HanaServer server)
|
||||
{
|
||||
var connectionString = server.BuildConnectionString();
|
||||
using var connection = new HanaConnection(connectionString);
|
||||
connection.Open();
|
||||
}
|
||||
|
||||
private static List<SalesRecord> ReadRecords(HanaConnection connection, string query, string land)
|
||||
private static List<SalesRecord> Read(HanaConnection connection, string query, string land)
|
||||
{
|
||||
var records = new List<SalesRecord>();
|
||||
|
||||
using var command = new HanaCommand(query, connection);
|
||||
using var reader = command.ExecuteReader();
|
||||
using var cmd = new HanaCommand(query, connection);
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
records.Add(new SalesRecord
|
||||
{
|
||||
ExtractionDate = reader.GetDateTime(reader.GetOrdinal("extraction_date")),
|
||||
Tsc = reader.GetString(reader.GetOrdinal("tsc")),
|
||||
TSC = reader["tsc"]?.ToString() ?? string.Empty,
|
||||
InvoiceNumber = reader["invoice_number"]?.ToString() ?? string.Empty,
|
||||
PositionOnInvoice = Convert.ToInt32(reader["invoice_position"]),
|
||||
InvoiceDate = reader.IsDBNull(reader.GetOrdinal("invoice_date")) ? null : reader.GetDateTime(reader.GetOrdinal("invoice_date")),
|
||||
@@ -205,13 +172,3 @@ LEFT JOIN {schema}.""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode""
|
||||
WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= '{dateFilter}'
|
||||
ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum""";
|
||||
}
|
||||
|
||||
public class ConnectionTestResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public DateTime TestedAtUtc { get; set; }
|
||||
public string Stage { get; set; } = string.Empty;
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
public string ExceptionType { get; set; } = string.Empty;
|
||||
public string ConnectionStringPreview { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
using System.Reflection;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class RecordTransformationService
|
||||
{
|
||||
private static readonly Dictionary<string, PropertyInfo> PropertyMap = typeof(SalesRecord)
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.ToDictionary(p => p.Name, p => p, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public void Apply(List<SalesRecord> records, IEnumerable<FieldTransformationRule> rules)
|
||||
{
|
||||
var orderedRules = rules.Where(r => r.IsActive).OrderBy(r => r.SortOrder).ToList();
|
||||
if (orderedRules.Count == 0 || records.Count == 0) return;
|
||||
|
||||
foreach (var record in records)
|
||||
{
|
||||
foreach (var rule in orderedRules)
|
||||
{
|
||||
ApplyRule(record, rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyRule(SalesRecord record, FieldTransformationRule rule)
|
||||
{
|
||||
if (!PropertyMap.TryGetValue(rule.SourceField, out var sourceProp)) return;
|
||||
if (!PropertyMap.TryGetValue(rule.TargetField, out var targetProp)) return;
|
||||
|
||||
var sourceValue = sourceProp.GetValue(record);
|
||||
object? result = rule.TransformationType switch
|
||||
{
|
||||
"Copy" => sourceValue,
|
||||
"Uppercase" => sourceValue?.ToString()?.ToUpperInvariant(),
|
||||
"Lowercase" => sourceValue?.ToString()?.ToLowerInvariant(),
|
||||
"Prefix" => $"{rule.Argument}{sourceValue}",
|
||||
"Suffix" => $"{sourceValue}{rule.Argument}",
|
||||
"Replace" => ApplyReplace(sourceValue?.ToString(), rule.Argument),
|
||||
"Constant" => rule.Argument,
|
||||
_ => sourceValue
|
||||
};
|
||||
|
||||
SetPropertyValue(record, targetProp, result);
|
||||
}
|
||||
|
||||
private static string ApplyReplace(string? input, string? argument)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input)) return string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(argument)) return input;
|
||||
|
||||
var parts = argument.Split("=>", 2, StringSplitOptions.TrimEntries);
|
||||
if (parts.Length != 2) return input;
|
||||
return input.Replace(parts[0], parts[1], StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static void SetPropertyValue(SalesRecord record, PropertyInfo property, object? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (property.PropertyType == typeof(string))
|
||||
{
|
||||
property.SetValue(record, value?.ToString() ?? string.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
if (property.PropertyType == typeof(int))
|
||||
{
|
||||
if (int.TryParse(value?.ToString(), out var parsedInt)) property.SetValue(record, parsedInt);
|
||||
return;
|
||||
}
|
||||
|
||||
if (property.PropertyType == typeof(decimal))
|
||||
{
|
||||
if (decimal.TryParse(value?.ToString(), out var parsedDecimal)) property.SetValue(record, parsedDecimal);
|
||||
return;
|
||||
}
|
||||
|
||||
if (property.PropertyType == typeof(DateTime?) || property.PropertyType == typeof(DateTime))
|
||||
{
|
||||
if (DateTime.TryParse(value?.ToString(), out var parsedDate)) property.SetValue(record, parsedDate);
|
||||
return;
|
||||
}
|
||||
|
||||
property.SetValue(record, value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// skip invalid conversion to keep export running
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,95 @@
|
||||
using Azure.Identity;
|
||||
using Microsoft.Graph;
|
||||
using Microsoft.Graph.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class SharePointUploadService
|
||||
{
|
||||
public async Task UploadAsync(string tenantId, string clientId, string clientSecret,
|
||||
string siteUrl, string exportFolder, string land, string localFilePath)
|
||||
public async Task UploadAsync(string siteUrl, string exportFolder, string tenantId, string clientId, string clientSecret, string land, string localFilePath)
|
||||
{
|
||||
var credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
|
||||
var graphClient = new GraphServiceClient(credential, ["https://graph.microsoft.com/.default"]);
|
||||
var graph = CreateGraphClient(tenantId, clientId, clientSecret);
|
||||
var (siteId, driveId) = await ResolveSiteAndDriveAsync(graph, siteUrl);
|
||||
|
||||
var uri = new Uri(siteUrl);
|
||||
var sitePath = uri.AbsolutePath;
|
||||
var site = await graphClient.Sites[$"{uri.Host}:{sitePath}"].GetAsync();
|
||||
|
||||
if (site?.Id is null)
|
||||
throw new InvalidOperationException("SharePoint Site konnte nicht gefunden werden.");
|
||||
|
||||
var drive = await graphClient.Sites[site.Id].Drive.GetAsync();
|
||||
if (drive?.Id is null)
|
||||
throw new InvalidOperationException("SharePoint Dokumentenbibliothek konnte nicht gefunden werden.");
|
||||
var folderPath = $"{exportFolder.Trim('/')}/{land}";
|
||||
await EnsureFolderPathAsync(graph, driveId, folderPath);
|
||||
|
||||
var fileName = Path.GetFileName(localFilePath);
|
||||
var folderPath = exportFolder.Trim('/').Trim();
|
||||
var remotePath = $"{folderPath}/{land}/{fileName}";
|
||||
var remotePath = $"{folderPath}/{fileName}";
|
||||
|
||||
await using var stream = File.OpenRead(localFilePath);
|
||||
await graphClient.Drives[drive.Id].Root.ItemWithPath(remotePath).Content.PutAsync(stream);
|
||||
await graph.Drives[driveId].Root.ItemWithPath(remotePath).Content.PutAsync(stream);
|
||||
}
|
||||
|
||||
public async Task TestConnectionAsync(string tenantId, string clientId, string clientSecret, string siteUrl)
|
||||
public async Task<bool> TestConnectionAsync(string siteUrl, string tenantId, string clientId, string clientSecret)
|
||||
{
|
||||
var graph = CreateGraphClient(tenantId, clientId, clientSecret);
|
||||
var (siteId, _) = await ResolveSiteAndDriveAsync(graph, siteUrl);
|
||||
return !string.IsNullOrWhiteSpace(siteId);
|
||||
}
|
||||
|
||||
private static GraphServiceClient CreateGraphClient(string tenantId, string clientId, string clientSecret)
|
||||
{
|
||||
var credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
|
||||
var graphClient = new GraphServiceClient(credential, ["https://graph.microsoft.com/.default"]);
|
||||
return new GraphServiceClient(credential, ["https://graph.microsoft.com/.default"]);
|
||||
}
|
||||
|
||||
private static async Task<(string siteId, string driveId)> ResolveSiteAndDriveAsync(GraphServiceClient graph, string siteUrl)
|
||||
{
|
||||
var uri = new Uri(siteUrl);
|
||||
var sitePath = uri.AbsolutePath;
|
||||
var site = await graphClient.Sites[$"{uri.Host}:{sitePath}"].GetAsync();
|
||||
|
||||
var site = await graph.Sites[$"{uri.Host}:{uri.AbsolutePath}"].GetAsync();
|
||||
if (site?.Id is null)
|
||||
throw new InvalidOperationException("SharePoint Site konnte nicht gefunden werden.");
|
||||
{
|
||||
throw new InvalidOperationException("SharePoint Site nicht gefunden.");
|
||||
}
|
||||
|
||||
var drive = await graph.Sites[site.Id].Drive.GetAsync();
|
||||
if (drive?.Id is null)
|
||||
{
|
||||
throw new InvalidOperationException("SharePoint Dokumentenbibliothek nicht gefunden.");
|
||||
}
|
||||
|
||||
return (site.Id, drive.Id);
|
||||
}
|
||||
|
||||
private static async Task EnsureFolderPathAsync(GraphServiceClient graph, string driveId, string folderPath)
|
||||
{
|
||||
var segments = folderPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
var currentPath = string.Empty;
|
||||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
currentPath = string.IsNullOrEmpty(currentPath) ? segment : $"{currentPath}/{segment}";
|
||||
|
||||
try
|
||||
{
|
||||
_ = await graph.Drives[driveId].Root.ItemWithPath(currentPath).GetAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
var parentPath = currentPath.Contains('/')
|
||||
? currentPath[..currentPath.LastIndexOf('/')]
|
||||
: string.Empty;
|
||||
|
||||
var parent = string.IsNullOrEmpty(parentPath)
|
||||
? await graph.Drives[driveId].Root.GetAsync()
|
||||
: await graph.Drives[driveId].Root.ItemWithPath(parentPath).GetAsync();
|
||||
|
||||
if (parent?.Id is null)
|
||||
{
|
||||
throw new InvalidOperationException("SharePoint Parent-Ordner konnte nicht ermittelt werden.");
|
||||
}
|
||||
|
||||
await graph.Drives[driveId].Items[parent.Id].Children.PostAsync(new DriveItem
|
||||
{
|
||||
Name = segment,
|
||||
Folder = new Folder(),
|
||||
AdditionalData = new Dictionary<string, object>
|
||||
{
|
||||
["@microsoft.graph.conflictBehavior"] = "replace"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,65 +3,56 @@ using TrafagSalesExporter.Data;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class TimerBackgroundService : BackgroundService
|
||||
public class TimerBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<TimerBackgroundService> logger) : BackgroundService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<TimerBackgroundService> _logger;
|
||||
private DateTime _nextRun = DateTime.MaxValue;
|
||||
|
||||
public DateTime NextRun => _nextRun;
|
||||
|
||||
public TimerBackgroundService(IServiceProvider serviceProvider, ILogger<TimerBackgroundService> logger)
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Recalculate()
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
_ = RecalculateNextRunAsync();
|
||||
}
|
||||
|
||||
private async Task RecalculateNextRunAsync()
|
||||
try
|
||||
{
|
||||
var dbFactory = _serviceProvider.GetRequiredService<IDbContextFactory<AppDbContext>>();
|
||||
using var db = await dbFactory.CreateDbContextAsync();
|
||||
var settings = await db.ExportSettings.FirstOrDefaultAsync();
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var dbFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<AppDbContext>>();
|
||||
var exportService = scope.ServiceProvider.GetRequiredService<ExportOrchestrationService>();
|
||||
|
||||
await using var db = await dbFactory.CreateDbContextAsync(stoppingToken);
|
||||
var settings = await db.ExportSettings.OrderBy(x => x.Id).FirstOrDefaultAsync(stoppingToken);
|
||||
|
||||
if (settings is null || !settings.TimerEnabled)
|
||||
{
|
||||
_nextRun = DateTime.MaxValue;
|
||||
return;
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||
continue;
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
var todayRun = new DateTime(now.Year, now.Month, now.Day, settings.TimerHour, settings.TimerMinute, 0);
|
||||
_nextRun = todayRun <= now ? todayRun.AddDays(1) : todayRun;
|
||||
var nextRun = new DateTime(now.Year, now.Month, now.Day, settings.TimerHour, settings.TimerMinute, 0);
|
||||
if (nextRun <= now)
|
||||
{
|
||||
nextRun = nextRun.AddDays(1);
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
var delay = nextRun - now;
|
||||
logger.LogInformation("Nächster automatischer Export um {NextRun}", nextRun);
|
||||
await Task.Delay(delay, stoppingToken);
|
||||
|
||||
if (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await RecalculateNextRunAsync();
|
||||
break;
|
||||
}
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
await exportService.ExportAllActiveSitesAsync(stoppingToken);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
|
||||
|
||||
if (DateTime.Now < _nextRun) continue;
|
||||
|
||||
_logger.LogInformation("Timer-Export gestartet um {Time}", DateTime.Now);
|
||||
|
||||
try
|
||||
{
|
||||
var orchestrator = _serviceProvider.GetRequiredService<ExportOrchestrationService>();
|
||||
await orchestrator.ExportAllAsync();
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Fehler beim Timer-Export");
|
||||
}
|
||||
|
||||
await RecalculateNextRunAsync();
|
||||
logger.LogError(ex, "Fehler im TimerBackgroundService");
|
||||
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
|
||||
<!--
|
||||
Pfad zur SAP HANA Client DLL (wird mit dem SAP HANA Client installiert).
|
||||
Standard-Pfad nach Installation: C:\Program Files\sap\hdbclient\dotnetcore\v2.1\
|
||||
Kann bei Bedarf via MSBuild-Property überschrieben werden:
|
||||
dotnet build /p:HanaClientDll="D:\pfad\zu\Sap.Data.Hana.Core.v2.1.dll"
|
||||
-->
|
||||
<HanaClientDll Condition="'$(HanaClientDll)' == ''">C:\Program Files\sap\hdbclient\dotnetcore\v2.1\Sap.Data.Hana.Core.v2.1.dll</HanaClientDll>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" Version="1.13.1" />
|
||||
<PackageReference Include="ClosedXML" Version="0.104.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Graph" Version="5.80.0" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.13.1" />
|
||||
<PackageReference Include="MudBlazor" Version="7.15.0" />
|
||||
<PackageReference Include="Sap.Data.Hana.v2" Version="2.22.26" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Sap.Data.Hana.Core.v2.1">
|
||||
<HintPath>$(HanaClientDll)</HintPath>
|
||||
<Private>true</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CheckHanaClient" BeforeTargets="ResolveAssemblyReferences">
|
||||
<Warning Condition="!Exists('$(HanaClientDll)')"
|
||||
Text="SAP HANA Client DLL nicht gefunden: $(HanaClientDll). Bitte SAP HANA Client installieren (https://tools.hana.ondemand.com) oder MSBuild-Property 'HanaClientDll' setzen." />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.37012.4 d17.14
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrafagSalesExporter", "TrafagSalesExporter.csproj", "{49B56D6D-731C-6482-4A5C-82EAEEBCE593}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {DC174EA0-ECCB-4957-9D97-E7ABED992867}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
html, body {
|
||||
font-family: Roboto, Arial, sans-serif;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
html, body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
Reference in New Issue
Block a user