@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("Export Dashboard", "Export dashboard") @T("Export Dashboard", "Export dashboard") @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") } @if (_readinessWarnings.Count > 0) { @T("Aktive Standorte sind noch nicht vollstaendig bereit:", "Active sites are not fully ready:") @foreach (var warning in _readinessWarnings) { @warning } } @if (_consolidatedStale) { @T("Seit der letzten zentralen Excel wurde mindestens ein Standort neu exportiert. Bitte `Zentrale Datei neu erzeugen` ausfuehren, damit das Endexcel aktuell ist.", "At least one site was exported after the last consolidated Excel. Please rebuild the consolidated file so the final Excel is current.") } @T("Land", "Country") @T("Basis", "Basis") 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.DataBasis @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 _readinessWarnings = new(); private bool _consolidatedStale; 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; _readinessWarnings = state.ReadinessWarnings; _consolidatedStale = state.IsConsolidatedStale; _anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting(); _loading = false; } private async Task ExportAll() { if (_readinessWarnings.Count > 0) { Snackbar.Add(T("Es gibt aktive Standorte mit fehlender manueller Datei. Bitte Warnung im Dashboard pruefen.", "There are active sites with missing manual files. Please check the dashboard warning."), Severity.Warning); } _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)); await InvokeAsync(() => Snackbar.Add(T("Die zentrale Excel ist danach noch nicht automatisch aktualisiert. Bitte `Zentrale Datei neu erzeugen` starten.", "The consolidated Excel is not automatically updated after this. Please rebuild the consolidated file."), Severity.Info)); } 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 FormatException(Exception ex) => ex.InnerException is null ? ex.Message : $"{ex.Message} Inner: {ex.InnerException.Message}"; private static string GetDataBasisIcon(string dataBasis) { if (dataBasis.Contains("Excel", StringComparison.OrdinalIgnoreCase)) return Icons.Material.Filled.TableView; if (dataBasis.Contains("CSV", StringComparison.OrdinalIgnoreCase) || dataBasis.Contains("Datei", StringComparison.OrdinalIgnoreCase)) return Icons.Material.Filled.Description; if (dataBasis.Contains("SAP", StringComparison.OrdinalIgnoreCase)) return Icons.Material.Filled.CloudSync; if (dataBasis.Contains("Server", StringComparison.OrdinalIgnoreCase)) return Icons.Material.Filled.Storage; return Icons.Material.Filled.Source; } private static Color GetDataBasisColor(string dataBasis) { if (dataBasis.Contains("Excel", StringComparison.OrdinalIgnoreCase)) return Color.Success; if (dataBasis.Contains("CSV", StringComparison.OrdinalIgnoreCase) || dataBasis.Contains("Datei", StringComparison.OrdinalIgnoreCase)) return Color.Info; if (dataBasis.Contains("SAP", StringComparison.OrdinalIgnoreCase)) return Color.Primary; if (dataBasis.Contains("Server", StringComparison.OrdinalIgnoreCase)) return Color.Secondary; return Color.Default; } } @code { private string T(string german, string english) => UiText.Text(german, english); }