englisch
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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">Fü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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user