@page "/management-cockpit" @rendermode @(Microsoft.AspNetCore.Components.Web.RenderMode.InteractiveServer) @using TrafagSalesExporter.Models @using TrafagSalesExporter.Services @inject IManagementCockpitPageService CockpitPageService @inject ISnackbar Snackbar @inject IUiTextService UiText @T("Management Analyse", "Management analysis") @T("Management Analyse", "Management analysis") @foreach (var year in _financeYearOptions) { @year } @foreach (var option in _financeCountryOptions) { @option } @foreach (var option in _financeCurrencyOptions) { @option } @(_analyzingFinance ? T("Lade...", "Loading...") : T("Finance Summary laden", "Load finance summary")) @if (_financeResult is not null) { @T("Net Sales Actual", "Net sales actual") @FormatValue(_financeResult.NetSalesActual, _financeResult.DisplayCurrency) @T("gefiltertes Endergebnis", "filtered final result") @T("Enthaltene Zeilen", "Included rows") @_financeResult.IncludedRows.ToString("N0") @T("Finance Include = TRUE", "Finance Include = TRUE") @T("Ausgeschlossen", "Excluded") @_financeResult.ExcludedRows.ToString("N0") @T("Finance-Regeln", "Finance rules") @T("Laender / Waehrungen", "Countries / currencies") @($"{_financeResult.CountryCount:N0} / {_financeResult.CurrencyCount:N0}") @($"{_financeResult.Filter.Year}") @T("Summen wie im Excel-Blatt Finance Summary", "Totals matching the Finance Summary Excel sheet") @T("Jahr", "Year") @T("Land", "Country") @T("Waehrung", "Currency") @T("Net Sales Actual", "Net sales actual") @T("Enthalten", "Included") @T("Ausgeschlossen", "Excluded") @context.Year @context.CountryKey @context.Currency @FormatValue(context.NetSalesActual, context.Currency) @context.IncludedRows.ToString("N0") @context.ExcludedRows.ToString("N0") @T("Keine Finance-Summary-Daten fuer diese Filter.", "No finance summary data for these filters.") @T("Hinweise", "Notes") @foreach (var notice in _financeResult.Notices) { @notice } @T("Jahresvergleich mit aktuellem Filter", "Year comparison with current filter") @T("Jahr", "Year") @T("Waehrung", "Currency") @T("Net Sales Actual", "Net sales actual") @T("Enthalten", "Included") @T("Ausgeschlossen", "Excluded") @context.Year @context.Currency @FormatValue(context.NetSalesActual, context.Currency) @context.IncludedRows.ToString("N0") @context.ExcludedRows.ToString("N0") @T("Finance-Status nach Land", "Finance status by country") @T("Status", "Status") @T("Land", "Country") TSC @T("Quelle", "Source") @T("Waehrung", "Currency") @T("Ist", "Actual") @T("Soll", "Reference") @T("Differenz", "Difference") @T("Zeilen", "Rows") @context.Status @context.CountryKey @context.Tscs @context.SourceSystems @context.Currency @FormatValue(context.NetSalesActual, context.Currency) @FormatNullableValue(context.ReferenceValue, context.Currency) @FormatNullableValue(context.Difference, context.Currency) @context.IncludedRows.ToString("N0") / @context.ExcludedRows.ToString("N0") @T("Keine Laenderdaten fuer diese Filter.", "No country data for these filters.") @T("Datenbestand nach Standort", "Data inventory by site") @T("Aktiv", "Active") @T("Land", "Country") TSC @T("Quelle", "Source") @T("Zentrale Zeilen", "Central rows") @T("Letzter Export", "Latest export") @T("Exportstatus", "Export status") @T("Letzte Speicherung", "Latest stored") @T("Manual Import", "Manual import") @context.Land @context.Tsc @context.SourceSystem @context.RowCount.ToString("N0") @FormatDateTime(context.LatestExportAt) @(string.IsNullOrWhiteSpace(context.LatestExportStatus) ? "-" : context.LatestExportStatus) @FormatDateTime(context.LatestStoredAtUtc) @FormatManualImportStatus(context) @T("Soll/Ist-Abweichungen", "Actual/reference deviations") @T("Status", "Status") @T("Land", "Country") @T("Waehrung", "Currency") @T("Ist", "Actual") @T("Soll", "Reference") @T("Differenz", "Difference") % @context.Status @context.CountryKey @context.Currency @FormatValue(context.NetSalesActual, context.Currency) @FormatNullableValue(context.ReferenceValue, context.Currency) @FormatNullableValue(context.Difference, context.Currency) @FormatPercent(context.DifferencePercent) @T("Keine Sollwerte oder keine Abweichungen fuer diese Filter.", "No reference values or deviations for these filters.") @T("Gutschriften-Kandidaten", "Credit-note candidates") @T("Diese Sicht zeigt technische Kandidaten anhand negativer Werte und erkennbarer Belegtypen/-nummern. Sie ersetzt keine landesspezifische Fachfreigabe.", "This view shows technical candidates based on negative values and recognizable document types/numbers. It does not replace country-specific business approval.") @T("Land", "Country") TSC @T("Rechnung", "Invoice") @T("Typ", "Type") @T("Wert", "Value") @T("Menge", "Quantity") @T("Grund", "Reason") @context.CountryKey @context.Tsc @context.InvoiceNumber @context.DocumentType @FormatValue(context.NetSalesActual, context.Currency) @context.Quantity.ToString("N2") @context.Reason @T("Keine Gutschriften-Kandidaten fuer diese Filter.", "No credit-note candidates for these filters.") @T("Pruefpunkte", "Checkpoints") @T("Status", "Status") @T("Pruefpunkt", "Checkpoint") @T("Anzahl", "Count") @context.Severity @context.Issue @context.Count.ToString("N0") @T("Keine Datenqualitaetsauffaelligkeiten fuer diese Filter.", "No data-quality findings for these filters.") @foreach (var file in _files) { @file.DisplayName } @foreach (var option in _valueFieldOptions) { @option.Label } @foreach (var option in _currencyOptions) { @option.Label } @T("Dateien laden", "Load files") @(_analyzing ? T("Analysiere...", "Analyzing...") : T("Cockpit erzeugen", "Build cockpit")) @T("Zentrale Roh-Auswertung", "Central raw analysis") @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.") @foreach (var year in _centralYears) { @year } @foreach (var month in Enumerable.Range(1, 12)) { @($"{month:D2}") } @foreach (var option in _valueFieldOptions) { @option.Label } @foreach (var option in _valueFieldOptions) { @option.Label } @foreach (var option in _currencyOptions) { @option.Label } @(_analyzingCentral ? T("Analysiere...", "Analyzing...") : T("Zentrale Auswertung laden", "Load central analysis")) @T("Global", "Global") @if (!string.IsNullOrWhiteSpace(_centralLandFilter) || !string.IsNullOrWhiteSpace(_centralTscFilter)) { @T("Gefiltert", "Filtered"): @($"{(_centralLandFilter ?? "-")} / {(_centralTscFilter ?? "-")}") } @if (_result is not null) { @T("Land", "Country")@_result.Summary.Land TSC@_result.Summary.Tsc @_result.Summary.ValueFieldLabel@FormatValue(_result.Summary.AggregatedValueTotal, _result.Summary.DisplayCurrency) @T("Nicht umgerechnet", "Not converted")@_result.Summary.MissingExchangeRateCount.ToString("N0") @T("Management Aussagen", "Management statements") @foreach (var finding in _result.Findings) { @finding.Title: @finding.Detail } @T("Top Kunden", "Top customers") @foreach (var item in _result.TopCustomers) { @($"{item.Label}: {FormatValue(item.Value, _result.Summary.DisplayCurrency)} ({item.SharePercent:F1}%)") } @T("Top Produktgruppen", "Top product groups") @foreach (var item in _result.TopProductGroups) { @($"{item.Label}: {FormatValue(item.Value, _result.Summary.DisplayCurrency)} ({item.SharePercent:F1}%)") } @T("Top Sales Owner", "Top sales owner") @foreach (var item in _result.TopSalesEmployees) { @($"{item.Label}: {FormatValue(item.Value, _result.Summary.DisplayCurrency)} ({item.SharePercent:F1}%)") } @T("Datenqualitaet", "Data quality") @foreach (var entry in _result.DataQualityCounts.OrderByDescending(x => x.Value)) { @($"{entry.Key}: {entry.Value}") } } @if (_centralResult is not null) { @T("Zeilen", "Rows")@_centralResult.Summary.RowCount.ToString("N0") @T("Rechnungen", "Invoices")@_centralResult.Summary.InvoiceCount.ToString("N0") @T("Standorte", "Sites")@_centralResult.Summary.SiteCount.ToString("N0") @T("Laender", "Countries")@_centralResult.Summary.CountryCount.ToString("N0") @_centralResult.Summary.ValueFieldLabel@FormatValue(_centralResult.Summary.ValueTotal, _centralResult.Summary.DisplayCurrency) @T("Nicht umgerechnet", "Not converted")@_centralResult.Summary.MissingExchangeRateCount.ToString("N0") @T("Hinweise", "Notes") @foreach (var notice in _centralResult.Notices) { @notice } @T("Jahreswerte", "Yearly values") @T("Jahr", "Year") @T("Waehrung", "Currency") @_centralResult.Summary.ValueFieldLabel @foreach (var field in _centralResult.AdditionalValueFields) { @field.Label } @T("Zeilen", "Rows") @context.Year @context.Currency @FormatValue(context.SalesValue, context.Currency) @foreach (var field in _centralResult.AdditionalValueFields) { @FormatAdditionalValue(context, field.Key) } @context.RowCount.ToString("N0") @T("Monatswerte", "Monthly values") @T("Monat", "Month") @T("Waehrung", "Currency") @_centralResult.Summary.ValueFieldLabel @foreach (var field in _centralResult.AdditionalValueFields) { @field.Label } @T("Zeilen", "Rows") @context.Label @context.Currency @FormatValue(context.SalesValue, context.Currency) @foreach (var field in _centralResult.AdditionalValueFields) { @FormatAdditionalValue(context, field.Key) } @context.RowCount.ToString("N0") @T("Tageswerte im ausgewaehlten Monat", "Daily values in selected month") @T("Tag", "Day") @T("Waehrung", "Currency") @_centralResult.Summary.ValueFieldLabel @foreach (var field in _centralResult.AdditionalValueFields) { @field.Label } @T("Zeilen", "Rows") @context.Label @context.Currency @FormatValue(context.SalesValue, context.Currency) @foreach (var field in _centralResult.AdditionalValueFields) { @FormatAdditionalValue(context, field.Key) } @context.RowCount.ToString("N0") @T("Fuer die Tagessicht bitte zusaetzlich einen Monat waehlen.", "Please select a month as well for the daily view.") @T("Werte nach Quelle", "Values by source") @T("Quelle", "Source") @T("Waehrung", "Currency") @_centralResult.Summary.ValueFieldLabel @T("Rechnungen", "Invoices") @context.Label @context.Currency @FormatValue(context.SalesValue, context.Currency) @context.InvoiceCount.ToString("N0") @T("Werte nach Land", "Values by country") @T("Land", "Country") @T("Waehrung", "Currency") @_centralResult.Summary.ValueFieldLabel @T("Rechnungen", "Invoices") @T("Zeilen", "Rows") @context.Label @context.Currency @FormatValue(context.SalesValue, context.Currency) @context.InvoiceCount.ToString("N0") @context.RowCount.ToString("N0") } } @code { private List _files = []; private List _centralYears = []; private List _financeYearOptions = []; private List _financeCountryOptions = []; private List _financeCurrencyOptions = []; private List _valueFieldOptions = []; private readonly List _currencyOptions = [ new(ManagementCockpitCurrencyOptions.Eur, "EUR"), new(ManagementCockpitCurrencyOptions.Usd, "USD"), new(ManagementCockpitCurrencyOptions.Native, "Original") ]; private string? _selectedFilePath; private ManagementCockpitResult? _result; private ManagementCockpitCentralResult? _centralResult; private ManagementFinanceSummaryResult? _financeResult; private int _selectedFinanceYear; private string? _selectedFinanceCountryKey; private string? _selectedFinanceCurrency; private int _selectedCentralYear; private int? _selectedCentralMonth; private string? _centralLandFilter; private string? _centralTscFilter; private string _selectedFileValueField = ManagementCockpitValueFieldKeys.SalesPriceValue; private string _selectedCentralValueField = ManagementCockpitValueFieldKeys.SalesPriceValue; private IEnumerable _selectedCentralAdditionalValueFields = []; private string _selectedFileTargetCurrency = ManagementCockpitCurrencyOptions.Eur; private string _selectedCentralTargetCurrency = ManagementCockpitCurrencyOptions.Native; private bool _loadingFiles; private bool _analyzing; private bool _analyzingCentral; private bool _analyzingFinance; protected override async Task OnInitializedAsync() { var state = await CockpitPageService.InitializeAsync(_selectedFilePath, _selectedCentralYear); _files = state.Files; _valueFieldOptions = state.ValueFieldOptions; _centralYears = state.CentralYears; _selectedFilePath = state.SelectedFilePath; _selectedCentralYear = state.SelectedCentralYear; _selectedFinanceYear = _selectedCentralYear; await AnalyzeFinanceSummary(); } private async Task ReloadFiles() { _loadingFiles = true; try { _files = await CockpitPageService.LoadFilesAsync(); _selectedFilePath ??= _files.FirstOrDefault()?.Path; } finally { _loadingFiles = false; } } private async Task ReloadCentralYears() { _centralYears = await CockpitPageService.LoadCentralYearsAsync(); if (_selectedCentralYear == 0) _selectedCentralYear = _centralYears.LastOrDefault(); } private async Task Analyze() { if (string.IsNullOrWhiteSpace(_selectedFilePath)) return; _analyzing = true; try { _result = await CockpitPageService.AnalyzeAsync(_selectedFilePath, new ManagementCockpitAnalysisOptions { ValueField = _selectedFileValueField, TargetCurrency = _selectedFileTargetCurrency }); _centralLandFilter = _result.Summary.Land; _centralTscFilter = _result.Summary.Tsc; } catch (Exception ex) { Snackbar.Add(string.Format(T("Cockpit konnte nicht erzeugt werden: {0}", "Could not build cockpit: {0}"), ex.Message), Severity.Error); } finally { _analyzing = false; } } private async Task AnalyzeCentral() { if (_selectedCentralYear == 0) return; _analyzingCentral = true; try { _centralResult = await CockpitPageService.AnalyzeCentralAsync(_selectedCentralYear, _selectedCentralMonth, new ManagementCockpitAnalysisOptions { ValueField = _selectedCentralValueField, AdditionalValueFields = _selectedCentralAdditionalValueFields.ToList(), TargetCurrency = _selectedCentralTargetCurrency, LandFilter = _centralLandFilter, TscFilter = _centralTscFilter }); } catch (Exception ex) { Snackbar.Add(string.Format(T("Zentrale Auswertung konnte nicht erzeugt werden: {0}", "Could not build central analysis: {0}"), ex.Message), Severity.Error); } finally { _analyzingCentral = false; } } private async Task AnalyzeFinanceSummary() { _analyzingFinance = true; try { _financeResult = await CockpitPageService.AnalyzeFinanceSummaryAsync( _selectedFinanceYear, _selectedFinanceCountryKey, _selectedFinanceCurrency); _financeYearOptions = _financeResult.YearOptions; _financeCountryOptions = _financeResult.CountryOptions; _financeCurrencyOptions = _financeResult.CurrencyOptions; _selectedFinanceYear = _financeResult.Filter.Year; } catch (Exception ex) { Snackbar.Add(string.Format(T("Finance Summary konnte nicht erzeugt werden: {0}", "Could not build finance summary: {0}"), ex.Message), Severity.Error); } finally { _analyzingFinance = false; } } private void ClearCentralScope() { _centralLandFilter = null; _centralTscFilter = null; } private static Severity MapSeverity(string severity) => severity switch { "Warning" => Severity.Warning, "Error" => Severity.Error, _ => Severity.Info }; private static string BuildPeriodLabel(ManagementCockpitCentralResult result) { if (result.Summary.PeriodStart is null || result.Summary.PeriodEnd is null) return "-"; return $"{result.Summary.PeriodStart.Value:dd.MM.yyyy} - {result.Summary.PeriodEnd.Value:dd.MM.yyyy}"; } private static string FormatValue(decimal value, string currency) => string.IsNullOrWhiteSpace(currency) || currency == "-" ? value.ToString("N2") : $"{value:N2} {currency}"; private static string FormatNullableValue(decimal? value, string currency) => value.HasValue ? FormatValue(value.Value, currency) : "-"; private static string FormatPercent(decimal? value) => value.HasValue ? $"{value.Value:N1}%" : "-"; private static string FormatDateTime(DateTime? value) => value.HasValue ? value.Value.ToLocalTime().ToString("dd.MM.yyyy HH:mm") : "-"; private static string FormatManualImportStatus(ManagementFinanceDataStatusRow row) { if (!string.Equals(row.SourceSystem, "MANUAL_EXCEL", StringComparison.OrdinalIgnoreCase)) return "-"; if (!string.IsNullOrWhiteSpace(row.ManualImportFilePath)) return row.ManualImportLastUploadedAtUtc.HasValue ? $"{System.IO.Path.GetFileName(row.ManualImportFilePath)} / {FormatDateTime(row.ManualImportLastUploadedAtUtc)}" : System.IO.Path.GetFileName(row.ManualImportFilePath); return "kein Pfad"; } private static Color StatusColor(string status) => status switch { "OK" => Color.Success, "Pruefen" => Color.Warning, _ => Color.Default }; private static Color SeverityColor(string severity) => severity switch { "Warning" => Color.Warning, "Error" => Color.Error, _ => Color.Info }; private void SetSelectedCentralAdditionalValueFields(IEnumerable values) { _selectedCentralAdditionalValueFields = values .Where(value => !string.IsNullOrWhiteSpace(value)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); } private static string FormatAdditionalValue(ManagementCockpitTimeValueRow row, string fieldKey) { if (!row.AdditionalValues.TryGetValue(fieldKey, out var value)) return "-"; var formattedValue = FormatValue(value.Value, value.Currency); return value.MissingExchangeRateCount == 0 ? formattedValue : $"{formattedValue} / {value.MissingExchangeRateCount} ohne Kurs"; } private sealed record CurrencySelectOption(string Key, string Label); } @code { private string T(string german, string english) => UiText.Text(german, english); }