@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); }