using Microsoft.EntityFrameworkCore; using TrafagSalesExporter.Data; using TrafagSalesExporter.Models; namespace TrafagSalesExporter.Services; public interface IDashboardPageService { Task LoadAsync(); } public sealed class DashboardPageService : IDashboardPageService { private readonly IDbContextFactory _dbFactory; public DashboardPageService(IDbContextFactory dbFactory) { _dbFactory = dbFactory; } public async Task LoadAsync() { await using var db = await _dbFactory.CreateDbContextAsync(); var sites = await db.Sites.Include(s => s.HanaServer).Where(s => s.IsActive).ToListAsync(); var sourceSystems = await db.SourceSystemDefinitions.AsNoTracking().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()); var rows = sites.Select(s => { var log = logs.FirstOrDefault(l => l.SiteId == s.Id); latestAppLogsBySite.TryGetValue(s.Id, out var appLog); var sourceSystem = sourceSystems.FirstOrDefault(x => string.Equals(x.Code, s.SourceSystem, StringComparison.OrdinalIgnoreCase)); return new DashboardRow { SiteId = s.Id, Land = s.Land, TSC = s.TSC, Schema = s.Schema, ServerName = string.Equals(sourceSystem?.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase) ? ResolveDashboardSapServiceUrl(s, sourceSystems) : s.HanaServer?.Name ?? string.Empty, LastStatus = log?.Status ?? string.Empty, RowCount = log?.RowCount ?? 0, LastRun = log?.Timestamp, DurationSeconds = log?.DurationSeconds ?? 0, ErrorMessage = log?.ErrorMessage ?? string.Empty, FilePath = log?.FilePath ?? string.Empty, LiveMessage = appLog is null ? string.Empty : $"{appLog.Category}: {appLog.Message}", LiveDetails = appLog?.Details ?? string.Empty }; }).ToList(); var consolidatedRows = BuildConsolidatedRows(await db.ExportSettings.FirstOrDefaultAsync() ?? new()); var latestSuccessfulSiteRun = logs .Where(log => log.Status == "OK") .Select(log => (DateTime?)log.Timestamp) .OrderByDescending(timestamp => timestamp) .FirstOrDefault(); var latestConsolidatedRun = consolidatedRows .Select(row => row.LastModified) .OrderByDescending(timestamp => timestamp) .FirstOrDefault(); return new DashboardPageState { DashboardRows = rows, ConsolidatedRows = consolidatedRows, ReadinessWarnings = BuildReadinessWarnings(sites, sourceSystems), IsConsolidatedStale = latestSuccessfulSiteRun.HasValue && (!latestConsolidatedRun.HasValue || latestSuccessfulSiteRun.Value > latestConsolidatedRun.Value), LatestSuccessfulSiteRun = latestSuccessfulSiteRun, LatestConsolidatedRun = latestConsolidatedRun }; } private static List BuildReadinessWarnings(List activeSites, List sourceSystems) { var warnings = new List(); foreach (var site in activeSites.OrderBy(x => x.Land).ThenBy(x => x.TSC)) { var sourceSystem = sourceSystems.FirstOrDefault(x => string.Equals(x.Code, site.SourceSystem, StringComparison.OrdinalIgnoreCase)); if (!string.Equals(sourceSystem?.ConnectionKind, SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase)) continue; if (string.IsNullOrWhiteSpace(site.ManualImportFilePath)) warnings.Add($"{site.Land} / {site.TSC}: manuelle Excel-/CSV-Datei fehlt."); } return warnings; } private static string ResolveDashboardSapServiceUrl(Site site, List sourceSystems) { if (!string.IsNullOrWhiteSpace(site.SapServiceUrl)) return site.SapServiceUrl; var sourceSystem = sourceSystems.FirstOrDefault(x => string.Equals(x.Code, site.SourceSystem, StringComparison.OrdinalIgnoreCase)); return string.IsNullOrWhiteSpace(sourceSystem?.CentralServiceUrl) ? "SAP Gateway" : sourceSystem.CentralServiceUrl; } 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"); } } public sealed class DashboardPageState { public List DashboardRows { get; set; } = []; public List ConsolidatedRows { get; set; } = []; public List ReadinessWarnings { get; set; } = []; public bool IsConsolidatedStale { get; set; } public DateTime? LatestSuccessfulSiteRun { get; set; } public DateTime? LatestConsolidatedRun { get; set; } } public sealed class DashboardRow { public int SiteId { get; set; } public string Land { get; set; } = string.Empty; public string TSC { get; set; } = string.Empty; public string Schema { get; set; } = string.Empty; public string ServerName { get; set; } = string.Empty; public string LastStatus { get; set; } = string.Empty; public int RowCount { get; set; } public DateTime? LastRun { get; set; } public double DurationSeconds { get; set; } public string ErrorMessage { get; set; } = string.Empty; public string FilePath { get; set; } = string.Empty; public string LiveMessage { get; set; } = string.Empty; public string LiveDetails { get; set; } = string.Empty; public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath); } public sealed class ConsolidatedDashboardRow { public string Label { get; set; } = string.Empty; public string FilePath { get; set; } = string.Empty; public string DisplayPath { get; set; } = string.Empty; public DateTime? LastModified { get; set; } public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath); }