@T("Net Sales Actuals 2025 Referenz", "Net sales actuals 2025 reference")
- @T("Gleiche Berechnungslogik wie FinanceProbe/Testprogramm", "Same calculation logic as FinanceProbe/test program")
+ @T("Verbindliche Finance-Sicht aus CentralSalesRecords", "Authoritative finance view from CentralSalesRecords")
@T("Diese Sicht arbeitet direkt auf `CentralSalesRecords`. Summenfeld und Anzeige-Waehrung koennen gewaehlt werden; fachliche Filter wie Intercompany, Budget und Spartenlogik sind weiterhin nicht enthalten.", "This view works directly on `CentralSalesRecords`. Value field and display currency can be selected; business filters such as intercompany, budget and divisional logic are still not included.")
+
+ @T("Diese Analyse ist eine Plausibilitaets- und Rohdatensicht. Fuer den verbindlichen Finance-Abgleich bitte `Soll/Ist Vergleich` oder im Endexcel die `Finance | ...`-Spalten verwenden.",
+ "This analysis is a plausibility/raw-data view. For the authoritative finance reconciliation, use `Actual/reference comparison` or the `Finance | ...` columns in the final Excel.")
+
diff --git a/TrafagSalesExporter/Components/Pages/ManualImports.razor b/TrafagSalesExporter/Components/Pages/ManualImports.razor
new file mode 100644
index 0000000..312a860
--- /dev/null
+++ b/TrafagSalesExporter/Components/Pages/ManualImports.razor
@@ -0,0 +1,192 @@
+@page "/manual-imports"
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.EntityFrameworkCore
+@using TrafagSalesExporter.Data
+@using TrafagSalesExporter.Models
+@using TrafagSalesExporter.Services
+@inject IDbContextFactory DbFactory
+@inject IStandortePageService StandortePageService
+@inject ISnackbar Snackbar
+@inject IUiTextService UiText
+
+@T("Manuelle Importe", "Manual imports")
+
+@T("Manuelle Importe", "Manual imports")
+
+
+ @T("Diese Seite ist fuer Keyuser: Hier werden Excel-/CSV-Dateien fuer manuelle Laender wie DE, UK und ES hinterlegt und aktiviert. Technische Spaltenmappings bleiben in Admin -> Standorte.",
+ "This page is for key users: Excel/CSV files for manual countries such as DE, UK and ES are maintained and activated here. Technical column mappings remain in Admin -> Sites.")
+
+
+
+
+ @T("Land", "Country")
+ TSC
+ @T("Aktiv", "Active")
+ @T("Datei / SharePoint-Ordner", "File / SharePoint folder")
+ @T("Letzter Upload", "Last upload")
+ @T("Aktionen", "Actions")
+
+
+ @context.Land
+ @context.TSC
+
+
+
+
+ @(context.ManualImportLastUploadedAtUtc?.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") ?? "-")
+
+
+
+ @T("Pfad pruefen", "Check path")
+
+
+ @T("Speichern", "Save")
+
+
+
+
+
+
+ @T("Keine manuellen Excel-/CSV-Standorte gefunden.", "No manual Excel/CSV sites found.")
+
+
+
+@code {
+ private List _rows = [];
+ private bool _loading = true;
+ private int? _busySiteId;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadAsync();
+ }
+
+ private async Task LoadAsync()
+ {
+ _loading = true;
+ await using var db = await DbFactory.CreateDbContextAsync();
+ var manualSourceCodes = await db.SourceSystemDefinitions
+ .Where(x => x.ConnectionKind == SourceSystemConnectionKinds.ManualExcel)
+ .Select(x => x.Code)
+ .ToListAsync();
+
+ _rows = await db.Sites
+ .Where(site => manualSourceCodes.Contains(site.SourceSystem))
+ .OrderBy(site => site.Land)
+ .ThenBy(site => site.TSC)
+ .Select(site => new ManualImportRow
+ {
+ Id = site.Id,
+ Land = site.Land,
+ TSC = site.TSC,
+ IsActive = site.IsActive,
+ ManualImportFilePath = site.ManualImportFilePath,
+ ManualImportLastUploadedAtUtc = site.ManualImportLastUploadedAtUtc
+ })
+ .ToListAsync();
+ _loading = false;
+ }
+
+ private async Task SaveAsync(ManualImportRow row)
+ {
+ _busySiteId = row.Id;
+ try
+ {
+ await using var db = await DbFactory.CreateDbContextAsync();
+ var site = await db.Sites.FirstAsync(x => x.Id == row.Id);
+ site.IsActive = row.IsActive;
+ site.ManualImportFilePath = row.ManualImportFilePath.Trim();
+ site.ManualImportLastUploadedAtUtc = row.ManualImportLastUploadedAtUtc;
+ await db.SaveChangesAsync();
+ Snackbar.Add(T("Import-Einstellungen gespeichert.", "Import settings saved."), Severity.Success);
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"{T("Speichern fehlgeschlagen", "Save failed")}: {ex.Message}", Severity.Error);
+ }
+ finally
+ {
+ _busySiteId = null;
+ }
+ }
+
+ private async Task ValidatePathAsync(ManualImportRow row)
+ {
+ _busySiteId = row.Id;
+ try
+ {
+ row.ManualImportLastUploadedAtUtc = await StandortePageService.ValidateManualImportPathAsync(row.ManualImportFilePath);
+ Snackbar.Add(T("Datei oder SharePoint-Referenz ist erreichbar.", "File or SharePoint reference is reachable."), Severity.Success);
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"{T("Pfadpruefung fehlgeschlagen", "Path check failed")}: {ex.Message}", Severity.Error);
+ }
+ finally
+ {
+ _busySiteId = null;
+ }
+ }
+
+ private async Task UploadAsync(ManualImportRow row, InputFileChangeEventArgs args)
+ {
+ var file = args.File;
+ if (file is null)
+ return;
+
+ _busySiteId = row.Id;
+ try
+ {
+ var extension = Path.GetExtension(file.Name);
+ if (!string.Equals(extension, ".xlsx", StringComparison.OrdinalIgnoreCase) &&
+ !string.Equals(extension, ".csv", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException(T("Bitte eine .xlsx- oder .csv-Datei auswaehlen.", "Please choose a .xlsx or .csv file."));
+ }
+
+ var uploadDirectory = Path.Combine(AppContext.BaseDirectory, "manual-imports");
+ Directory.CreateDirectory(uploadDirectory);
+ var safeBaseName = string.Concat(Path.GetFileNameWithoutExtension(file.Name)
+ .Select(ch => char.IsLetterOrDigit(ch) || ch == '-' || ch == '_' ? ch : '_'));
+ if (string.IsNullOrWhiteSpace(safeBaseName))
+ safeBaseName = "manual_import";
+
+ var targetPath = Path.Combine(uploadDirectory, $"{safeBaseName}_{Guid.NewGuid():N}{extension}");
+ await using (var sourceStream = file.OpenReadStream(maxAllowedSize: 50 * 1024 * 1024))
+ await using (var targetStream = File.Create(targetPath))
+ {
+ await sourceStream.CopyToAsync(targetStream);
+ }
+
+ row.ManualImportFilePath = targetPath;
+ row.ManualImportLastUploadedAtUtc = DateTime.UtcNow;
+ await SaveAsync(row);
+ Snackbar.Add(T("Datei hochgeladen.", "File uploaded."), Severity.Success);
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"{T("Upload fehlgeschlagen", "Upload failed")}: {ex.Message}", Severity.Error);
+ }
+ finally
+ {
+ _busySiteId = null;
+ }
+ }
+
+ private string T(string german, string english) => UiText.Text(german, english);
+
+ private sealed class ManualImportRow
+ {
+ public int Id { get; set; }
+ public string Land { get; set; } = string.Empty;
+ public string TSC { get; set; } = string.Empty;
+ public bool IsActive { get; set; }
+ public string ManualImportFilePath { get; set; } = string.Empty;
+ public DateTime? ManualImportLastUploadedAtUtc { get; set; }
+ }
+}
diff --git a/TrafagSalesExporter/NEXT_STEPS_2026-04-15.md b/TrafagSalesExporter/NEXT_STEPS_2026-04-15.md
index 0a0f9d4..25205fe 100644
--- a/TrafagSalesExporter/NEXT_STEPS_2026-04-15.md
+++ b/TrafagSalesExporter/NEXT_STEPS_2026-04-15.md
@@ -2,6 +2,23 @@
Stand: 2026-05-20
+## Nachtrag 2026-05-20 Workflow-Fixes nach Review
+
+Umgesetzt:
+
+- Dashboard warnt vor aktiven manuellen Standorten ohne Datei.
+- Nach Einzelstandortexport wird sichtbar, dass die zentrale Excel neu erzeugt werden muss.
+- Dashboard erkennt eine veraltete zentrale Excel nach neuem Standortexport.
+- Neuer Menuepunkt `Manuelle Importe` fuer Keyuser.
+- Zentrale Excel hat ein Blatt `Finance Summary`.
+- `Management Analyse` ist als Rohdaten-/Plausibilitaetssicht markiert.
+- `Soll/Ist Vergleich` ist als verbindliche Finance-Sicht markiert.
+- Export-Live-Status ist nicht mehr pauschal `HANA Abfrage...`.
+
+Weiterhin offen:
+
+- DE Alphaplan-Fachabgrenzung: Kundenlaender/Filter muessen von Munir/Finance bestaetigt werden.
+
## Nachtrag 2026-05-20 Keyuser Prozess-SVG
Erstellt:
diff --git a/TrafagSalesExporter/Services/DashboardPageService.cs b/TrafagSalesExporter/Services/DashboardPageService.cs
index cd60a41..42bb010 100644
--- a/TrafagSalesExporter/Services/DashboardPageService.cs
+++ b/TrafagSalesExporter/Services/DashboardPageService.cs
@@ -62,13 +62,45 @@ public sealed class DashboardPageService : IDashboardPageService
};
}).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 = BuildConsolidatedRows(await db.ExportSettings.FirstOrDefaultAsync() ?? new())
+ 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))
@@ -114,6 +146,10 @@ 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
diff --git a/TrafagSalesExporter/Services/ExcelExportService.cs b/TrafagSalesExporter/Services/ExcelExportService.cs
index 789b6e3..f18bc2c 100644
--- a/TrafagSalesExporter/Services/ExcelExportService.cs
+++ b/TrafagSalesExporter/Services/ExcelExportService.cs
@@ -146,11 +146,98 @@ public class ExcelExportService : IExcelExportService
ws.Columns().AdjustToContents();
if (includeFinanceHelpSheet)
+ {
+ AddFinanceSummarySheet(workbook, records);
AddFinanceHelpSheet(workbook);
+ }
workbook.SaveAs(fullPath);
}
+ private static void AddFinanceSummarySheet(XLWorkbook workbook, List records)
+ {
+ var ws = workbook.Worksheets.Add("Finance Summary");
+ ws.Position = 1;
+ ws.Cell(1, 1).Value = "Finance Summary";
+ ws.Cell(1, 1).Style.Font.Bold = true;
+ ws.Cell(1, 1).Style.Font.FontSize = 14;
+ ws.Cell(2, 1).Value = "Diese Summen verwenden dieselbe Finance-Sicht wie die Spalten Finance | ... im Blatt Sales.";
+
+ var headers = new[]
+ {
+ "Year",
+ "Country Key",
+ "Currency",
+ "Included Rows",
+ "Net Sales Actual",
+ "Excluded Rows",
+ "Hinweis"
+ };
+
+ for (var i = 0; i < headers.Length; i++)
+ {
+ ws.Cell(4, i + 1).Value = headers[i];
+ ws.Cell(4, i + 1).Style.Font.Bold = true;
+ }
+
+ var italyBlankSupplierCountryRows = new HashSet(StringComparer.OrdinalIgnoreCase);
+ var summaryRows = records
+ .Select(record =>
+ {
+ var financeDate = ResolveFinanceDate(record);
+ var countryKey = ResolveFinanceCountryKey(record.Land, record.Tsc);
+ var include = ResolveFinanceInclude(record, countryKey, italyBlankSupplierCountryRows) && record.SalesPriceValue != 0m;
+ return new
+ {
+ Year = financeDate.Year,
+ CountryKey = countryKey,
+ Currency = ResolveFinanceCurrency(record),
+ Include = include,
+ Value = include ? record.SalesPriceValue : 0m
+ };
+ })
+ .GroupBy(row => new { row.Year, row.CountryKey, row.Currency })
+ .OrderBy(group => group.Key.Year)
+ .ThenBy(group => group.Key.CountryKey, StringComparer.OrdinalIgnoreCase)
+ .ThenBy(group => group.Key.Currency, StringComparer.OrdinalIgnoreCase)
+ .Select(group => new
+ {
+ group.Key.Year,
+ group.Key.CountryKey,
+ group.Key.Currency,
+ IncludedRows = group.Count(row => row.Include),
+ NetSalesActual = group.Sum(row => row.Value),
+ ExcludedRows = group.Count(row => !row.Include)
+ })
+ .ToList();
+
+ var rowIndex = 5;
+ foreach (var row in summaryRows)
+ {
+ ws.Cell(rowIndex, 1).Value = row.Year;
+ ws.Cell(rowIndex, 2).Value = row.CountryKey;
+ ws.Cell(rowIndex, 3).Value = row.Currency;
+ ws.Cell(rowIndex, 4).Value = row.IncludedRows;
+ ws.Cell(rowIndex, 5).Value = row.NetSalesActual;
+ ws.Cell(rowIndex, 6).Value = row.ExcludedRows;
+ ws.Cell(rowIndex, 7).Value = BuildFinanceSummaryHint(row.CountryKey);
+ rowIndex++;
+ }
+
+ ws.Column(5).Style.NumberFormat.Format = "#,##0.00";
+ ws.Columns().AdjustToContents();
+ }
+
+ private static string BuildFinanceSummaryHint(string countryKey)
+ => countryKey.ToUpperInvariant() switch
+ {
+ "DE" => "DE Alphaplan ist technisch vorbereitet; Kundenlaender/Filter fachlich noch bestaetigen.",
+ "IT" => "IT: Trafag Italia ausgeschlossen; doppelte Blank-Supplier-Zeilen nur einmal.",
+ "UK" => "UK: Sage/Manual Excel, Credit Notes negativ.",
+ "ES" => "ES: Sage CSV/Manual Excel, REC/Credit Notes negativ.",
+ _ => string.Empty
+ };
+
private static void AddFinanceHelpSheet(XLWorkbook workbook)
{
var ws = workbook.Worksheets.Add("Finance Filter Hilfe");
diff --git a/TrafagSalesExporter/Services/ExportOrchestrationService.cs b/TrafagSalesExporter/Services/ExportOrchestrationService.cs
index 3b965c8..c98dd8c 100644
--- a/TrafagSalesExporter/Services/ExportOrchestrationService.cs
+++ b/TrafagSalesExporter/Services/ExportOrchestrationService.cs
@@ -96,7 +96,7 @@ public class ExportOrchestrationService
lock (_lock)
{
if (_runningExports.ContainsKey(site.Id)) return null;
- _runningExports[site.Id] = "HANA Abfrage...";
+ _runningExports[site.Id] = BuildInitialExportStatus(site);
}
NotifyChanged();
@@ -134,6 +134,17 @@ public class ExportOrchestrationService
OnExportStatusChanged?.Invoke();
}
+ private static string BuildInitialExportStatus(Site site)
+ {
+ var sourceSystem = (site.SourceSystem ?? string.Empty).Trim().ToUpperInvariant();
+ return sourceSystem switch
+ {
+ "MANUAL_EXCEL" => "Manuelle Excel/CSV lesen...",
+ "SAP" => "SAP OData lesen...",
+ _ => "Quelldaten lesen..."
+ };
+ }
+
private async Task RunConsolidatedExportAsync()
{
lock (_lock)
diff --git a/TrafagSalesExporter/lastchange.md b/TrafagSalesExporter/lastchange.md
index ec9a519..8e273cf 100644
--- a/TrafagSalesExporter/lastchange.md
+++ b/TrafagSalesExporter/lastchange.md
@@ -1,5 +1,26 @@
# Last Change 2026-05-04
+## Workflow-Konsistenz fuer Keyuser verbessert 2026-05-20
+
+Geaendert:
+
+- Export Dashboard zeigt jetzt Warnungen, wenn aktive Manual-Excel-Standorte noch keine Datei/Pfad hinterlegt haben.
+- Nach einem Einzelstandortexport wird darauf hingewiesen, dass die zentrale Excel separat neu erzeugt werden muss.
+- Dashboard markiert, wenn seit der letzten zentralen Excel ein Standortexport gelaufen ist.
+- Neuer Keyuser-Menuepunkt `Manuelle Importe` fuer DE/UK/ES-artige Excel-/CSV-Quellen:
+ - Pfad/SharePoint-Referenz pflegen
+ - Datei hochladen
+ - Standort aktiv/inaktiv setzen
+ - Pfad pruefen
+- Live-Status startet nicht mehr pauschal mit `HANA Abfrage...`, sondern quellenneutral bzw. fuer Manual Excel/SAP passender.
+- Zentrale Excel enthaelt ein neues Blatt `Finance Summary` mit Summen nach Jahr, Land und Waehrung.
+- `Management Analyse` ist klarer als Rohdaten-/Plausibilitaetssicht markiert.
+- `Soll/Ist Vergleich` ist klarer als verbindliche Finance-Sicht markiert.
+
+Bewusst nicht geaendert:
+
+- DE-Fachregel bleibt offen, bis Munir/Finance bestaetigt, welche Kundenlaender/Filter zum offiziellen DE-Ist gehoeren.
+
## Keyuser Prozessdoku SVG 2026-05-20
Erstellt: