diff --git a/TrafagSalesExporter/Components/Layout/NavMenu.razor b/TrafagSalesExporter/Components/Layout/NavMenu.razor index 77eaa78..1ecee67 100644 --- a/TrafagSalesExporter/Components/Layout/NavMenu.razor +++ b/TrafagSalesExporter/Components/Layout/NavMenu.razor @@ -2,35 +2,38 @@ @inject TrafagSalesExporter.Services.IUiTextService UiText - - @T("Dashboard", "Dashboard") - - - - - @T("Standorte", "Sites") - - - @T("Transformationen", "Transformations") - - - - - @T("Management Cockpit", "Management Cockpit") - + + + @T("Export Dashboard", "Export dashboard") + + + @T("Management Analyse", "Management analysis") + + + @T("Soll/Ist Vergleich", "Actual/reference comparison") + + - @T("HR KPI", "HR KPI") - - - - - @T("Settings", "Settings") - - - - - @T("Logs", "Logs") + @T("HR KPI (Login)", "HR KPI (login)") + + + + + @T("Standorte", "Sites") + + + @T("Transformationen", "Transformations") + + + @T("Settings", "Settings") + + + + + @T("Logs", "Logs") + + @code { diff --git a/TrafagSalesExporter/Components/Pages/Dashboard.razor b/TrafagSalesExporter/Components/Pages/Dashboard.razor index 12bc323..566dbc5 100644 --- a/TrafagSalesExporter/Components/Pages/Dashboard.razor +++ b/TrafagSalesExporter/Components/Pages/Dashboard.razor @@ -8,66 +8,9 @@ @inject IUiTextService UiText @implements IDisposable -@T("Dashboard", "Dashboard") +@T("Export Dashboard", "Export dashboard") -@T("Dashboard", "Dashboard") - - - - @T("Net Sales Actuals 2025 Referenz", "Net sales actuals 2025 reference") - - check.xlsx / Power BI Stand 29.04.2026 - - - - @T("Firma", "Company") - @T("Ist 2025", "Actual 2025") - @T("IC-Abzug", "IC deduction") - @T("Ist exkl. IC", "Actual excl. IC") - @T("Referenz", "Reference") - @T("Summenfeld", "Value field") - @T("Quelle", "Source") - @T("Differenz", "Difference") - @T("Diff exkl. IC", "Diff excl. IC") - @T("Waehrung", "Currency") - @T("Zeilen", "Rows") - @T("Status", "Status") - - - @context.Label - @FormatAmount(context.ActualValue) - @FormatAmount(context.IntercompanyDeduction) - @FormatAmount(context.ActualValueExcludingIntercompany) - @FormatAmount(context.ReferenceValue) - @(string.IsNullOrWhiteSpace(context.ValueField) ? "-" : context.ValueField) - @context.ReferenceSource - @FormatAmount(context.Difference) - @FormatAmount(context.DifferenceExcludingIntercompany) - @(string.IsNullOrWhiteSpace(context.Currencies) ? "-" : context.Currencies) - @(context.RowCount > 0 ? context.RowCount.ToString("N0") : "-") - - @if (context.Status == "OK") - { - OK - } - else if (context.Status == "Pruefen") - { - @T("Pruefen", "Check") - } - else - { - @T("Keine Daten", "No data") - } - - - - @T("Keine Referenzdaten fuer aktive Standorte gefunden.", "No reference data found for active sites.") - - - - @T("Vergleich: Jahr 2025 aus Invoice Date, sonst Extraction Date. Das Summenfeld wird automatisch aus Sales Price/Value, DocTotalFC - VatSumFC oder DocTotal - VatSum gewaehlt; Belegkopfwerte werden pro DocEntry nur einmal gezaehlt. IC-Abzug ist eine Diagnose fuer den aktuellen Trafag-IT-Abgleich und veraendert die Originaldaten nicht.", "Comparison: year 2025 from Invoice Date, otherwise Extraction Date. The value field is selected automatically from Sales Price/Value, DocTotalFC - VatSumFC, or DocTotal - VatSum; document header values are counted only once per DocEntry. IC deduction is a diagnostic value for the current Trafag IT reconciliation and does not change the original data.") - - +@T("Export Dashboard", "Export dashboard") @@ -212,7 +155,6 @@ @code { private List _dashboardRows = new(); private List _consolidatedRows = new(); - private List _netSalesReferenceRows = new(); private bool _loading = true; private bool _anyRunning; private CancellationTokenSource? _pollingCts; @@ -229,7 +171,6 @@ var state = await DashboardPageActions.LoadAsync(); _dashboardRows = state.DashboardRows; _consolidatedRows = state.ConsolidatedRows; - _netSalesReferenceRows = state.NetSalesReferenceRows; _anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting(); _loading = false; @@ -460,9 +401,6 @@ return Task.CompletedTask; } - private static string FormatAmount(decimal? value) - => value.HasValue ? value.Value.ToString("N2") : "-"; - private static string FormatException(Exception ex) => ex.InnerException is null ? ex.Message : $"{ex.Message} Inner: {ex.InnerException.Message}"; diff --git a/TrafagSalesExporter/Components/Pages/FinanceComparison.razor b/TrafagSalesExporter/Components/Pages/FinanceComparison.razor new file mode 100644 index 0000000..62f0507 --- /dev/null +++ b/TrafagSalesExporter/Components/Pages/FinanceComparison.razor @@ -0,0 +1,96 @@ +@page "/finance-cockpit/vergleich" +@using TrafagSalesExporter.Models +@using TrafagSalesExporter.Services +@inject IFinanceReconciliationService FinanceReconciliationService +@inject IUiTextService UiText + +@T("Soll/Ist Vergleich", "Actual/reference comparison") + +@T("Soll/Ist Vergleich", "Actual/reference comparison") + + + + + @T("Net Sales Actuals 2025 Referenz", "Net sales actuals 2025 reference") + check.xlsx / Power BI Stand 29.04.2026 + + + + @(_loading ? T("Lade...", "Loading...") : T("Aktualisieren", "Refresh")) + + + + + + @T("Firma", "Company") + @T("Ist 2025", "Actual 2025") + @T("IC-Abzug", "IC deduction") + @T("Ist exkl. IC", "Actual excl. IC") + @T("Referenz", "Reference") + @T("Summenfeld", "Value field") + @T("Quelle", "Source") + @T("Differenz", "Difference") + @T("Diff exkl. IC", "Diff excl. IC") + @T("Waehrung", "Currency") + @T("Zeilen", "Rows") + @T("Status", "Status") + + + @context.Label + @FormatAmount(context.ActualValue) + @FormatAmount(context.IntercompanyDeduction) + @FormatAmount(context.ActualValueExcludingIntercompany) + @FormatAmount(context.ReferenceValue) + @(string.IsNullOrWhiteSpace(context.ValueField) ? "-" : context.ValueField) + @context.ReferenceSource + @FormatAmount(context.Difference) + @FormatAmount(context.DifferenceExcludingIntercompany) + @(string.IsNullOrWhiteSpace(context.Currencies) ? "-" : context.Currencies) + @(context.RowCount > 0 ? context.RowCount.ToString("N0") : "-") + + @if (context.Status == "OK") + { + OK + } + else if (context.Status == "Pruefen") + { + @T("Pruefen", "Check") + } + else + { + @T("Keine Daten", "No data") + } + + + + @T("Keine Referenzdaten fuer aktive Standorte gefunden.", "No reference data found for active sites.") + + + + + @T("Vergleich: Jahr 2025 aus Buchungsdatum, sonst Invoice Date, sonst Extraction Date. Das Summenfeld wird automatisch aus Sales Price/Value, DocTotalFC - VatSumFC oder DocTotal - VatSum gewaehlt; Belegkopfwerte werden pro DocEntry nur einmal gezaehlt. IC-Abzug ist eine Diagnose fuer den aktuellen Abgleich und veraendert die Originaldaten nicht.", "Comparison: year 2025 from posting date, otherwise invoice date, otherwise extraction date. The value field is selected automatically from Sales Price/Value, DocTotalFC - VatSumFC, or DocTotal - VatSum; document header values are counted only once per DocEntry. IC deduction is a diagnostic value for the current reconciliation and does not change the original data.") + + + +@code { + private List _netSalesReferenceRows = new(); + private bool _loading = true; + + protected override async Task OnInitializedAsync() + { + await LoadAsync(); + } + + private async Task LoadAsync() + { + _loading = true; + _netSalesReferenceRows = await FinanceReconciliationService.BuildNetSalesReferenceRowsAsync(2025); + _loading = false; + } + + private static string FormatAmount(decimal? value) + => value.HasValue ? value.Value.ToString("N2") : "-"; + + private string T(string german, string english) => UiText.Text(german, english); +} diff --git a/TrafagSalesExporter/Components/Pages/HrKpi.razor b/TrafagSalesExporter/Components/Pages/HrKpi.razor index f818afb..e7d0aef 100644 --- a/TrafagSalesExporter/Components/Pages/HrKpi.razor +++ b/TrafagSalesExporter/Components/Pages/HrKpi.razor @@ -4,6 +4,7 @@ @using TrafagSalesExporter.Services @inject IHrKpiService HrKpiService @inject IOptions DataSourceOptions +@inject IHrKpiAccessService HrKpiAccess @inject ISnackbar Snackbar @inject IUiTextService UiText @@ -11,94 +12,125 @@ @T("HR KPI", "HR KPI") - - - - - - - - @foreach (var option in _result?.ExitYearOptions ?? []) - { - @option - } - - - - - @foreach (var option in _result?.OrganisationOptions ?? []) - { - @option - } - - - - - @(_loading ? T("Lade...", "Loading...") : T("Laden", "Load")) +@if (!CanShowHrKpi) +{ + + + + @T("HR KPI enthaelt sensible Personaldaten. Bitte separat anmelden.", "HR KPI contains sensitive HR data. Please sign in separately.") + + @if (!HrKpiAccess.IsConfigured) + { + + @T("HR-KPI-Zugang ist noch nicht konfiguriert. Bitte Username und PasswordHash in HrKpiAccess konfigurieren.", "HR KPI access is not configured yet. Please configure Username and PasswordHash in HrKpiAccess.") + + } + + + + @T("HR KPI entsperren", "Unlock HR KPI") - - - - - - - - - - @foreach (var option in _result?.EntryYearOptions ?? []) - { - @option - } - - - - - - - - @foreach (var option in _result?.KostenstelleOptions ?? []) - { - @option - } - - - - - @foreach (var option in _result?.MitarbeitertypOptions ?? []) - { - @option - } - - - - - @foreach (var option in _fluktuationOptions) - { - @option.Label - } - - - - - @foreach (var option in _ampelOptions) - { - @option - } - - - - - @foreach (var option in _restferienOptions) - { - @option - } - - - - + + +} +else +{ + + + + + + + + @foreach (var option in _result?.ExitYearOptions ?? []) + { + @option + } + + + + + @foreach (var option in _result?.OrganisationOptions ?? []) + { + @option + } + + + + + @(_loading ? T("Lade...", "Loading...") : T("Laden", "Load")) + + + + + + + + + + + @foreach (var option in _result?.EntryYearOptions ?? []) + { + @option + } + + + + + + + + @foreach (var option in _result?.KostenstelleOptions ?? []) + { + @option + } + + + + + @foreach (var option in _result?.MitarbeitertypOptions ?? []) + { + @option + } + + + + + @foreach (var option in _fluktuationOptions) + { + @option.Label + } + + + + + @foreach (var option in _ampelOptions) + { + @option + } + + + + + @foreach (var option in _restferienOptions) + { + @option + } + + + + + @T("Sperren", "Lock") + + + + +} -@if (_result is not null) +@if (CanShowHrKpi && _result is not null) { @if (_result.Notices.Count > 0) { @@ -127,6 +159,8 @@ private string? _glzAmpel; private string? _restferienAmpel; private string? _searchText; + private string? _hrUsername; + private string? _hrPassword; private bool _loading; private HrKpiResult? _result; private readonly List<(string Key, string Label)> _fluktuationOptions = @@ -142,11 +176,19 @@ protected override async Task OnInitializedAsync() { _dataFolder = DataSourceOptions.Value.Normalize().DataFolder; - await LoadAsync(); + if (CanShowHrKpi) + { + await LoadAsync(); + } } private async Task LoadAsync() { + if (!CanShowHrKpi) + { + return; + } + _loading = true; try { @@ -176,5 +218,26 @@ } } + private async Task UnlockHrKpiAsync() + { + if (!HrKpiAccess.TryUnlock(_hrUsername ?? string.Empty, _hrPassword ?? string.Empty)) + { + Snackbar.Add(T("HR-KPI-Anmeldung fehlgeschlagen.", "HR KPI sign-in failed."), Severity.Error); + return; + } + + _hrPassword = string.Empty; + await LoadAsync(); + } + + private void LockHrKpi() + { + HrKpiAccess.Lock(); + _result = null; + _hrPassword = string.Empty; + } + + private bool CanShowHrKpi => !HrKpiAccess.IsEnabled || HrKpiAccess.IsUnlocked; + private string T(string german, string english) => UiText.Text(german, english); } diff --git a/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor b/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor index 91ed3af..d8d16d4 100644 --- a/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor +++ b/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor @@ -5,9 +5,9 @@ @inject ISnackbar Snackbar @inject IUiTextService UiText -@T("Management Cockpit", "Management Cockpit") +@T("Management Analyse", "Management analysis") -@T("Management Cockpit", "Management Cockpit") +@T("Management Analyse", "Management analysis") @@ -64,6 +64,12 @@ } + + + + + + @foreach (var month in Enumerable.Range(1, 12)) @@ -102,10 +108,22 @@ - - @(_analyzingCentral ? T("Analysiere...", "Analyzing...") : T("Zentrale Auswertung laden", "Load central analysis")) - + + + @(_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 ?? "-")}") + + } + @@ -332,6 +350,8 @@ private ManagementCockpitCentralResult? _centralResult; 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 = []; @@ -385,6 +405,8 @@ ValueField = _selectedFileValueField, TargetCurrency = _selectedFileTargetCurrency }); + _centralLandFilter = _result.Summary.Land; + _centralTscFilter = _result.Summary.Tsc; } catch (Exception ex) { @@ -408,7 +430,9 @@ { ValueField = _selectedCentralValueField, AdditionalValueFields = _selectedCentralAdditionalValueFields.ToList(), - TargetCurrency = _selectedCentralTargetCurrency + TargetCurrency = _selectedCentralTargetCurrency, + LandFilter = _centralLandFilter, + TscFilter = _centralTscFilter }); } catch (Exception ex) @@ -421,6 +445,12 @@ } } + private void ClearCentralScope() + { + _centralLandFilter = null; + _centralTscFilter = null; + } + private static Severity MapSeverity(string severity) => severity switch { "Warning" => Severity.Warning, diff --git a/TrafagSalesExporter/Models/ManagementCockpitModels.cs b/TrafagSalesExporter/Models/ManagementCockpitModels.cs index 3bec7e2..63853f8 100644 --- a/TrafagSalesExporter/Models/ManagementCockpitModels.cs +++ b/TrafagSalesExporter/Models/ManagementCockpitModels.cs @@ -34,6 +34,8 @@ public class ManagementCockpitAnalysisOptions public string ValueField { get; set; } = ManagementCockpitValueFieldKeys.SalesPriceValue; public List AdditionalValueFields { get; set; } = []; public string TargetCurrency { get; set; } = ManagementCockpitCurrencyOptions.Native; + public string? LandFilter { get; set; } + public string? TscFilter { get; set; } } public class ManagementCockpitSummary @@ -89,6 +91,8 @@ public class ManagementCockpitCentralFilter public int? Month { get; set; } public string ValueField { get; set; } = ManagementCockpitValueFieldKeys.SalesPriceValue; public string TargetCurrency { get; set; } = ManagementCockpitCurrencyOptions.Native; + public string? Land { get; set; } + public string? Tsc { get; set; } } public class ManagementCockpitCentralSummary diff --git a/TrafagSalesExporter/Program.cs b/TrafagSalesExporter/Program.cs index 14ab3de..56dc36a 100644 --- a/TrafagSalesExporter/Program.cs +++ b/TrafagSalesExporter/Program.cs @@ -45,6 +45,7 @@ builder.Services.AddAuthorization(options => builder.Services.AddMudServices(); builder.Services.AddHttpClient(nameof(ExchangeRateImportService)); builder.Services.Configure(builder.Configuration.GetSection(HrKpiDataSourceOptions.SectionName)); +builder.Services.Configure(builder.Configuration.GetSection(HrKpiAccessOptions.SectionName)); builder.Services.AddDbContextFactory(options => options.UseSqlite("Data Source=trafag_exporter.db;Default Timeout=60")); @@ -104,6 +105,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); diff --git a/TrafagSalesExporter/Security/HrKpiAccessOptions.cs b/TrafagSalesExporter/Security/HrKpiAccessOptions.cs new file mode 100644 index 0000000..670e6e6 --- /dev/null +++ b/TrafagSalesExporter/Security/HrKpiAccessOptions.cs @@ -0,0 +1,11 @@ +namespace TrafagSalesExporter.Security; + +public sealed class HrKpiAccessOptions +{ + public const string SectionName = "HrKpiAccess"; + + public bool Enabled { get; set; } = true; + public string Username { get; set; } = "hr"; + public string PasswordHash { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} diff --git a/TrafagSalesExporter/Services/DashboardPageService.cs b/TrafagSalesExporter/Services/DashboardPageService.cs index 93cc782..cd60a41 100644 --- a/TrafagSalesExporter/Services/DashboardPageService.cs +++ b/TrafagSalesExporter/Services/DashboardPageService.cs @@ -12,14 +12,10 @@ public interface IDashboardPageService public sealed class DashboardPageService : IDashboardPageService { private readonly IDbContextFactory _dbFactory; - private readonly IFinanceReconciliationService _financeReconciliationService; - public DashboardPageService( - IDbContextFactory dbFactory, - IFinanceReconciliationService financeReconciliationService) + public DashboardPageService(IDbContextFactory dbFactory) { _dbFactory = dbFactory; - _financeReconciliationService = financeReconciliationService; } public async Task LoadAsync() @@ -69,8 +65,7 @@ public sealed class DashboardPageService : IDashboardPageService return new DashboardPageState { DashboardRows = rows, - ConsolidatedRows = BuildConsolidatedRows(await db.ExportSettings.FirstOrDefaultAsync() ?? new()), - NetSalesReferenceRows = await _financeReconciliationService.BuildNetSalesReferenceRowsAsync(2025) + ConsolidatedRows = BuildConsolidatedRows(await db.ExportSettings.FirstOrDefaultAsync() ?? new()) }; } @@ -119,7 +114,6 @@ public sealed class DashboardPageState { public List DashboardRows { get; set; } = []; public List ConsolidatedRows { get; set; } = []; - public List NetSalesReferenceRows { get; set; } = []; } public sealed class DashboardRow diff --git a/TrafagSalesExporter/Services/HrKpiAccessService.cs b/TrafagSalesExporter/Services/HrKpiAccessService.cs new file mode 100644 index 0000000..0d5691d --- /dev/null +++ b/TrafagSalesExporter/Services/HrKpiAccessService.cs @@ -0,0 +1,74 @@ +using System.Security.Cryptography; +using System.Text; +using Microsoft.Extensions.Options; +using TrafagSalesExporter.Security; + +namespace TrafagSalesExporter.Services; + +public interface IHrKpiAccessService +{ + bool IsEnabled { get; } + bool IsConfigured { get; } + bool IsUnlocked { get; } + bool TryUnlock(string username, string password); + void Lock(); +} + +public sealed class HrKpiAccessService : IHrKpiAccessService +{ + private readonly HrKpiAccessOptions _options; + + public HrKpiAccessService(IOptions options) + { + _options = options.Value; + } + + public bool IsEnabled => _options.Enabled; + + public bool IsConfigured => + !IsEnabled || + !string.IsNullOrWhiteSpace(_options.Username) && + (!string.IsNullOrWhiteSpace(_options.PasswordHash) || !string.IsNullOrEmpty(_options.Password)); + + public bool IsUnlocked { get; private set; } + + public bool TryUnlock(string username, string password) + { + if (!IsEnabled) + { + IsUnlocked = true; + return true; + } + + if (!IsConfigured || + string.IsNullOrWhiteSpace(username) || + string.IsNullOrEmpty(password) || + !FixedEquals(username.Trim(), _options.Username.Trim())) + { + return false; + } + + var valid = !string.IsNullOrWhiteSpace(_options.PasswordHash) + ? VerifyPasswordHash(password, _options.PasswordHash) + : FixedEquals(password, _options.Password); + + IsUnlocked = valid; + return valid; + } + + public void Lock() => IsUnlocked = false; + + private static bool VerifyPasswordHash(string password, string configuredHash) + { + var passwordHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(password))); + return FixedEquals(passwordHash, configuredHash.Trim()); + } + + private static bool FixedEquals(string left, string right) + { + var leftBytes = Encoding.UTF8.GetBytes(left); + var rightBytes = Encoding.UTF8.GetBytes(right); + return leftBytes.Length == rightBytes.Length && + CryptographicOperations.FixedTimeEquals(leftBytes, rightBytes); + } +} diff --git a/TrafagSalesExporter/Services/ManagementCockpitService.cs b/TrafagSalesExporter/Services/ManagementCockpitService.cs index 441296e..6c37c12 100644 --- a/TrafagSalesExporter/Services/ManagementCockpitService.cs +++ b/TrafagSalesExporter/Services/ManagementCockpitService.cs @@ -199,14 +199,17 @@ public class ManagementCockpitService : IManagementCockpitService .Select(row => BuildCentralAggregationRow(row, aggregation)) .ToList(); - var selectedRows = aggregatedRows + var scopedRows = ApplyCentralDimensionFilters(aggregatedRows, options) + .ToList(); + + var selectedRows = scopedRows .Where(r => r.PeriodDate.Year == year && (!month.HasValue || r.PeriodDate.Month == month.Value)) .ToList(); if (selectedRows.Count == 0) throw new InvalidOperationException("Für den gewählten Zeitraum gibt es keine Datensätze in der zentralen Tabelle."); - var yearlyRows = aggregatedRows; + var yearlyRows = scopedRows; var dailyBaseRows = selectedRows .Where(r => month.HasValue) @@ -219,7 +222,9 @@ public class ManagementCockpitService : IManagementCockpitService Year = year, Month = month, ValueField = aggregation.ValueField.Key, - TargetCurrency = aggregation.TargetCurrency + TargetCurrency = aggregation.TargetCurrency, + Land = NormalizeOptionalFilter(options?.LandFilter), + Tsc = NormalizeOptionalFilter(options?.TscFilter) }, Summary = new ManagementCockpitCentralSummary { @@ -239,7 +244,7 @@ public class ManagementCockpitService : IManagementCockpitService AdditionalValueFields = aggregation.AdditionalValueFields .Select(ToValueFieldOption) .ToList(), - Notices = BuildCentralNotices(aggregation, selectedRows.Count(x => x.MissingExchangeRate)), + Notices = BuildCentralNotices(aggregation, selectedRows.Count(x => x.MissingExchangeRate), options), YearlyTotals = yearlyRows .GroupBy(x => new { x.PeriodDate.Year, x.DisplayCurrency }) .OrderBy(g => g.Key.Year) @@ -291,6 +296,18 @@ public class ManagementCockpitService : IManagementCockpitService }; } + private static IEnumerable ApplyCentralDimensionFilters( + IEnumerable rows, + ManagementCockpitAnalysisOptions? options) + { + var landFilter = NormalizeOptionalFilter(options?.LandFilter); + var tscFilter = NormalizeOptionalFilter(options?.TscFilter); + + return rows.Where(row => + (landFilter is null || string.Equals(row.Land, landFilter, StringComparison.OrdinalIgnoreCase)) && + (tscFilter is null || string.Equals(row.Tsc, tscFilter, StringComparison.OrdinalIgnoreCase))); + } + private static IEnumerable GetCandidateDirectories(ExportSettings settings) { yield return Path.Combine(AppContext.BaseDirectory, "output"); @@ -456,7 +473,10 @@ public class ManagementCockpitService : IManagementCockpitService }; } - private static List BuildCentralNotices(AggregationSelection aggregation, int missingExchangeRateCount) + private static List BuildCentralNotices( + AggregationSelection aggregation, + int missingExchangeRateCount, + ManagementCockpitAnalysisOptions? options) { var notices = new List { @@ -467,6 +487,13 @@ public class ManagementCockpitService : IManagementCockpitService "Periodenlogik basiert auf Invoice Date, falls vorhanden, sonst auf Extraction Date." }; + var landFilter = NormalizeOptionalFilter(options?.LandFilter); + var tscFilter = NormalizeOptionalFilter(options?.TscFilter); + if (landFilter is not null || tscFilter is not null) + { + notices.Add($"Filter aus Auswahl: Land {(landFilter ?? "alle")}, TSC {(tscFilter ?? "alle")}."); + } + if (aggregation.AdditionalValueFields.Count > 0) notices.Add($"Weitere Summenfelder: {string.Join(", ", aggregation.AdditionalValueFields.Select(x => x.Label))}."); @@ -488,6 +515,9 @@ public class ManagementCockpitService : IManagementCockpitService return notices; } + private static string? NormalizeOptionalFilter(string? value) + => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + private static ManagementCockpitTimeValueRow BuildTimeValueRow( IEnumerable groupRows, AggregationSelection aggregation, diff --git a/TrafagSalesExporter/appsettings.json b/TrafagSalesExporter/appsettings.json index 629f479..7ac8233 100644 --- a/TrafagSalesExporter/appsettings.json +++ b/TrafagSalesExporter/appsettings.json @@ -24,5 +24,10 @@ "SapFile": "HR_KPI_Export.xlsx", "AbsenceFile": "Abwesenheitinstunden.xlsx", "LeaverFile": "Personalausgeschieden.xlsx" + }, + "HrKpiAccess": { + "Enabled": true, + "Username": "hr", + "PasswordHash": "A8AF253007750E0C2986CBD0BC570530B4AE2417AAC59067591E708547834AE4" } } diff --git a/TrafagSalesExporter/lastchange.md b/TrafagSalesExporter/lastchange.md index c8f533d..dbf8b2d 100644 --- a/TrafagSalesExporter/lastchange.md +++ b/TrafagSalesExporter/lastchange.md @@ -1313,3 +1313,111 @@ Inhalt: - Todo-Liste fuer Group Sales Reporting Intranet-Dashboard. - Priorisierte Punkte fuer CFO-Dokument, offene Laenderabweichungen, Intercompany, Budgetkurse und Berechtigungskonzept. + +## Navigation und HR-KPI-Zugriff 2026-05-15 + +Geaendert: + +- Linke Navigation reduziert: + - Hauptgruppe `Finance Cockpit` + - eigener Hauptpunkt `HR KPI (Login)` +- Bisherige Finance-Seiten liegen als Unterpunkte unter `Finance Cockpit`: + - Dashboard + - Management Cockpit + - Standorte + - Transformationen + - Settings + - Logs +- HR KPI hat eine separate zweite Zugriffssperre mit Name und Passwort. +- HR-Daten werden erst geladen und angezeigt, wenn die HR-KPI-Sperre erfolgreich entsperrt wurde. + +Konfiguration: + +- Abschnitt `HrKpiAccess` in `appsettings.json` +- Benutzer: `hr` +- Passwortvorschlag: `Trafag-HR-KPI-2026!` +- Im Repo ist nur der SHA-256-Hash gespeichert, nicht das Klartextpasswort. + +Verifikation: + +```text +dotnet build .\TrafagSalesExporter.csproj --no-restore -p:UseAppHost=false -p:OutDir=.\obj\verify_hrlogin\ --verbosity minimal +``` + +Ergebnis: + +- Build erfolgreich. +- 3 bestehende MudBlazor-Analyzer-Warnungen in `Logs.razor`, `Transformations.razor` und `Standorte.razor`. + +## Navigation in Finance/HR/Admin gegliedert 2026-05-15 + +Geaendert: + +- Linke Navigation neu gegliedert: + - `Finance Cockpit` + - `HR KPI (Login)` + - `Admin` +- Unter `Finance Cockpit` stehen: + - `Export Dashboard` + - `Management Analyse` + - `Soll/Ist Vergleich` +- Unter `Admin` stehen: + - `Standorte` + - `Transformationen` + - `Settings` + - `Logs` +- Seitentitel wurden an die neuen Menuebezeichnungen angepasst. + +Verifikation: + +```text +dotnet build .\TrafagSalesExporter.csproj --no-restore -p:UseAppHost=false -p:OutDir=.\obj\verify_nav_groups\ --verbosity minimal +``` + +Ergebnis: + +- Build erfolgreich. +- 3 bestehende MudBlazor-Analyzer-Warnungen in `Logs.razor`, `Transformations.razor` und `Standorte.razor`. + +## Management Cockpit zentrale Filterkopplung 2026-05-15 + +Geaendert: + +- Die untere `Zentrale Roh-Auswertung` im Management Cockpit ist nicht mehr nur global. +- Neue Filterfelder: `Landfilter` und `TSC`. +- Wenn oben eine Einzeldatei analysiert wird, uebernimmt die zentrale Auswertung automatisch Land und TSC aus dieser Datei. +- Beispiel: Auswahl `USA | TRUS | Sales_TRUS_2026-05-08.xlsx` setzt unten automatisch `USA / TRUS`. +- Button `Global` leert die Filter, falls wieder alle Laender/Standorte ausgewertet werden sollen. +- Jahres-, Monats-, Jahreswerte-, Monatswerte-, Tageswerte-, Quellen- und Laendertabellen verwenden denselben Land/TSC-Filter. + +Verifikation: + +```text +dotnet build .\TrafagSalesExporter.csproj --no-restore -p:UseAppHost=false -p:OutDir=.\obj\verify_management_scope2\ --verbosity minimal +``` + +Ergebnis: + +- Build erfolgreich. +- 3 bestehende MudBlazor-Analyzer-Warnungen in `Logs.razor`, `Transformations.razor` und `Standorte.razor`. + +## Finance Vergleich als eigener Reiter 2026-05-15 + +Geaendert: + +- `Net Sales Actuals 2025 Referenz` aus dem Start-Dashboard entfernt. +- Neue Seite `Finance Vergleich` unter `Finance Cockpit` angelegt. +- Route: `/finance-cockpit/vergleich` +- Die Seite zeigt den Soll/Ist-Vergleich gegen `check.xlsx` separat, inklusive IC-Abzug, Referenzwert, Summenfeld, Differenz, Waehrung, Zeilen und Status. +- `DashboardPageService` laedt die Finance-Referenzdaten nicht mehr automatisch mit dem operativen Dashboard. + +Verifikation: + +```text +dotnet build .\TrafagSalesExporter.csproj --no-restore -p:UseAppHost=false -p:OutDir=.\obj\verify_finance_compare_tab\ --verbosity minimal +``` + +Ergebnis: + +- Build erfolgreich. +- 3 bestehende MudBlazor-Analyzer-Warnungen in `Logs.razor`, `Transformations.razor` und `Standorte.razor`.