This commit is contained in:
2026-04-17 07:08:04 +02:00
parent ca91af9682
commit 0d3bd47f7a
34 changed files with 17503 additions and 160 deletions
@@ -1,4 +1,6 @@
@inherits LayoutComponentBase
@implements IDisposable
@inject TrafagSalesExporter.Services.IUiTextService UiText
<MudThemeProvider Theme="_theme" />
<MudPopoverProvider />
@@ -9,8 +11,18 @@
<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 app-title">Trafag Finanze/Sales Management Cockpit</MudText>
<MudText Typo="Typo.h6" Class="ml-3 app-title">@T("Trafag Finanze/Sales Management Cockpit", "Trafag Finance/Sales Management Cockpit")</MudText>
<MudSpacer />
<MudSelect T="string"
Value="@UiText.CurrentLanguage"
ValueChanged="ChangeLanguage"
Dense
Variant="Variant.Outlined"
Class="mr-3"
Style="min-width:100px; color:white;">
<MudSelectItem Value="@("de")">DE</MudSelectItem>
<MudSelectItem Value="@("en")">EN</MudSelectItem>
</MudSelect>
<img src="trafag.jpg" alt="Trafag" class="app-logo" />
</MudAppBar>
@@ -18,7 +30,7 @@
<NavMenu />
</MudDrawer>
<MudMainContent Class="pa-4">
<MudMainContent Class="pa-4" @key="UiText.CurrentLanguage">
@Body
</MudMainContent>
</MudLayout>
@@ -36,5 +48,27 @@
}
};
protected override void OnInitialized()
{
UiText.Changed += HandleLanguageChanged;
}
private void ToggleDrawer() => _drawerOpen = !_drawerOpen;
private void ChangeLanguage(string language)
{
UiText.SetLanguage(language);
}
private void HandleLanguageChanged()
{
InvokeAsync(StateHasChanged);
}
private string T(string german, string english) => UiText.Text(german, english);
public void Dispose()
{
UiText.Changed -= HandleLanguageChanged;
}
}
@@ -1,20 +1,26 @@
@inject TrafagSalesExporter.Services.IUiTextService UiText
<MudNavMenu>
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">
Dashboard
@T("Dashboard", "Dashboard")
</MudNavLink>
<MudNavLink Href="/standorte" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.LocationOn">
Standorte
@T("Standorte", "Sites")
</MudNavLink>
<MudNavLink Href="/transformations" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Transform">
Transformationen
@T("Transformationen", "Transformations")
</MudNavLink>
<MudNavLink Href="/management-cockpit" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Analytics">
Management Cockpit
@T("Management Cockpit", "Management Cockpit")
</MudNavLink>
<MudNavLink Href="/settings" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Settings">
Settings
@T("Settings", "Settings")
</MudNavLink>
<MudNavLink Href="/logs" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">
Logs
@T("Logs", "Logs")
</MudNavLink>
</MudNavMenu>
@code {
private string T(string german, string english) => UiText.Text(german, english);
}
@@ -7,32 +7,33 @@
@inject ExportOrchestrationService Orchestrator
@inject TimerBackgroundService TimerService
@inject ISnackbar Snackbar
@inject IUiTextService UiText
@implements IDisposable
<PageTitle>Dashboard</PageTitle>
<PageTitle>@T("Dashboard", "Dashboard")</PageTitle>
<MudText Typo="Typo.h4" Class="mb-4">Dashboard</MudText>
<MudText Typo="Typo.h4" Class="mb-4">@T("Dashboard", "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
@T("Alle exportieren", "Export all")
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" StartIcon="@Icons.Material.Filled.TableView"
OnClick="ExportConsolidatedOnly" Disabled="_anyRunning">
Zentrale Datei neu erzeugen
@T("Zentrale Datei neu erzeugen", "Rebuild consolidated file")
</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}")
@(string.Format(T("Naechster automatischer Lauf: {0}", "Next automatic run: {0}"), TimerService.NextRun.ToString("dd.MM.yyyy HH:mm")))
}
else
{
<MudIcon Icon="@Icons.Material.Filled.TimerOff" Size="Size.Small" Class="mr-1" />
@("Timer deaktiviert")
@T("Timer deaktiviert", "Timer disabled")
}
</MudText>
</MudStack>
@@ -40,16 +41,16 @@
<MudTable Items="_dashboardRows" Dense Hover Striped Loading="_loading">
<HeaderContent>
<MudTh>Land</MudTh>
<MudTh>@T("Land", "Country")</MudTh>
<MudTh>TSC</MudTh>
<MudTh>Schema</MudTh>
<MudTh>Server</MudTh>
<MudTh>Status</MudTh>
<MudTh>Live-Status</MudTh>
<MudTh>Zeilen</MudTh>
<MudTh>Letzter Lauf</MudTh>
<MudTh>Dauer</MudTh>
<MudTh>Aktion</MudTh>
<MudTh>@T("Schema", "Schema")</MudTh>
<MudTh>@T("Server", "Server")</MudTh>
<MudTh>@T("Status", "Status")</MudTh>
<MudTh>@T("Live-Status", "Live status")</MudTh>
<MudTh>@T("Zeilen", "Rows")</MudTh>
<MudTh>@T("Letzter Lauf", "Last run")</MudTh>
<MudTh>@T("Dauer", "Duration")</MudTh>
<MudTh>@T("Aktion", "Action")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Land</MudTd>
@@ -106,7 +107,7 @@
StartIcon="@Icons.Material.Filled.OpenInNew"
OnClick="() => OpenExportFile(context)"
Disabled="@(!context.HasOpenableFile || Orchestrator.IsExporting(context.SiteId))">
Excel öffnen
@T("Excel oeffnen", "Open Excel")
</MudButton>
</MudStack>
</MudTd>
@@ -114,14 +115,14 @@
</MudTable>
<MudPaper Class="pa-4 mt-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-3">Zentrale Datei</MudText>
<MudText Typo="Typo.h6" Class="mb-3">@T("Zentrale Datei", "Consolidated file")</MudText>
<MudTable Items="_consolidatedRows" Dense Hover Striped>
<HeaderContent>
<MudTh>Datei</MudTh>
<MudTh>@T("Datei", "File")</MudTh>
<MudTh>Pfad</MudTh>
<MudTh>Letzte Änderung</MudTh>
<MudTh>Status</MudTh>
<MudTh>Aktion</MudTh>
<MudTh>@T("Status", "Status")</MudTh>
<MudTh>@T("Aktion", "Action")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Label</MudTd>
@@ -143,12 +144,12 @@
StartIcon="@Icons.Material.Filled.OpenInNew"
OnClick="() => OpenFile(context.FilePath)"
Disabled="@(!context.HasOpenableFile)">
Excel öffnen
@T("Excel oeffnen", "Open Excel")
</MudButton>
</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText Typo="Typo.caption">Keine zentrale Excel-Datei gefunden.</MudText>
<MudText Typo="Typo.caption">@T("Keine zentrale Excel-Datei gefunden.", "No consolidated Excel file found.")</MudText>
</NoRecordsContent>
</MudTable>
</MudPaper>
@@ -229,7 +230,7 @@
StateHasChanged();
});
});
Snackbar.Add("Export für alle Standorte gestartet", Severity.Info);
Snackbar.Add(T("Export fuer alle Standorte gestartet", "Export started for all sites"), Severity.Info);
}
private async Task ExportConsolidatedOnly()
@@ -249,15 +250,15 @@
if (!string.IsNullOrWhiteSpace(filePath))
{
await InvokeAsync(() =>
Snackbar.Add($"Zentrale Datei erzeugt: {filePath}", Severity.Success));
Snackbar.Add(string.Format(T("Zentrale Datei erzeugt: {0}", "Consolidated file created: {0}"), filePath), Severity.Success));
}
else
{
await InvokeAsync(() =>
Snackbar.Add("Zentrale Datei konnte nicht erzeugt werden.", Severity.Warning));
Snackbar.Add(T("Zentrale Datei konnte nicht erzeugt werden.", "Consolidated file could not be created."), Severity.Warning));
}
});
Snackbar.Add("Zentrale Datei wird erzeugt", Severity.Info);
Snackbar.Add(T("Zentrale Datei wird erzeugt", "Building consolidated file"), Severity.Info);
}
private void ExportSingle(int siteId)
@@ -277,15 +278,15 @@
if (result?.Log.Status == "OK" && !string.IsNullOrWhiteSpace(result.FilePath))
{
await InvokeAsync(() =>
Snackbar.Add($"Export gespeichert: {result.FilePath}", Severity.Success));
Snackbar.Add(string.Format(T("Export gespeichert: {0}", "Export saved: {0}"), result.FilePath), Severity.Success));
}
else if (result?.Log.Status == "Error" && !string.IsNullOrWhiteSpace(result.Log.ErrorMessage))
{
await InvokeAsync(() =>
Snackbar.Add($"Export fehlgeschlagen: {result.Log.ErrorMessage}", Severity.Error));
Snackbar.Add(string.Format(T("Export fehlgeschlagen: {0}", "Export failed: {0}"), result.Log.ErrorMessage), Severity.Error));
}
});
Snackbar.Add("Export gestartet", Severity.Info);
Snackbar.Add(T("Export gestartet", "Export started"), Severity.Info);
}
private async void HandleStatusChanged()
@@ -322,7 +323,7 @@
{
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
{
Snackbar.Add("Exportdatei nicht gefunden.", Severity.Warning);
Snackbar.Add(T("Exportdatei nicht gefunden.", "Export file not found."), Severity.Warning);
return;
}
@@ -336,7 +337,7 @@
}
catch (Exception ex)
{
Snackbar.Add($"Datei konnte nicht geöffnet werden: {ex.Message}", Severity.Error);
Snackbar.Add(string.Format(T("Datei konnte nicht geoeffnet werden: {0}", "Could not open file: {0}"), ex.Message), Severity.Error);
}
}
@@ -463,3 +464,7 @@
public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath);
}
}
@code {
private string T(string german, string english) => UiText.Text(german, english);
}
+28 -23
View File
@@ -4,46 +4,47 @@
@inject IDbContextFactory<AppDbContext> DbFactory
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject TrafagSalesExporter.Services.IUiTextService UiText
<PageTitle>Logs</PageTitle>
<PageTitle>@T("Logs", "Logs")</PageTitle>
<MudText Typo="Typo.h4" Class="mb-4">Export Logs</MudText>
<MudText Typo="Typo.h4" Class="mb-4">@T("Export Logs", "Export Logs")</MudText>
<MudPaper Class="pa-4 mb-4" Elevation="1">
<MudStack Row AlignItems="AlignItems.Center" Spacing="3">
<MudSelect @bind-Value="_filterLand" Label="Land" Clearable Dense Style="max-width:200px;">
<MudSelect @bind-Value="_filterLand" Label="@T("Land", "Country")" Clearable Dense Style="max-width:200px;">
@foreach (var land in _availableLands)
{
<MudSelectItem Value="@land">@land</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="_filterStatus" Label="Status" Clearable Dense Style="max-width:150px;">
<MudSelect @bind-Value="_filterStatus" Label="@T("Status", "Status")" Clearable Dense Style="max-width:150px;">
<MudSelectItem Value="@("OK")">OK</MudSelectItem>
<MudSelectItem Value="@("Error")">Error</MudSelectItem>
</MudSelect>
<MudDatePicker @bind-Date="_filterDate" Label="Datum" Clearable Dense Style="max-width:200px;" />
<MudDatePicker @bind-Date="_filterDate" Label="@T("Datum", "Date")" Clearable Dense Style="max-width:200px;" />
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ApplyFilter"
StartIcon="@Icons.Material.Filled.FilterAlt">
Filtern
@T("Filtern", "Filter")
</MudButton>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Error" OnClick="DeleteOldLogs"
StartIcon="@Icons.Material.Filled.DeleteSweep">
Alte Logs löschen
@T("Alte Logs loeschen", "Delete old logs")
</MudButton>
</MudStack>
</MudPaper>
<MudTable Items="_logs" Dense Hover Striped Loading="_loading">
<HeaderContent>
<MudTh>Zeitpunkt</MudTh>
<MudTh>Land</MudTh>
<MudTh>@T("Zeitpunkt", "Timestamp")</MudTh>
<MudTh>@T("Land", "Country")</MudTh>
<MudTh>TSC</MudTh>
<MudTh>Status</MudTh>
<MudTh>Zeilen</MudTh>
<MudTh>Dauer</MudTh>
<MudTh>Dateiname</MudTh>
<MudTh>Fehler</MudTh>
<MudTh>@T("Status", "Status")</MudTh>
<MudTh>@T("Zeilen", "Rows")</MudTh>
<MudTh>@T("Dauer", "Duration")</MudTh>
<MudTh>@T("Dateiname", "File name")</MudTh>
<MudTh>@T("Fehler", "Error")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Timestamp.ToString("dd.MM.yyyy HH:mm:ss")</MudTd>
@@ -75,15 +76,15 @@
</RowTemplate>
</MudTable>
<MudText Typo="Typo.h5" Class="mt-6 mb-2">Technische Logs</MudText>
<MudText Typo="Typo.h5" Class="mt-6 mb-2">@T("Technische Logs", "Technical logs")</MudText>
<MudTable Items="_appLogs" Dense Hover Striped Loading="_loading">
<HeaderContent>
<MudTh>Zeitpunkt</MudTh>
<MudTh>@T("Zeitpunkt", "Timestamp")</MudTh>
<MudTh>Level</MudTh>
<MudTh>Kategorie</MudTh>
<MudTh>Land</MudTh>
<MudTh>Meldung</MudTh>
<MudTh>@T("Kategorie", "Category")</MudTh>
<MudTh>@T("Land", "Country")</MudTh>
<MudTh>@T("Meldung", "Message")</MudTh>
<MudTh>Details</MudTh>
</HeaderContent>
<RowTemplate>
@@ -158,9 +159,9 @@
private async Task DeleteOldLogs()
{
var result = await DialogService.ShowMessageBox(
"Alte Logs löschen",
"Logs älter als 90 Tage löschen?",
yesText: "Löschen", cancelText: "Abbrechen");
T("Alte Logs loeschen", "Delete old logs"),
T("Logs aelter als 90 Tage loeschen?", "Delete logs older than 90 days?"),
yesText: T("Loeschen", "Delete"), cancelText: T("Abbrechen", "Cancel"));
if (result != true) return;
@@ -170,6 +171,10 @@
db.ExportLogs.RemoveRange(oldLogs);
var count = await db.SaveChangesAsync();
await LoadLogsAsync();
Snackbar.Add($"{oldLogs.Count} alte Logs gelöscht", Severity.Info);
Snackbar.Add(string.Format(T("{0} alte Logs geloescht", "{0} old logs deleted"), oldLogs.Count), Severity.Info);
}
}
@code {
private string T(string german, string english) => UiText.Text(german, english);
}
@@ -3,15 +3,16 @@
@using TrafagSalesExporter.Services
@inject IManagementCockpitService CockpitService
@inject ISnackbar Snackbar
@inject IUiTextService UiText
<PageTitle>Management Cockpit</PageTitle>
<PageTitle>@T("Management Cockpit", "Management Cockpit")</PageTitle>
<MudText Typo="Typo.h4" Class="mb-4">Management Cockpit</MudText>
<MudText Typo="Typo.h4" Class="mb-4">@T("Management Cockpit", "Management Cockpit")</MudText>
<MudPaper Class="pa-4 mb-4" Elevation="1">
<MudGrid>
<MudItem xs="12" md="8">
<MudSelect T="string" @bind-Value="_selectedFilePath" Label="Vorhandene Excel-Datei" Dense>
<MudSelect T="string" @bind-Value="_selectedFilePath" Label="@T("Vorhandene Excel-Datei", "Available Excel file")" Dense>
@foreach (var file in _files)
{
<MudSelectItem Value="@file.Path">@file.DisplayName</MudSelectItem>
@@ -22,11 +23,11 @@
<MudStack Row Spacing="2">
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick="ReloadFiles"
StartIcon="@Icons.Material.Filled.Refresh" Disabled="_loadingFiles">
Dateien laden
@T("Dateien laden", "Load files")
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="Analyze"
StartIcon="@Icons.Material.Filled.Analytics" Disabled="_analyzing || string.IsNullOrWhiteSpace(_selectedFilePath)">
@(_analyzing ? "Analysiere..." : "Cockpit erzeugen")
@(_analyzing ? T("Analysiere...", "Analyzing...") : T("Cockpit erzeugen", "Build cockpit"))
</MudButton>
</MudStack>
</MudItem>
@@ -34,13 +35,13 @@
</MudPaper>
<MudPaper Class="pa-4 mb-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-3">Zentrale Roh-Auswertung</MudText>
<MudText Typo="Typo.h6" Class="mb-3">@T("Zentrale Roh-Auswertung", "Central raw analysis")</MudText>
<MudAlert Severity="Severity.Info" Dense Variant="Variant.Outlined" Class="mb-3">
Diese Sicht arbeitet direkt auf `CentralSalesRecords` und zeigt nur fachlich neutrale Rohkennzahlen. Kein Intercompany-Filter, keine CHF-Umrechnung, kein Budget, keine Spartenlogik.
@T("Diese Sicht arbeitet direkt auf `CentralSalesRecords` und zeigt nur fachlich neutrale Rohkennzahlen. Kein Intercompany-Filter, keine CHF-Umrechnung, kein Budget, keine Spartenlogik.", "This view works directly on `CentralSalesRecords` and shows only neutral raw metrics. No intercompany filter, no CHF conversion, no budget, no divisional logic.")
</MudAlert>
<MudGrid>
<MudItem xs="12" md="4">
<MudSelect T="int" @bind-Value="_selectedCentralYear" Label="Jahr" Dense>
<MudSelect T="int" @bind-Value="_selectedCentralYear" Label='@T("Jahr", "Year")' Dense>
@foreach (var year in _centralYears)
{
<MudSelectItem Value="@year">@year</MudSelectItem>
@@ -48,7 +49,7 @@
</MudSelect>
</MudItem>
<MudItem xs="12" md="4">
<MudSelect T="int?" @bind-Value="_selectedCentralMonth" Label="Monat (optional)" Dense Clearable>
<MudSelect T="int?" @bind-Value="_selectedCentralMonth" Label='@T("Monat (optional)", "Month (optional)")' Dense Clearable>
@foreach (var month in Enumerable.Range(1, 12))
{
<MudSelectItem Value="@((int?)month)">@($"{month:D2}")</MudSelectItem>
@@ -58,7 +59,7 @@
<MudItem xs="12" md="4">
<MudButton Variant="Variant.Filled" Color="Color.Secondary" OnClick="AnalyzeCentral"
StartIcon="@Icons.Material.Filled.QueryStats" Disabled="_analyzingCentral || _selectedCentralYear == 0">
@(_analyzingCentral ? "Analysiere..." : "Zentrale Auswertung laden")
@(_analyzingCentral ? T("Analysiere...", "Analyzing...") : T("Zentrale Auswertung laden", "Load central analysis"))
</MudButton>
</MudItem>
</MudGrid>
@@ -67,14 +68,14 @@
@if (_result is not null)
{
<MudGrid Class="mb-4">
<MudItem xs="12" md="3"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Land</MudText><MudText Typo="Typo.h6">@_result.Summary.Land</MudText></MudPaper></MudItem>
<MudItem xs="12" md="3"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@T("Land", "Country")</MudText><MudText Typo="Typo.h6">@_result.Summary.Land</MudText></MudPaper></MudItem>
<MudItem xs="12" md="3"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">TSC</MudText><MudText Typo="Typo.h6">@_result.Summary.Tsc</MudText></MudPaper></MudItem>
<MudItem xs="12" md="3"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Umsatz</MudText><MudText Typo="Typo.h6">@_result.Summary.SalesValueTotal.ToString("N2")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="3"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Geschätzte Marge</MudText><MudText Typo="Typo.h6">@($"{_result.Summary.EstimatedMarginPercent:F1}%")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="3"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@T("Umsatz", "Sales")</MudText><MudText Typo="Typo.h6">@_result.Summary.SalesValueTotal.ToString("N2")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="3"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@T("Geschaetzte Marge", "Estimated margin")</MudText><MudText Typo="Typo.h6">@($"{_result.Summary.EstimatedMarginPercent:F1}%")</MudText></MudPaper></MudItem>
</MudGrid>
<MudPaper Class="pa-4 mb-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Management Aussagen</MudText>
<MudText Typo="Typo.h6" Class="mb-2">@T("Management Aussagen", "Management statements")</MudText>
@foreach (var finding in _result.Findings)
{
<MudAlert Severity="@MapSeverity(finding.Severity)" Dense Variant="Variant.Outlined" Class="mb-2">
@@ -86,7 +87,7 @@
<MudGrid Class="mb-4">
<MudItem xs="12" md="4">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Top Kunden</MudText>
<MudText Typo="Typo.h6" Class="mb-2">@T("Top Kunden", "Top customers")</MudText>
@foreach (var item in _result.TopCustomers)
{
<MudText Typo="Typo.body2">@($"{item.Label}: {item.Value:N2} ({item.SharePercent:F1}%)")</MudText>
@@ -95,7 +96,7 @@
</MudItem>
<MudItem xs="12" md="4">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Top Produktgruppen</MudText>
<MudText Typo="Typo.h6" Class="mb-2">@T("Top Produktgruppen", "Top product groups")</MudText>
@foreach (var item in _result.TopProductGroups)
{
<MudText Typo="Typo.body2">@($"{item.Label}: {item.Value:N2} ({item.SharePercent:F1}%)")</MudText>
@@ -104,7 +105,7 @@
</MudItem>
<MudItem xs="12" md="4">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Top Sales Owner</MudText>
<MudText Typo="Typo.h6" Class="mb-2">@T("Top Sales Owner", "Top sales owner")</MudText>
@foreach (var item in _result.TopSalesEmployees)
{
<MudText Typo="Typo.body2">@($"{item.Label}: {item.Value:N2} ({item.SharePercent:F1}%)")</MudText>
@@ -114,7 +115,7 @@
</MudGrid>
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Datenqualität</MudText>
<MudText Typo="Typo.h6" Class="mb-2">@T("Datenqualitaet", "Data quality")</MudText>
@foreach (var entry in _result.DataQualityCounts.OrderByDescending(x => x.Value))
{
<MudText Typo="Typo.body2">@($"{entry.Key}: {entry.Value}")</MudText>
@@ -125,16 +126,16 @@
@if (_centralResult is not null)
{
<MudGrid Class="mb-4">
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Zeilen</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.RowCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Rechnungen</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.InvoiceCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Standorte</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.SiteCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Länder</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.CountryCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Währungen</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.CurrencyCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Periode</MudText><MudText Typo="Typo.h6">@BuildPeriodLabel(_centralResult)</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@T("Zeilen", "Rows")</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.RowCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@T("Rechnungen", "Invoices")</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.InvoiceCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@T("Standorte", "Sites")</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.SiteCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@T("Laender", "Countries")</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.CountryCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@T("Waehrungen", "Currencies")</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.CurrencyCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@T("Periode", "Period")</MudText><MudText Typo="Typo.h6">@BuildPeriodLabel(_centralResult)</MudText></MudPaper></MudItem>
</MudGrid>
<MudPaper Class="pa-4 mb-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Hinweise</MudText>
<MudText Typo="Typo.h6" Class="mb-2">@T("Hinweise", "Notes")</MudText>
@foreach (var notice in _centralResult.Notices)
{
<MudAlert Severity="Severity.Info" Dense Variant="Variant.Outlined" Class="mb-2">@notice</MudAlert>
@@ -144,13 +145,13 @@
<MudGrid Class="mb-4">
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Jahresumsatz 2025/2026</MudText>
<MudText Typo="Typo.h6" Class="mb-2">@T("Jahresumsatz 2025/2026", "Yearly sales 2025/2026")</MudText>
<MudTable Items="_centralResult.YearlyTotals" Dense Hover Striped>
<HeaderContent>
<MudTh>Jahr</MudTh>
<MudTh>Währung</MudTh>
<MudTh>Umsatz</MudTh>
<MudTh>Zeilen</MudTh>
<MudTh>@T("Jahr", "Year")</MudTh>
<MudTh>@T("Waehrung", "Currency")</MudTh>
<MudTh>@T("Umsatz", "Sales")</MudTh>
<MudTh>@T("Zeilen", "Rows")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Year</MudTd>
@@ -163,13 +164,13 @@
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Monatsumsatz</MudText>
<MudText Typo="Typo.h6" Class="mb-2">@T("Monatsumsatz", "Monthly sales")</MudText>
<MudTable Items="_centralResult.MonthlyTotals" Dense Hover Striped>
<HeaderContent>
<MudTh>Monat</MudTh>
<MudTh>Währung</MudTh>
<MudTh>Umsatz</MudTh>
<MudTh>Zeilen</MudTh>
<MudTh>@T("Monat", "Month")</MudTh>
<MudTh>@T("Waehrung", "Currency")</MudTh>
<MudTh>@T("Umsatz", "Sales")</MudTh>
<MudTh>@T("Zeilen", "Rows")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Label</MudTd>
@@ -185,13 +186,13 @@
<MudGrid Class="mb-4">
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Tagesumsatz im ausgewählten Monat</MudText>
<MudText Typo="Typo.h6" Class="mb-2">@T("Tagesumsatz im ausgewaehlten Monat", "Daily sales in selected month")</MudText>
<MudTable Items="_centralResult.DailyTotals" Dense Hover Striped>
<HeaderContent>
<MudTh>Tag</MudTh>
<MudTh>Währung</MudTh>
<MudTh>Umsatz</MudTh>
<MudTh>Zeilen</MudTh>
<MudTh>@T("Tag", "Day")</MudTh>
<MudTh>@T("Waehrung", "Currency")</MudTh>
<MudTh>@T("Umsatz", "Sales")</MudTh>
<MudTh>@T("Zeilen", "Rows")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Label</MudTd>
@@ -200,20 +201,20 @@
<MudTd>@context.RowCount.ToString("N0")</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText Typo="Typo.caption">r die Tagessicht bitte zusätzlich einen Monat wählen.</MudText>
<MudText Typo="Typo.caption">@T("Fuer die Tagessicht bitte zusaetzlich einen Monat waehlen.", "Please select a month as well for the daily view.")</MudText>
</NoRecordsContent>
</MudTable>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Umsatz nach Quelle</MudText>
<MudText Typo="Typo.h6" Class="mb-2">@T("Umsatz nach Quelle", "Sales by source")</MudText>
<MudTable Items="_centralResult.SourceSystemTotals" Dense Hover Striped>
<HeaderContent>
<MudTh>Quelle</MudTh>
<MudTh>Währung</MudTh>
<MudTh>Umsatz</MudTh>
<MudTh>Rechnungen</MudTh>
<MudTh>@T("Quelle", "Source")</MudTh>
<MudTh>@T("Waehrung", "Currency")</MudTh>
<MudTh>@T("Umsatz", "Sales")</MudTh>
<MudTh>@T("Rechnungen", "Invoices")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Label</MudTd>
@@ -227,14 +228,14 @@
</MudGrid>
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Umsatz nach Land</MudText>
<MudText Typo="Typo.h6" Class="mb-2">@T("Umsatz nach Land", "Sales by country")</MudText>
<MudTable Items="_centralResult.CountryTotals" Dense Hover Striped>
<HeaderContent>
<MudTh>Land</MudTh>
<MudTh>Währung</MudTh>
<MudTh>Umsatz</MudTh>
<MudTh>Rechnungen</MudTh>
<MudTh>Zeilen</MudTh>
<MudTh>@T("Land", "Country")</MudTh>
<MudTh>@T("Waehrung", "Currency")</MudTh>
<MudTh>@T("Umsatz", "Sales")</MudTh>
<MudTh>@T("Rechnungen", "Invoices")</MudTh>
<MudTh>@T("Zeilen", "Rows")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Label</MudTd>
@@ -298,7 +299,7 @@
}
catch (Exception ex)
{
Snackbar.Add($"Cockpit konnte nicht erzeugt werden: {ex.Message}", Severity.Error);
Snackbar.Add(string.Format(T("Cockpit konnte nicht erzeugt werden: {0}", "Could not build cockpit: {0}"), ex.Message), Severity.Error);
}
finally
{
@@ -318,7 +319,7 @@
}
catch (Exception ex)
{
Snackbar.Add($"Zentrale Auswertung konnte nicht erzeugt werden: {ex.Message}", Severity.Error);
Snackbar.Add(string.Format(T("Zentrale Auswertung konnte nicht erzeugt werden: {0}", "Could not build central analysis: {0}"), ex.Message), Severity.Error);
}
finally
{
@@ -341,3 +342,7 @@
return $"{result.Summary.PeriodStart.Value:dd.MM.yyyy} - {result.Summary.PeriodEnd.Value:dd.MM.yyyy}";
}
}
@code {
private string T(string german, string english) => UiText.Text(german, english);
}
@@ -9,6 +9,7 @@
@inject IHanaQueryService HanaService
@inject ISapGatewayService SapGatewayService
@inject IConfigTransferService ConfigTransferService
@inject IExchangeRateImportService ExchangeRateImportService
@inject IJSRuntime JS
@inject ISnackbar Snackbar
@@ -51,6 +52,11 @@
<MudItem xs="12" md="6">
<MudTextField @bind-Value="_spConfig.ExportFolder" Label="Export Folder" />
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="_spConfig.CentralExportFolder"
Label="Central Export Folder"
HelperText="Optional. Wenn leer, wird weiterhin Export Folder/Alle verwendet." />
</MudItem>
<MudItem xs="12" md="4">
<MudTextField @bind-Value="_spConfig.TenantId" Label="Tenant ID" />
</MudItem>
@@ -80,6 +86,15 @@
</MudButton>
</MudStack>
</MudItem>
@if (!string.IsNullOrWhiteSpace(_sharePointTestPreview))
{
<MudItem xs="12">
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mt-3">
<div><b>Test Preview</b></div>
<div style="white-space: pre-wrap">@_sharePointTestPreview</div>
</MudAlert>
</MudItem>
}
</MudGrid>
</MudPaper>
@@ -151,6 +166,70 @@
</MudGrid>
</MudPaper>
<MudText Typo="Typo.h5" Class="mb-2">Wechselkurse</MudText>
<MudPaper Class="pa-4 mb-6" Elevation="1">
<MudText Typo="Typo.body2" Class="mb-3">
Diese Kurstabelle wird von der Transformation <b>ConvertCurrency</b> verwendet. Gleiche Waehrung rechnet automatisch mit Faktor 1.
</MudText>
<MudStack Row Spacing="2" Class="mb-3">
<MudButton Variant="Variant.Outlined" Color="Color.Primary" OnClick="AddExchangeRate"
StartIcon="@Icons.Material.Filled.Add">
Kurs hinzufuegen
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick="RefreshEcbRates"
StartIcon="@Icons.Material.Filled.Refresh" Disabled="_refreshingExchangeRates">
@(_refreshingExchangeRates ? "Aktualisiere ECB-Kurse..." : "Refresh Kurse")
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveExchangeRates"
StartIcon="@Icons.Material.Filled.Save">
Kurse speichern
</MudButton>
</MudStack>
<MudTable Items="_exchangeRates" Hover="true" Breakpoint="Breakpoint.Md">
<HeaderContent>
<MudTh>Von</MudTh>
<MudTh>Nach</MudTh>
<MudTh>Kurs</MudTh>
<MudTh>Gueltig ab</MudTh>
<MudTh>Gueltig bis</MudTh>
<MudTh>Notiz</MudTh>
<MudTh>Aktiv</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudTextField @bind-Value="context.FromCurrency" Immediate="true" />
</MudTd>
<MudTd>
<MudTextField @bind-Value="context.ToCurrency" Immediate="true" />
</MudTd>
<MudTd>
<MudNumericField T="decimal" @bind-Value="context.Rate" Immediate="true" />
</MudTd>
<MudTd>
<MudDatePicker Date="context.ValidFrom"
DateChanged="@(value => context.ValidFrom = value ?? context.ValidFrom)"
Editable="true" />
</MudTd>
<MudTd>
<MudDatePicker Date="context.ValidTo"
DateChanged="@(value => context.ValidTo = value)"
Editable="true"
Clearable="true" />
</MudTd>
<MudTd>
<MudTextField @bind-Value="context.Notes" Immediate="true" />
</MudTd>
<MudTd>
<MudCheckBox @bind-Value="context.IsActive" />
</MudTd>
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@(() => RemoveExchangeRate(context))" />
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
@* Export Settings *@
<MudText Typo="Typo.h5" Class="mb-2">Export Einstellungen</MudText>
<MudPaper Class="pa-4 mb-6" Elevation="1">
@@ -210,6 +289,9 @@
private bool _includeSecretsInExport;
private bool _exportingConfig;
private bool _importingConfig;
private bool _refreshingExchangeRates;
private string _sharePointTestPreview = string.Empty;
private List<CurrencyExchangeRate> _exchangeRates = [];
private readonly HashSet<string> _testingSystems = [];
protected override async Task OnInitializedAsync()
@@ -217,6 +299,11 @@
using var db = await DbFactory.CreateDbContextAsync();
_spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig();
_exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
_exchangeRates = await db.CurrencyExchangeRates
.OrderBy(x => x.FromCurrency)
.ThenBy(x => x.ToCurrency)
.ThenByDescending(x => x.ValidFrom)
.ToListAsync();
}
private async Task SaveSharePoint()
@@ -231,6 +318,7 @@
{
existing.SiteUrl = _spConfig.SiteUrl;
existing.ExportFolder = _spConfig.ExportFolder;
existing.CentralExportFolder = _spConfig.CentralExportFolder;
existing.TenantId = _spConfig.TenantId;
existing.ClientId = _spConfig.ClientId;
existing.ClientSecret = _spConfig.ClientSecret;
@@ -244,8 +332,15 @@
_testingSp = true;
try
{
var tenantId = NormalizeConfigValue(_spConfig.TenantId);
var clientId = NormalizeConfigValue(_spConfig.ClientId);
var clientSecret = NormalizeConfigValue(_spConfig.ClientSecret);
var siteUrl = NormalizeConfigValue(_spConfig.SiteUrl);
_sharePointTestPreview = BuildSharePointTestPreview(tenantId, clientId, clientSecret, siteUrl);
await SpService.TestConnectionAsync(
_spConfig.TenantId, _spConfig.ClientId, _spConfig.ClientSecret, _spConfig.SiteUrl);
tenantId, clientId, clientSecret, siteUrl);
Snackbar.Add("SharePoint Verbindung erfolgreich!", Severity.Success);
}
catch (Exception ex)
@@ -287,6 +382,76 @@
Snackbar.Add("Export Einstellungen gespeichert", Severity.Success);
}
private void AddExchangeRate()
{
_exchangeRates.Add(new CurrencyExchangeRate
{
FromCurrency = "USD",
ToCurrency = "EUR",
Rate = 1m,
ValidFrom = DateTime.Today,
IsActive = true
});
}
private void RemoveExchangeRate(CurrencyExchangeRate rate)
{
_exchangeRates.Remove(rate);
}
private async Task SaveExchangeRates()
{
using var db = await DbFactory.CreateDbContextAsync();
var existingRates = await db.CurrencyExchangeRates.ToListAsync();
if (existingRates.Count > 0)
db.CurrencyExchangeRates.RemoveRange(existingRates);
db.CurrencyExchangeRates.AddRange(_exchangeRates.Select(rate => new CurrencyExchangeRate
{
FromCurrency = NormalizeConfigValue(rate.FromCurrency).ToUpperInvariant(),
ToCurrency = NormalizeConfigValue(rate.ToCurrency).ToUpperInvariant(),
Rate = rate.Rate,
ValidFrom = rate.ValidFrom.Date,
ValidTo = rate.ValidTo?.Date,
Notes = NormalizeConfigValue(rate.Notes),
IsActive = rate.IsActive
}).Where(rate => !string.IsNullOrWhiteSpace(rate.FromCurrency)
&& !string.IsNullOrWhiteSpace(rate.ToCurrency)
&& rate.Rate > 0m));
await db.SaveChangesAsync();
_exchangeRates = await db.CurrencyExchangeRates
.OrderBy(x => x.FromCurrency)
.ThenBy(x => x.ToCurrency)
.ThenByDescending(x => x.ValidFrom)
.ToListAsync();
Snackbar.Add("Wechselkurse gespeichert", Severity.Success);
}
private async Task RefreshEcbRates()
{
if (_refreshingExchangeRates)
return;
_refreshingExchangeRates = true;
try
{
var result = await ExchangeRateImportService.RefreshEcbRatesAsync();
_exchangeRates = await LoadExchangeRatesAsync();
Snackbar.Add($"ECB-Kurse aktualisiert: {result.ImportedCount} Kurse vom {result.RateDate:yyyy-MM-dd}.", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"ECB-Kursimport fehlgeschlagen: {ex.Message}", Severity.Error);
}
finally
{
_refreshingExchangeRates = false;
}
}
private async Task ExportConfiguration()
{
if (_exportingConfig)
@@ -328,6 +493,11 @@
using var db = await DbFactory.CreateDbContextAsync();
_spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig();
_exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
_exchangeRates = await db.CurrencyExchangeRates
.OrderBy(x => x.FromCurrency)
.ThenBy(x => x.ToCurrency)
.ThenByDescending(x => x.ValidFrom)
.ToListAsync();
TimerService.Recalculate();
Snackbar.Add("Konfiguration importiert", Severity.Success);
}
@@ -466,4 +636,31 @@
"SAGE" => _exportSettings.SagePassword,
_ => _exportSettings.SapPassword
};
private static string NormalizeConfigValue(string? value) => value?.Trim() ?? string.Empty;
private static string BuildSharePointTestPreview(string tenantId, string clientId, string clientSecret, string siteUrl)
{
var maskedSecret = string.IsNullOrEmpty(clientSecret)
? "<leer>"
: $"{new string('*', Math.Min(clientSecret.Length, 8))} (len={clientSecret.Length})";
return string.Join(Environment.NewLine,
[
$"Tenant ID: {tenantId}",
$"Client ID: {clientId}",
$"Client Secret: {maskedSecret}",
$"Site URL: {siteUrl}"
]);
}
private async Task<List<CurrencyExchangeRate>> LoadExchangeRatesAsync()
{
using var db = await DbFactory.CreateDbContextAsync();
return await db.CurrencyExchangeRates
.OrderBy(x => x.FromCurrency)
.ThenBy(x => x.ToCurrency)
.ThenByDescending(x => x.ValidFrom)
.ToListAsync();
}
}
@@ -0,0 +1,134 @@
@page "/source-viewer"
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.WebUtilities
@inject IWebHostEnvironment Environment
@inject NavigationManager Navigation
@inject TrafagSalesExporter.Services.IUiTextService UiText
<PageTitle>@T("Source Viewer", "Source Viewer")</PageTitle>
<MudStack Spacing="2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">@T("Source Viewer", "Source Viewer")</MudText>
<MudButton Variant="Variant.Outlined" Href="/transformations">
@T("Zurueck zur Transformation", "Back to transformations")
</MudButton>
</MudStack>
@if (!string.IsNullOrWhiteSpace(_requestedPath))
{
<MudText Typo="Typo.body2">
@T("Datei:", "File:")
<MudText Inline="true" Typo="Typo.body2"><code>@_requestedPath</code></MudText>
</MudText>
}
@if (!string.IsNullOrWhiteSpace(_requestedType))
{
<MudText Typo="Typo.body2">
@T("Klasse:", "Class:")
<MudText Inline="true" Typo="Typo.body2"><code>@_requestedType</code></MudText>
@if (_highlightLineNumber is not null)
{
<span> @T("bei Zeile", "at line") @_highlightLineNumber</span>
}
</MudText>
}
@if (!string.IsNullOrWhiteSpace(_error))
{
<MudAlert Severity="Severity.Error" Variant="Variant.Outlined">@_error</MudAlert>
}
else if (string.IsNullOrWhiteSpace(_content))
{
<MudProgressCircular Indeterminate="true" />
}
else
{
<MudPaper Class="pa-4">
<div style="font-family: Consolas, monospace; font-size: 0.9rem;">
@foreach (var line in _lines)
{
<div id="@GetLineAnchor(line.Number)"
style="@GetLineStyle(line.Number)">
<span style="display:inline-block; width:4rem; color:#666;">@line.Number.ToString("0000")</span>
<span>@line.Text</span>
</div>
}
</div>
</MudPaper>
@if (_highlightLineNumber is not null)
{
<script>
location.hash = '@GetLineAnchor(_highlightLineNumber.Value)';
</script>
}
}
</MudStack>
@code {
private string? _requestedPath;
private string? _requestedType;
private string? _content;
private string? _error;
private List<SourceLine> _lines = [];
private int? _highlightLineNumber;
protected override void OnInitialized()
{
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
var query = QueryHelpers.ParseQuery(uri.Query);
_requestedPath = query.TryGetValue("path", out var value) ? value.ToString() : null;
_requestedType = query.TryGetValue("type", out var typeValue) ? typeValue.ToString() : null;
if (string.IsNullOrWhiteSpace(_requestedPath))
{
_error = T("Kein Dateipfad angegeben.", "No file path provided.");
return;
}
if (_requestedPath.Contains("..", StringComparison.Ordinal) || Path.IsPathRooted(_requestedPath))
{
_error = T("Ungueltiger Dateipfad.", "Invalid file path.");
return;
}
var fullPath = Path.Combine(Environment.ContentRootPath, _requestedPath.Replace('/', Path.DirectorySeparatorChar));
if (!File.Exists(fullPath))
{
_error = string.Format(T("Datei nicht gefunden: {0}", "File not found: {0}"), _requestedPath);
return;
}
_content = File.ReadAllText(fullPath);
_lines = _content
.Replace("\r\n", "\n", StringComparison.Ordinal)
.Split('\n')
.Select((text, index) => new SourceLine(index + 1, text))
.ToList();
if (!string.IsNullOrWhiteSpace(_requestedType))
{
_highlightLineNumber = _lines
.FirstOrDefault(x => x.Text.Contains($"class {_requestedType}", StringComparison.Ordinal) ||
x.Text.Contains($"sealed class {_requestedType}", StringComparison.Ordinal) ||
x.Text.Contains($"public class {_requestedType}", StringComparison.Ordinal) ||
x.Text.Contains($"public sealed class {_requestedType}", StringComparison.Ordinal))
?.Number;
}
}
private static string GetLineAnchor(int lineNumber) => $"line-{lineNumber}";
private string GetLineStyle(int lineNumber)
{
var highlight = _highlightLineNumber == lineNumber;
return highlight
? "background-color:#fff3cd; white-space:pre-wrap;"
: "white-space:pre-wrap;";
}
private sealed record SourceLine(int Number, string Text);
private string T(string german, string english) => UiText.Text(german, english);
}
@@ -1250,4 +1250,4 @@
.Where(x => !string.IsNullOrWhiteSpace(x))
.ToHashSet(StringComparer.OrdinalIgnoreCase)
?? [];
}
}
@@ -7,11 +7,12 @@
@inject IDbContextFactory<AppDbContext> DbFactory
@inject ITransformationCatalog TransformationCatalog
@inject ISnackbar Snackbar
@inject IUiTextService UiText
<PageTitle>Transformationen</PageTitle>
<PageTitle>@T("Transformationen", "Transformations")</PageTitle>
<MudText Typo="Typo.h4" Class="mb-4">Transformer Ansicht</MudText>
<MudText Typo="Typo.body1" Class="mb-4">Definiere pro Quellsystem einfache Feldregeln und komplexe record-basierte Strategien.</MudText>
<MudText Typo="Typo.h4" Class="mb-4">@T("Transformer Ansicht", "Transformation view")</MudText>
<MudText Typo="Typo.body1" Class="mb-4">@T("Definiere pro Quellsystem einfache Feldregeln und komplexe record-basierte Strategien.", "Define simple field rules and complex record-based strategies per source system.")</MudText>
<MudPaper Class="pa-4" Elevation="1">
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3">
@@ -20,10 +21,10 @@
<MudStack Row="true" Spacing="2" Class="mb-3">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="AddRule">
Regel hinzufuegen
@T("Regel hinzufuegen", "Add rule")
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" StartIcon="@Icons.Material.Filled.Save" OnClick="SaveAllAsync">
Alle speichern
@T("Alle speichern", "Save all")
</MudButton>
</MudStack>
@@ -34,9 +35,10 @@
<MudTh>Scope</MudTh>
<MudTh>Source</MudTh>
<MudTh>Target</MudTh>
<MudTh>Typ</MudTh>
<MudTh>Typ / Klasse</MudTh>
<MudTh>Argument</MudTh>
<MudTh>Sort</MudTh>
<MudTh>Info</MudTh>
<MudTh>Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
@@ -58,12 +60,19 @@
</MudSelect>
</MudTd>
<MudTd>
<MudSelect T="string" Value="@context.SourceField" ValueChanged="@(v => context.SourceField = v)" Dense Disabled="@IsRecordScope(context)">
@foreach (var field in _recordFields)
{
<MudSelectItem Value="@field">@field</MudSelectItem>
}
</MudSelect>
@if (IsRecordScope(context))
{
<MudChip T="string" Color="Color.Default" Variant="Variant.Outlined" Size="Size.Small" Text="Record-Regel" />
}
else
{
<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>
@@ -74,12 +83,26 @@
</MudSelect>
</MudTd>
<MudTd>
<MudSelect T="string" Value="@context.TransformationType" ValueChanged="@(v => context.TransformationType = v)" Dense>
@foreach (var type in GetTypesForScope(context.RuleScope))
@{
var availableTypes = GetTypesForScope(context.RuleScope);
}
<MudSelect T="string"
@key="@GetTypeSelectKey(context)"
Value="@context.TransformationType"
ValueChanged="@(v => context.TransformationType = v)"
Dense
HelperText="@GetTypeHelperText(context)">
@foreach (var type in availableTypes)
{
<MudSelectItem Value="@type.Key">@type.Key</MudSelectItem>
<MudSelectItem Value="@type.Key">@(IsRecordScope(context) ? $"Klasse: {type.Key}" : type.Key)</MudSelectItem>
}
</MudSelect>
@if (IsRecordScope(context))
{
<MudText Typo="Typo.caption" Class="mt-1">
Hier waehlt man die registrierte C#-Strategie.
</MudText>
}
</MudTd>
<MudTd>
<MudTextField T="string" Value="@context.Argument" ValueChanged="@(v => context.Argument = v)"
@@ -88,6 +111,20 @@
<MudTd>
<MudNumericField T="int" Value="@context.SortOrder" ValueChanged="@(v => context.SortOrder = v)" Dense />
</MudTd>
<MudTd>
@{
var catalogItem = GetCatalogItem(context);
}
<MudStack Spacing="1">
<MudText Typo="Typo.caption">@((catalogItem?.Description ?? T("Keine Beschreibung.", "No description.")) )</MudText>
<MudButton Variant="Variant.Text" Color="Color.Info" Size="Size.Small"
StartIcon="@Icons.Material.Filled.Code"
Disabled="@(catalogItem is null)"
OnClick="() => ShowCode(context)">
@T("Code anzeigen", "Show code")
</MudButton>
</MudStack>
</MudTd>
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small"
OnClick="() => RemoveRule(context)" />
@@ -96,6 +133,48 @@
</MudTable>
</MudPaper>
<MudDialog @bind-Visible="_codeDialogVisible" Options="_codeDialogOptions">
<TitleContent>
<MudText Typo="Typo.h6">@T("Transformationscode", "Transformation code")</MudText>
</TitleContent>
<DialogContent>
@if (_selectedCatalogItem is not null)
{
<MudStack Spacing="2">
<MudText Typo="Typo.subtitle2">@_selectedCatalogItem.Key (@_selectedCatalogItem.RuleScope)</MudText>
<MudText Typo="Typo.body2">@_selectedCatalogItem.Description</MudText>
<MudText Typo="Typo.caption">Klasse: @_selectedCatalogItem.TypeName</MudText>
<MudText Typo="Typo.caption">
Datei:
<MudLink Href="@GetSourceViewerUrl(_selectedCatalogItem.SourceFile, _selectedCatalogItem.TypeName)" Target="_blank">
@_selectedCatalogItem.SourceFile
</MudLink>
</MudText>
<MudPaper Class="pa-3">
<MudText Typo="Typo.caption">Snippet</MudText>
<pre style="margin:0; white-space:pre-wrap;">@_selectedCatalogItem.CodeSnippet</pre>
</MudPaper>
@if (_selectedRule is not null)
{
<MudPaper Class="pa-3">
<MudText Typo="Typo.caption">Aktuelle Regel</MudText>
<MudText Typo="Typo.body2">System: @_selectedRule.SourceSystem</MudText>
<MudText Typo="Typo.body2">Target: @_selectedRule.TargetField</MudText>
@if (!string.IsNullOrWhiteSpace(_selectedRule.SourceField))
{
<MudText Typo="Typo.body2">Source: @_selectedRule.SourceField</MudText>
}
<MudText Typo="Typo.body2">Argument: @(string.IsNullOrWhiteSpace(_selectedRule.Argument) ? "-" : _selectedRule.Argument)</MudText>
</MudPaper>
}
</MudStack>
}
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Text" OnClick="CloseCodeDialog">@T("Schliessen", "Close")</MudButton>
</DialogActions>
</MudDialog>
@code {
private readonly string[] _systems = ["SAP", "BI1", "SAGE", "MANUAL_EXCEL"];
private readonly string[] _ruleScopes = ["Value", "Record"];
@@ -107,6 +186,10 @@
private List<FieldTransformationRule> _rules = new();
private IReadOnlyList<TransformationCatalogItem> _catalogItems = [];
private bool _codeDialogVisible;
private FieldTransformationRule? _selectedRule;
private TransformationCatalogItem? _selectedCatalogItem;
private readonly DialogOptions _codeDialogOptions = new() { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
protected override async Task OnInitializedAsync()
{
@@ -158,7 +241,7 @@
db.FieldTransformationRules.AddRange(_rules);
await db.SaveChangesAsync();
Snackbar.Add("Transformationsregeln gespeichert.", Severity.Success);
Snackbar.Add(T("Transformationsregeln gespeichert.", "Transformation rules saved."), Severity.Success);
await LoadAsync();
}
@@ -190,6 +273,45 @@
string.Equals(x.RuleScope, rule.RuleScope, StringComparison.OrdinalIgnoreCase) &&
string.Equals(x.Key, rule.TransformationType, StringComparison.OrdinalIgnoreCase));
return item?.Description ?? "Optionales Argument.";
return item?.Description ?? T("Optionales Argument.", "Optional argument.");
}
private TransformationCatalogItem? GetCatalogItem(FieldTransformationRule rule)
=> _catalogItems.FirstOrDefault(x =>
string.Equals(x.RuleScope, rule.RuleScope, StringComparison.OrdinalIgnoreCase) &&
string.Equals(x.Key, rule.TransformationType, StringComparison.OrdinalIgnoreCase));
private void ShowCode(FieldTransformationRule rule)
{
_selectedRule = rule;
_selectedCatalogItem = GetCatalogItem(rule);
_codeDialogVisible = _selectedCatalogItem is not null;
}
private void CloseCodeDialog()
{
_codeDialogVisible = false;
_selectedRule = null;
_selectedCatalogItem = null;
}
private static string GetSourceViewerUrl(string sourceFile, string typeName)
=> $"/source-viewer?path={Uri.EscapeDataString(sourceFile)}&type={Uri.EscapeDataString(typeName)}";
private static string GetTypeSelectKey(FieldTransformationRule rule)
=> $"{rule.Id}:{rule.RuleScope}:{rule.TransformationType}";
private string GetTypeHelperText(FieldTransformationRule rule)
{
var types = GetTypesForScope(rule.RuleScope);
return types.Count == 0
? T("Keine Typen fuer diesen Scope registriert.", "No types registered for this scope.")
: IsRecordScope(rule)
? string.Format(T("Verfuegbare Klassen: {0}", "Available classes: {0}"), string.Join(", ", types.Select(x => x.Key)))
: string.Format(T("Verfuegbare Typen: {0}", "Available types: {0}"), string.Join(", ", types.Select(x => x.Key)));
}
}
@code {
private string T(string german, string english) => UiText.Text(german, english);
}