@page "/management-cockpit" @rendermode @(Microsoft.AspNetCore.Components.Web.RenderMode.InteractiveServer) @using Microsoft.AspNetCore.Components @using Microsoft.JSInterop @using TrafagSalesExporter.Models @using TrafagSalesExporter.Services @inject IManagementCockpitPageService CockpitPageService @inject ISnackbar Snackbar @inject IUiTextService UiText @inject IJSRuntime JsRuntime @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) @($"{_financeResult.Filter.Year}") @T("Laender OK", "Countries OK") @_financeResult.CountryRows.Count(row => row.Status == "OK").ToString("N0") @T("Soll/Ist ohne Abweichung", "Actual/reference without deviation") @T("Zu pruefen", "To check") @_financeResult.CountryRows.Count(row => row.Status == "Pruefen").ToString("N0") @T("Abweichung oder offene Regel", "Deviation or open rule") @T("Datenstandorte", "Data sites") @_financeResult.DataStatusRows.Count(row => row.IsActive).ToString("N0") @T("aktive Quellen", "active sources") @T("Finance-Freigabe je Land", "Finance approval by country") @T("Status", "Status") @T("Land", "Country") @T("Ist", "Actual") @T("Soll", "Reference") @T("Differenz", "Difference") @T("Datenstand", "Data status") @T("Hinweis", "Note") @context.Status @FormatCountryWithFlag(context.CountryKey) @FormatValue(context.NetSalesActual, context.Currency) @FormatNullableValue(context.ReferenceValue, context.Currency) @FormatNullableValue(context.Difference, context.Currency) @BuildDataStatusText(context) @BuildQuickFinanceNote(context) @T("Letzter Datenstand je Standort", "Latest data status by site") @T("Aktiv", "Active") @T("Land", "Country") TSC @T("Quelle", "Source") @T("Zentrale Zeilen", "Central rows") @T("Letzter Export", "Latest export") @T("Status", "Status") @T("Manual Import", "Manual import") @context.Land @context.Tsc @context.SourceSystem @context.RowCount.ToString("N0") @FormatDateTime(context.LatestExportAt) @(string.IsNullOrWhiteSpace(context.LatestExportStatus) ? "-" : context.LatestExportStatus) @FormatManualImportStatus(context) @T("Sparten-Abdeckung nach Land", "Division coverage by country") @T("Land", "Country") TSC @T("Gesamtumsatz", "Total sales") @T("Zugeordnet", "Assigned") @T("Nicht im Stamm", "Not in master") @T("Abdeckung", "Coverage") @FormatCountryWithFlag(context.CountryKey) @context.Tsc @FormatValue(context.TotalValue, context.Currency) @FormatValue(context.AssignedValue, context.Currency) @FormatValue(context.MissingReferenceValue, context.Currency) @FormatPercent(context.AssignedValuePercent) @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 @FormatCountryWithFlag(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("IC/2nd-party", "IC/2nd-party") @T("Ist ohne IC", "Actual excl. IC") @T("Soll", "Reference") @T("Differenz", "Difference") @T("Zeilen", "Rows") @context.Status @FormatCountryWithFlag(context.CountryKey) @context.Tscs @context.SourceSystems @context.Currency @FormatValue(context.NetSalesActual, context.Currency) @FormatValue(context.IntercompanyValue, context.Currency) @FormatValue(context.NetSalesActualExcludingIntercompany, 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 @FormatCountryWithFlag(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") @FormatCountryWithFlag(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.") @T("Gesamtumsatz", "Total sales") @FormatValue(_financeResult.ProductFinanceSummary.TotalValue, _financeResult.ProductFinanceSummary.DisplayCurrency) @T("Zugeordneter Umsatz", "Assigned sales") @FormatValue(_financeResult.ProductFinanceSummary.AssignedValue, _financeResult.ProductFinanceSummary.DisplayCurrency) @FormatPercent(_financeResult.ProductFinanceSummary.AssignedValuePercent) @T("Nicht zugeordnet", "Unassigned") @FormatValue(_financeResult.ProductFinanceSummary.UnassignedValue, _financeResult.ProductFinanceSummary.DisplayCurrency) @FormatPercent(_financeResult.ProductFinanceSummary.UnassignedValuePercent) @T("Nicht im Stamm", "Not in master") @FormatValue(_financeResult.ProductFinanceSummary.MissingReferenceValue, _financeResult.ProductFinanceSummary.DisplayCurrency) @FormatPercent(_financeResult.ProductFinanceSummary.MissingReferenceValuePercent) @T("Umsatz nach Produktsparte", "Sales by product division") @foreach (var option in _productFinanceGroupingOptions) { @T(option.GermanLabel, option.EnglishLabel) } @T("Top 10 anzeigen", "Show top 10") @T("Produktsparte", "Product division") @if (ShowProductFamilyColumn) { @T("Produktfamilie", "Product family") } @if (ShowProductHierarchyColumn) { PAPH1 } @T("Umsatz", "Sales") @T("Anteil", "Share") @T("Materialien", "Materials") @T("Zeilen", "Rows") @T("Laender", "Countries") @BuildCodeText(context.ProductDivisionCode, context.ProductDivisionText) @if (ShowProductFamilyColumn) { @BuildCodeText(context.ProductFamilyCode, context.ProductFamilyText) } @if (ShowProductHierarchyColumn) { @BuildCodeText(context.ProductHierarchyCode, context.ProductHierarchyText) } @FormatValue(context.NetSalesActual, context.Currency) @FormatPercent(context.SharePercent) @context.MaterialCount.ToString("N0") @context.RowCount.ToString("N0") @FormatCountriesWithFlags(context.Countries) @T("Keine zugeordneten Spartenumsaetze fuer diese Filter.", "No assigned division sales for these filters.") @T("Umsatzabdeckung nach Land", "Sales coverage by country") @T("Land", "Country") TSC @T("Gesamt", "Total") @T("Zugeordnet", "Assigned") @T("Nicht zugeordnet", "Unassigned") @T("Nicht im Stamm", "Not in master") @T("Material fehlt", "Material missing") @T("Abdeckung", "Coverage") @FormatCountryWithFlag(context.CountryKey) @context.Tsc @FormatValue(context.TotalValue, context.Currency) @FormatValue(context.AssignedValue, context.Currency) @FormatValue(context.UnassignedValue, context.Currency) @FormatValue(context.MissingReferenceValue, context.Currency) @FormatValue(context.MissingMaterialValue, context.Currency) @FormatPercent(context.AssignedValuePercent) @T("Keine Umsatzabdeckung fuer diese Filter.", "No sales coverage for these filters.") @T("Materialien", "Materials") @_financeResult.ProductAssignmentSummary.DistinctMaterialCount.ToString("N0") @T("Zugeordnet", "Assigned") @_financeResult.ProductAssignmentSummary.MatchedMaterialCount.ToString("N0") @T("Nicht zugeordnet", "Unassigned") @_financeResult.ProductAssignmentSummary.UnassignedMaterialCount.ToString("N0") @T("Nicht im Stamm", "Not in master") @_financeResult.ProductAssignmentSummary.MissingReferenceMaterialCount.ToString("N0") @T("Material fehlt", "Material missing") @_financeResult.ProductAssignmentSummary.MissingMaterialNumberCount.ToString("N0") @T("TR-AG Referenz", "TR AG reference") @_financeResult.ProductAssignmentSummary.ReferenceMaterialCount.ToString("N0") @T("Diese Sicht prueft Materialnummern aller gefilterten Laender gegen die fuehrende TR-AG-Referenz aus `ProductDivisionRefSet`. Die Produktsparten der lokalen ERPs werden nicht verwendet.", "This view checks material numbers from all filtered countries against the leading TR AG reference from `ProductDivisionRefSet`. Local ERP product divisions are not used.") @T("Abdeckung nach Land", "Coverage by country") @T("Land", "Country") TSC @T("Materialien", "Materials") @T("Zugeordnet", "Assigned") @T("Nicht zugeordnet", "Unassigned") @T("Nicht im Stamm", "Not in master") @T("Material fehlt", "Material missing") @T("Trefferquote", "Match rate") @FormatCountryWithFlag(context.CountryKey) @context.Tsc @context.DistinctMaterialCount.ToString("N0") @context.MatchedMaterialCount.ToString("N0") @context.UnassignedMaterialCount.ToString("N0") @context.MissingReferenceMaterialCount.ToString("N0") @context.MissingMaterialNumberCount.ToString("N0") @FormatPercent(context.MatchPercent) @T("Keine Materialdaten fuer diese Filter.", "No material data for these filters.") @T("Materialpruefung gegen TR-AG-Referenz", "Material check against TR AG reference") @T("Status", "Status") @T("Land", "Country") TSC @T("Land-Material", "Local material") @T("Land-Text", "Local text") @T("TR-AG-MATNR", "TR AG MATNR") PAPH1 @T("Produktfamilie", "Product family") @T("Produktsparte", "Product division") @T("Zeilen", "Rows") @T("Finance-Wert", "Finance value") @context.Status @FormatCountryWithFlag(context.CountryKey) @context.Tsc @context.Material @context.ArticleName @context.ReferenceMaterial @BuildCodeText(context.ProductHierarchyCode, context.ProductHierarchyText) @BuildCodeText(context.ProductFamilyCode, context.ProductFamilyText) @BuildCodeText(context.ProductDivisionCode, context.ProductDivisionText) @context.RowCount.ToString("N0") @FormatValue(context.NetSalesActual, context.Currency) @T("Keine Materialpruefung fuer diese Filter.", "No material check for these filters.") @foreach (var option in _finance3dIndicatorOptions) { @T(option.GermanLabel, option.EnglishLabel) } @T("Drehbar mit gedrueckter Maus, Zoom mit Mausrad. X-Achse = Land, Z-Achse = Jahr, Hoehe = gewaehlter Indikator.", "Drag with the mouse to rotate, use the mouse wheel to zoom. X axis = country, Z axis = year, height = selected indicator.") @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("Kursdatum", "Rate date")@_centralResult.Summary.ExchangeRateDateLabel @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 { [Parameter] [SupplyParameterFromQuery(Name = "section")] public string? Section { get; set; } [Parameter] [SupplyParameterFromQuery(Name = "division")] public string? Division { get; set; } private List _files = []; private List _centralYears = []; private List _financeYearOptions = []; private List _financeCountryOptions = []; private List _financeCurrencyOptions = []; private List _valueFieldOptions = []; private readonly List _currencyOptions = [ new(ManagementCockpitCurrencyOptions.Chf, "CHF"), new(ManagementCockpitCurrencyOptions.Eur, "EUR"), new(ManagementCockpitCurrencyOptions.Usd, "USD"), new(ManagementCockpitCurrencyOptions.Native, "Original") ]; private readonly List _productFinanceGroupingOptions = [ new(ProductFinanceGroupLevels.Hierarchy, "PAPH1 Detail", "PAPH1 detail"), new(ProductFinanceGroupLevels.Family, "Produktfamilie", "Product family"), new(ProductFinanceGroupLevels.Division, "Produktsparte", "Product division") ]; private readonly List _finance3dIndicatorOptions = [ new(Finance3dIndicators.Actual, "Net Sales Actual", "Net sales actual"), new(Finance3dIndicators.IncludedRows, "Enthaltene Zeilen", "Included rows"), new(Finance3dIndicators.ExcludedRows, "Ausgeschlossene Zeilen", "Excluded rows"), new(Finance3dIndicators.Deviation, "Soll/Ist Differenz Filterjahr", "Actual/reference difference filter year") ]; 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; private int _activeOverviewTabIndex; private int _activeFinanceTabIndex; private int _activeDivisionTabIndex; private string _productFinanceGroupLevel = ProductFinanceGroupLevels.Hierarchy; private bool _limitProductFinanceTop10; private string _finance3dIndicator = Finance3dIndicators.Actual; private ElementReference _finance3dCanvas; private bool _finance3dNeedsRender; private bool ShowProductFamilyColumn => _productFinanceGroupLevel != ProductFinanceGroupLevels.Division; private bool ShowProductHierarchyColumn => _productFinanceGroupLevel == ProductFinanceGroupLevels.Hierarchy; protected override void OnParametersSet() { _activeOverviewTabIndex = string.IsNullOrWhiteSpace(Section) ? 0 : 1; if (string.Equals(Section, "division", StringComparison.OrdinalIgnoreCase)) { _activeFinanceTabIndex = ManagementFinanceTabIndexes.Division; _activeDivisionTabIndex = string.Equals(Division, "central", StringComparison.OrdinalIgnoreCase) ? 1 : 0; } else if (string.IsNullOrWhiteSpace(Section)) { _activeFinanceTabIndex = ManagementFinanceTabIndexes.Summary; _activeDivisionTabIndex = 0; } else { _activeFinanceTabIndex = Section.ToLowerInvariant() switch { "countries" => ManagementFinanceTabIndexes.Countries, "status" => ManagementFinanceTabIndexes.Status, "deviations" => ManagementFinanceTabIndexes.Deviations, "credits" => ManagementFinanceTabIndexes.Credits, "quality" => ManagementFinanceTabIndexes.Quality, "3d" => ManagementFinanceTabIndexes.ThreeD, "raw" => ManagementFinanceTabIndexes.Raw, _ => ManagementFinanceTabIndexes.Summary }; } } 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(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (_finance3dNeedsRender && _financeResult is not null) { _finance3dNeedsRender = false; await RenderFinance3dAsync(); } } 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; _finance3dNeedsRender = true; } 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 void ToggleProductFinanceTop10() { _limitProductFinanceTop10 = !_limitProductFinanceTop10; } private async Task SetFinance3dIndicator(string value) { _finance3dIndicator = string.IsNullOrWhiteSpace(value) ? Finance3dIndicators.Actual : value; await RenderFinance3dAsync(); } private async Task RenderFinance3dAsync() { if (_financeResult is null) return; var rows = BuildFinance3dRows(); await JsRuntime.InvokeVoidAsync("trafagFinance3d.render", _finance3dCanvas, rows, new { indicator = _finance3dIndicator, title = ResolveFinance3dIndicatorLabel(_finance3dIndicator) }); } private IReadOnlyList BuildFinance3dRows() { if (_financeResult is null) return []; var deviationsByKey = _financeResult.CountryRows .Where(row => row.Difference.HasValue) .GroupBy(row => $"{row.Year}|{row.CountryKey}", StringComparer.OrdinalIgnoreCase) .ToDictionary( group => group.Key, group => group.Sum(row => Math.Abs(row.Difference!.Value))); var sourceRows = _finance3dIndicator == Finance3dIndicators.Deviation ? _financeResult.Rows : (_financeResult.YearCountryRows.Count > 0 ? _financeResult.YearCountryRows : _financeResult.Rows); return sourceRows .OrderBy(row => row.CountryKey, StringComparer.OrdinalIgnoreCase) .ThenBy(row => row.Year) .Select(row => { deviationsByKey.TryGetValue($"{row.Year}|{row.CountryKey}", out var deviation); var value = _finance3dIndicator switch { Finance3dIndicators.IncludedRows => row.IncludedRows, Finance3dIndicators.ExcludedRows => row.ExcludedRows, Finance3dIndicators.Deviation => deviation, _ => Math.Abs(row.NetSalesActual) }; return new { country = row.CountryKey, year = row.Year, currency = row.Currency, value }; }) .Cast() .ToList(); } private string ResolveFinance3dIndicatorLabel(string key) => _finance3dIndicatorOptions.FirstOrDefault(option => option.Key == key) is { } option ? T(option.GermanLabel, option.EnglishLabel) : T("Net Sales Actual", "Net sales actual"); private IReadOnlyList BuildProductFinanceRows() { if (_financeResult is null) return []; var sourceRows = _financeResult.ProductDivisionFinanceRows; var totalsByCurrency = sourceRows .GroupBy(row => row.Currency, StringComparer.OrdinalIgnoreCase) .ToDictionary(group => group.Key, group => group.Sum(row => row.NetSalesActual), StringComparer.OrdinalIgnoreCase); var rows = sourceRows .GroupBy(row => BuildProductFinanceGroupKey(row)) .Select(group => { var value = group.Sum(row => row.NetSalesActual); totalsByCurrency.TryGetValue(group.Key.Currency, out var total); return new ManagementProductDivisionFinanceRow { ProductDivisionCode = group.Key.ProductDivisionCode, ProductDivisionText = group.Key.ProductDivisionText, ProductFamilyCode = group.Key.ProductFamilyCode, ProductFamilyText = group.Key.ProductFamilyText, ProductHierarchyCode = group.Key.ProductHierarchyCode, ProductHierarchyText = group.Key.ProductHierarchyText, Currency = group.Key.Currency, NetSalesActual = value, SharePercent = PercentOf(value, total), MaterialCount = group.Sum(row => row.MaterialCount), RowCount = group.Sum(row => row.RowCount), Countries = JoinCountries(group.Select(row => row.Countries)) }; }) .OrderByDescending(row => Math.Abs(row.NetSalesActual)) .ThenBy(row => row.ProductDivisionCode, StringComparer.OrdinalIgnoreCase) .ThenBy(row => row.ProductFamilyCode, StringComparer.OrdinalIgnoreCase) .ThenBy(row => row.ProductHierarchyCode, StringComparer.OrdinalIgnoreCase) .ToList(); return _limitProductFinanceTop10 ? rows.Take(10).ToList() : rows; } private ProductFinanceGroupKey BuildProductFinanceGroupKey(ManagementProductDivisionFinanceRow row) { return _productFinanceGroupLevel switch { ProductFinanceGroupLevels.Division => new ProductFinanceGroupKey( row.ProductDivisionCode, row.ProductDivisionText, string.Empty, string.Empty, string.Empty, string.Empty, row.Currency), ProductFinanceGroupLevels.Family => new ProductFinanceGroupKey( row.ProductDivisionCode, row.ProductDivisionText, row.ProductFamilyCode, row.ProductFamilyText, string.Empty, string.Empty, row.Currency), _ => new ProductFinanceGroupKey( row.ProductDivisionCode, row.ProductDivisionText, row.ProductFamilyCode, row.ProductFamilyText, row.ProductHierarchyCode, row.ProductHierarchyText, row.Currency) }; } 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 decimal PercentOf(decimal value, decimal total) => total == 0m ? 0m : value * 100m / total; 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 string BuildDataStatusText(ManagementFinanceCountryStatusRow countryRow) { if (_financeResult is null) return "-"; var tscs = countryRow.Tscs .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .ToHashSet(StringComparer.OrdinalIgnoreCase); var matchingRows = _financeResult.DataStatusRows .Where(row => row.Land.Equals(countryRow.CountryKey, StringComparison.OrdinalIgnoreCase) || tscs.Contains(row.Tsc)) .OrderByDescending(row => row.LatestExportAt ?? row.LatestStoredAtUtc ?? DateTime.MinValue) .ToList(); var latest = matchingRows.FirstOrDefault(); if (latest is null) return "-"; var date = latest.LatestExportAt ?? latest.LatestStoredAtUtc; var status = string.IsNullOrWhiteSpace(latest.LatestExportStatus) ? latest.SourceSystem : latest.LatestExportStatus; return $"{status} / {FormatDateTime(date)}"; } private string BuildQuickFinanceNote(ManagementFinanceCountryStatusRow row) { if (!row.ReferenceValue.HasValue) return T("Kein Sollwert gepflegt.", "No reference value maintained."); if (row.Status == "OK") return T("Freigabefaehig.", "Ready for approval."); if (row.Difference.HasValue) return T("Abweichung pruefen.", "Check deviation."); return T("Pruefen.", "Check."); } 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 static Color ProductAssignmentColor(string status) => status switch { "Zugeordnet" => Color.Success, "Nicht zugeordnet" => Color.Warning, "Nicht im TR-AG-Stamm" => Color.Error, "Material fehlt" => Color.Default, _ => Color.Info }; private static string BuildCodeText(string code, string text) { if (string.IsNullOrWhiteSpace(code)) return string.IsNullOrWhiteSpace(text) ? "-" : text; return string.IsNullOrWhiteSpace(text) ? code : $"{code} - {text}"; } private static string ResolveProductDivisionIcon( string productDivisionCode, string productDivisionText, string productFamilyText, string productHierarchyText) { var combinedText = string.Join(' ', productDivisionText, productFamilyText, productHierarchyText).ToUpperInvariant(); if (string.Equals(productDivisionCode, "UNASS", StringComparison.OrdinalIgnoreCase) || combinedText.Contains("NICHT ZUGEORDNET", StringComparison.OrdinalIgnoreCase) || combinedText.Contains("UNASS", StringComparison.OrdinalIgnoreCase)) { return Icons.Material.Filled.HelpOutline; } if (combinedText.Contains("GAS", StringComparison.OrdinalIgnoreCase) || combinedText.Contains("DENSITY", StringComparison.OrdinalIgnoreCase)) { return Icons.Material.Filled.Sensors; } if (combinedText.Contains("PRESSURE", StringComparison.OrdinalIgnoreCase) || combinedText.Contains("DRUCK", StringComparison.OrdinalIgnoreCase)) { return Icons.Material.Filled.Compress; } if (combinedText.Contains("TEMP", StringComparison.OrdinalIgnoreCase) || combinedText.Contains("THERMOSTAT", StringComparison.OrdinalIgnoreCase)) { return Icons.Material.Filled.DeviceThermostat; } if (combinedText.Contains("SWITCH", StringComparison.OrdinalIgnoreCase) || combinedText.Contains("SCHALTER", StringComparison.OrdinalIgnoreCase)) { return Icons.Material.Filled.ToggleOn; } if (combinedText.Contains("ACCESS", StringComparison.OrdinalIgnoreCase) || combinedText.Contains("ZUBEH", StringComparison.OrdinalIgnoreCase)) { return Icons.Material.Filled.Extension; } return Icons.Material.Filled.Category; } private static string JoinCountries(IEnumerable countryValues) { var countries = countryValues .SelectMany(value => value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) .Where(value => !string.IsNullOrWhiteSpace(value)) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(value => value, StringComparer.OrdinalIgnoreCase) .Select(FormatCountryWithFlag); return string.Join(", ", countries); } private static string FormatCountriesWithFlags(string countries) => string.IsNullOrWhiteSpace(countries) ? "-" : JoinCountries([countries]); private static string FormatCountryWithFlag(string country) { if (string.IsNullOrWhiteSpace(country)) return "-"; var normalized = country.Trim().ToUpperInvariant(); if (normalized.Length != 2 || normalized.Any(character => character is < 'A' or > 'Z')) return country; var flag = string.Concat(normalized.Select(character => char.ConvertFromUtf32(0x1F1E6 + character - 'A'))); return $"{flag} {normalized}"; } 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 string T(string german, string english) => UiText.Text(german, english); private static class ProductFinanceGroupLevels { public const string Hierarchy = "hierarchy"; public const string Family = "family"; public const string Division = "division"; } private static class ManagementFinanceTabIndexes { public const int Summary = 0; public const int Countries = 1; public const int Status = 2; public const int Deviations = 3; public const int Credits = 4; public const int Quality = 5; public const int Division = 6; public const int ThreeD = 7; public const int Raw = 8; } private static class Finance3dIndicators { public const string Actual = "actual"; public const string IncludedRows = "includedRows"; public const string ExcludedRows = "excludedRows"; public const string Deviation = "deviation"; } private sealed record ProductFinanceGroupingOption(string Key, string GermanLabel, string EnglishLabel); private sealed record Finance3dIndicatorOption(string Key, string GermanLabel, string EnglishLabel); private sealed record ProductFinanceGroupKey( string ProductDivisionCode, string ProductDivisionText, string ProductFamilyCode, string ProductFamilyText, string ProductHierarchyCode, string ProductHierarchyText, string Currency); private sealed record CurrencySelectOption(string Key, string Label); }