@page "/"
@using System.Diagnostics
@using TrafagSalesExporter.Services
@inject IDashboardPageService DashboardPageActions
@inject ExportOrchestrationService Orchestrator
@inject TimerBackgroundService TimerService
@inject ISnackbar Snackbar
@inject IUiTextService UiText
@implements IDisposable
@T("Dashboard", "Dashboard")
@T("Dashboard", "Dashboard")
@T("Net Sales Actuals 2025 Referenz", "Net sales actuals 2025 reference")
check.xlsx / Power BI Stand 29.04.2026
@T("Firma", "Company")
@T("Ist 2025", "Actual 2025")
@T("IC-Abzug", "IC deduction")
@T("Ist exkl. IC", "Actual excl. IC")
@T("Referenz", "Reference")
@T("Summenfeld", "Value field")
@T("Quelle", "Source")
@T("Differenz", "Difference")
@T("Diff exkl. IC", "Diff excl. IC")
@T("Waehrung", "Currency")
@T("Zeilen", "Rows")
@T("Status", "Status")
@context.Label
@FormatAmount(context.ActualValue)
@FormatAmount(context.IntercompanyDeduction)
@FormatAmount(context.ActualValueExcludingIntercompany)
@FormatAmount(context.ReferenceValue)
@(string.IsNullOrWhiteSpace(context.ValueField) ? "-" : context.ValueField)
@context.ReferenceSource
@FormatAmount(context.Difference)
@FormatAmount(context.DifferenceExcludingIntercompany)
@(string.IsNullOrWhiteSpace(context.Currencies) ? "-" : context.Currencies)
@(context.RowCount > 0 ? context.RowCount.ToString("N0") : "-")
@if (context.Status == "OK")
{
OK
}
else if (context.Status == "Pruefen")
{
@T("Pruefen", "Check")
}
else
{
@T("Keine Daten", "No data")
}
@T("Keine Referenzdaten fuer aktive Standorte gefunden.", "No reference data found for active sites.")
@T("Vergleich: Jahr 2025 aus Invoice Date, sonst Extraction Date. Das Summenfeld wird automatisch aus Sales Price/Value, DocTotalFC - VatSumFC oder DocTotal - VatSum gewaehlt; Belegkopfwerte werden pro DocEntry nur einmal gezaehlt. IC-Abzug ist eine Diagnose fuer den aktuellen Trafag-IT-Abgleich und veraendert die Originaldaten nicht.", "Comparison: year 2025 from Invoice Date, otherwise Extraction Date. The value field is selected automatically from Sales Price/Value, DocTotalFC - VatSumFC, or DocTotal - VatSum; document header values are counted only once per DocEntry. IC deduction is a diagnostic value for the current Trafag IT reconciliation and does not change the original data.")
@T("Alle exportieren", "Export all")
@T("Zentrale Datei neu erzeugen", "Rebuild consolidated file")
@if (TimerService.NextRun < DateTime.MaxValue)
{
@(string.Format(T("Naechster automatischer Lauf: {0}", "Next automatic run: {0}"), TimerService.NextRun.ToString("dd.MM.yyyy HH:mm")))
}
else
{
@T("Timer deaktiviert", "Timer disabled")
}
@T("Land", "Country")
TSC
@T("Schema", "Schema")
@T("Server", "Server")
@T("Status", "Status")
@T("Live-Status", "Live status")
@T("Zeilen", "Rows")
@T("Letzter Lauf", "Last run")
@T("Dauer", "Duration")
@T("Aktion", "Action")
@context.Land
@context.TSC
@context.Schema
@context.ServerName
@if (Orchestrator.IsExporting(context.SiteId))
{
@Orchestrator.GetExportStatus(context.SiteId)
}
else if (context.LastStatus == "OK")
{
}
else if (context.LastStatus == "Error")
{
}
else
{
-
}
@if (!string.IsNullOrWhiteSpace(context.LiveMessage))
{
@context.LiveMessage
}
else
{
-
}
@(context.RowCount > 0 ? context.RowCount.ToString("N0") : "-")
@(context.LastRun.HasValue ? context.LastRun.Value.ToString("dd.MM.yyyy HH:mm:ss") : "-")
@(context.DurationSeconds > 0 ? $"{context.DurationSeconds:F1}s" : "-")
Export
@T("Excel oeffnen", "Open Excel")
@T("Zentrale Datei", "Consolidated file")
@T("Datei", "File")
Pfad
Letzte Änderung
@T("Status", "Status")
@T("Aktion", "Action")
@context.Label
@context.DisplayPath
@(context.LastModified.HasValue ? context.LastModified.Value.ToString("dd.MM.yyyy HH:mm:ss") : "-")
@if (Orchestrator.IsConsolidatedExporting())
{
@Orchestrator.GetConsolidatedExportStatus()
}
else
{
-
}
@T("Excel oeffnen", "Open Excel")
@T("Keine zentrale Excel-Datei gefunden.", "No consolidated Excel file found.")
@code {
private List _dashboardRows = new();
private List _consolidatedRows = new();
private List _netSalesReferenceRows = new();
private bool _loading = true;
private bool _anyRunning;
private CancellationTokenSource? _pollingCts;
protected override async Task OnInitializedAsync()
{
Orchestrator.OnExportStatusChanged += HandleStatusChanged;
await LoadDataAsync();
}
private async Task LoadDataAsync()
{
_loading = true;
var state = await DashboardPageActions.LoadAsync();
_dashboardRows = state.DashboardRows;
_consolidatedRows = state.ConsolidatedRows;
_netSalesReferenceRows = state.NetSalesReferenceRows;
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting();
_loading = false;
}
private async Task ExportAll()
{
_anyRunning = true;
await LoadDataAsync();
StartPolling();
_ = Task.Run(async () =>
{
try
{
await Orchestrator.ExportAllAsync();
await InvokeAsync(() =>
Snackbar.Add(T("Export fuer alle Standorte beendet", "Export completed for all sites"), Severity.Success));
}
catch (Exception ex)
{
await InvokeAsync(() =>
Snackbar.Add(string.Format(T("Export fuer alle Standorte fehlgeschlagen: {0}", "Export for all sites failed: {0}"), FormatException(ex)), Severity.Error));
}
finally
{
await InvokeAsync(async () =>
{
await LoadDataAsync();
StateHasChanged();
});
}
});
Snackbar.Add(T("Export fuer alle Standorte gestartet", "Export started for all sites"), Severity.Info);
}
private async Task ExportConsolidatedOnly()
{
_anyRunning = true;
await LoadDataAsync();
StartPolling();
_ = Task.Run(async () =>
{
try
{
var filePath = await Orchestrator.ExportConsolidatedOnlyAsync();
if (!string.IsNullOrWhiteSpace(filePath))
{
await InvokeAsync(() =>
Snackbar.Add(string.Format(T("Zentrale Datei erzeugt: {0}", "Consolidated file created: {0}"), filePath), Severity.Success));
}
else
{
await InvokeAsync(() =>
Snackbar.Add(T("Zentrale Datei konnte nicht erzeugt werden. Details stehen in den Logs.", "Consolidated file could not be created. Details are in the logs."), Severity.Warning));
}
}
catch (Exception ex)
{
await InvokeAsync(() =>
Snackbar.Add(string.Format(T("Zentrale Datei fehlgeschlagen: {0}", "Consolidated file failed: {0}"), FormatException(ex)), Severity.Error));
}
finally
{
await InvokeAsync(async () =>
{
await LoadDataAsync();
StateHasChanged();
});
}
});
Snackbar.Add(T("Zentrale Datei wird erzeugt", "Building consolidated file"), Severity.Info);
}
private void ExportSingle(int siteId)
{
_anyRunning = true;
_ = InvokeAsync(async () => await LoadDataAsync());
StartPolling();
_ = Task.Run(async () =>
{
try
{
var result = await Orchestrator.ExportSiteByIdAsync(siteId);
if (result?.Log.Status == "OK" && !string.IsNullOrWhiteSpace(result.FilePath))
{
await InvokeAsync(() =>
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(string.Format(T("Export fehlgeschlagen: {0}", "Export failed: {0}"), result.Log.ErrorMessage), Severity.Error));
}
}
catch (Exception ex)
{
await InvokeAsync(() =>
Snackbar.Add(string.Format(T("Export fehlgeschlagen: {0}", "Export failed: {0}"), FormatException(ex)), Severity.Error));
}
finally
{
await InvokeAsync(async () =>
{
await LoadDataAsync();
StateHasChanged();
});
}
});
Snackbar.Add(T("Export gestartet", "Export started"), Severity.Info);
}
private async void HandleStatusChanged()
{
await InvokeAsync(async () =>
{
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting() || _dashboardRows.Count == 0;
if (_anyRunning)
{
StartPolling();
await RefreshLiveDataAsync();
StateHasChanged();
return;
}
StopPolling();
await LoadDataAsync();
StateHasChanged();
});
}
public void Dispose()
{
StopPolling();
Orchestrator.OnExportStatusChanged -= HandleStatusChanged;
}
private void OpenExportFile(DashboardRow row)
{
OpenFile(row.FilePath);
}
private void OpenFile(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
{
Snackbar.Add(T("Exportdatei nicht gefunden.", "Export file not found."), Severity.Warning);
return;
}
try
{
Process.Start(new ProcessStartInfo
{
FileName = filePath,
UseShellExecute = true
});
}
catch (Exception ex)
{
Snackbar.Add(string.Format(T("Datei konnte nicht geoeffnet werden: {0}", "Could not open file: {0}"), ex.Message), Severity.Error);
}
}
private void StartPolling()
{
if (_pollingCts is not null && !_pollingCts.IsCancellationRequested)
return;
_pollingCts = new CancellationTokenSource();
_ = PollDashboardAsync(_pollingCts.Token);
}
private void StopPolling()
{
_pollingCts?.Cancel();
_pollingCts?.Dispose();
_pollingCts = null;
}
private async Task PollDashboardAsync(CancellationToken cancellationToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3));
try
{
while (await timer.WaitForNextTickAsync(cancellationToken))
{
var anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting();
if (!anyRunning)
{
await InvokeAsync(async () =>
{
_anyRunning = false;
await LoadDataAsync();
StateHasChanged();
});
StopPolling();
break;
}
await InvokeAsync(async () =>
{
_anyRunning = true;
await RefreshLiveDataAsync();
StateHasChanged();
});
}
}
catch (OperationCanceledException)
{
}
}
private Task RefreshLiveDataAsync()
{
foreach (var row in _dashboardRows)
{
if (!Orchestrator.IsExporting(row.SiteId))
continue;
row.LiveMessage = Orchestrator.GetExportStatus(row.SiteId);
row.LiveDetails = string.Empty;
}
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting();
return Task.CompletedTask;
}
private static string FormatAmount(decimal? value)
=> value.HasValue ? value.Value.ToString("N2") : "-";
private static string FormatException(Exception ex)
=> ex.InnerException is null ? ex.Message : $"{ex.Message} Inner: {ex.InnerException.Message}";
}
@code {
private string T(string german, string english) => UiText.Text(german, english);
}