@page "/management-cockpit"
@rendermode @(Microsoft.AspNetCore.Components.Web.RenderMode.InteractiveServer)
@using Microsoft.AspNetCore.Components
@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)
@($"{_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 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 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 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,
"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();
}
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 void ToggleProductFinanceTop10()
{
_limitProductFinanceTop10 = !_limitProductFinanceTop10;
}
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 Raw = 7;
}
private sealed record ProductFinanceGroupingOption(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);
}