@page "/" @using Microsoft.EntityFrameworkCore @using System.Diagnostics @using TrafagSalesExporter.Data @using TrafagSalesExporter.Services @inject IDbContextFactory DbFactory @inject ExportOrchestrationService Orchestrator @inject TimerBackgroundService TimerService @inject ISnackbar Snackbar @inject IUiTextService UiText @implements IDisposable @T("Dashboard", "Dashboard") @T("Dashboard", "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") } @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 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; using var db = await DbFactory.CreateDbContextAsync(); var sites = await db.Sites.Include(s => s.HanaServer).Where(s => s.IsActive).ToListAsync(); var logs = await db.ExportLogs .GroupBy(l => l.SiteId) .Select(g => g.OrderByDescending(l => l.Timestamp).First()) .ToListAsync(); var appLogs = await db.AppEventLogs .Where(l => l.SiteId != null) .OrderByDescending(l => l.Timestamp) .Take(1000) .ToListAsync(); var latestAppLogsBySite = appLogs .GroupBy(l => l.SiteId!.Value) .ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.Timestamp).First()); _dashboardRows = sites.Select(s => { var log = logs.FirstOrDefault(l => l.SiteId == s.Id); latestAppLogsBySite.TryGetValue(s.Id, out var appLog); return new DashboardRow { SiteId = s.Id, Land = s.Land, TSC = s.TSC, Schema = s.Schema, ServerName = string.Equals(s.SourceSystem, "SAP", StringComparison.OrdinalIgnoreCase) ? (string.IsNullOrWhiteSpace(s.SapServiceUrl) ? "SAP Gateway" : s.SapServiceUrl) : s.HanaServer?.Name ?? "", LastStatus = log?.Status ?? "", RowCount = log?.RowCount ?? 0, LastRun = log?.Timestamp, DurationSeconds = log?.DurationSeconds ?? 0, ErrorMessage = log?.ErrorMessage ?? "", FilePath = log?.FilePath ?? "", LiveMessage = appLog is null ? string.Empty : $"{appLog.Category}: {appLog.Message}", LiveDetails = appLog?.Details ?? "" }; }).ToList(); _consolidatedRows = BuildConsolidatedRows(settings: await db.ExportSettings.FirstOrDefaultAsync() ?? new()); _anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting(); _loading = false; } private async Task ExportAll() { _anyRunning = true; await LoadDataAsync(); StartPolling(); _ = Task.Run(async () => { await Orchestrator.ExportAllAsync(); 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 () => { var filePath = await Orchestrator.ExportConsolidatedOnlyAsync(); await InvokeAsync(async () => { await LoadDataAsync(); StateHasChanged(); }); 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.", "Consolidated file could not be created."), Severity.Warning)); } }); 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 () => { var result = await Orchestrator.ExportSiteByIdAsync(siteId); await InvokeAsync(async () => { await LoadDataAsync(); StateHasChanged(); }); 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)); } }); 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 List BuildConsolidatedRows(ExportSettings settings) { var outputDirectory = ResolveConsolidatedOutputDirectory(settings); if (!Directory.Exists(outputDirectory)) return []; return Directory.GetFiles(outputDirectory, "Sales_All_*.xlsx") .Select(path => new FileInfo(path)) .OrderByDescending(file => file.LastWriteTime) .Take(1) .Select(file => new ConsolidatedDashboardRow { Label = "Konsolidierter Export", FilePath = file.FullName, DisplayPath = file.FullName, LastModified = file.LastWriteTime }) .ToList(); } private static string ResolveConsolidatedOutputDirectory(ExportSettings settings) { if (!string.IsNullOrWhiteSpace(settings.LocalConsolidatedExportFolder)) return settings.LocalConsolidatedExportFolder.Trim(); if (!string.IsNullOrWhiteSpace(settings.LocalSiteExportFolder)) return settings.LocalSiteExportFolder.Trim(); return Path.Combine(AppContext.BaseDirectory, "output"); } private class DashboardRow { public int SiteId { get; set; } public string Land { get; set; } = ""; public string TSC { get; set; } = ""; public string Schema { get; set; } = ""; public string ServerName { get; set; } = ""; public string LastStatus { get; set; } = ""; public int RowCount { get; set; } public DateTime? LastRun { get; set; } public double DurationSeconds { get; set; } public string ErrorMessage { get; set; } = ""; public string FilePath { get; set; } = ""; public string LiveMessage { get; set; } = ""; public string LiveDetails { get; set; } = ""; public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath); } private class ConsolidatedDashboardRow { public string Label { get; set; } = ""; public string FilePath { get; set; } = ""; public string DisplayPath { get; set; } = ""; public DateTime? LastModified { get; set; } public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath); } } @code { private string T(string german, string english) => UiText.Text(german, english); }