Refine cockpit navigation and HR access

This commit is contained in:
2026-05-15 11:14:46 +02:00
parent e20693243d
commit 83e556e89e
13 changed files with 556 additions and 198 deletions
@@ -12,14 +12,10 @@ public interface IDashboardPageService
public sealed class DashboardPageService : IDashboardPageService
{
private readonly IDbContextFactory<AppDbContext> _dbFactory;
private readonly IFinanceReconciliationService _financeReconciliationService;
public DashboardPageService(
IDbContextFactory<AppDbContext> dbFactory,
IFinanceReconciliationService financeReconciliationService)
public DashboardPageService(IDbContextFactory<AppDbContext> dbFactory)
{
_dbFactory = dbFactory;
_financeReconciliationService = financeReconciliationService;
}
public async Task<DashboardPageState> 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<DashboardRow> DashboardRows { get; set; } = [];
public List<ConsolidatedDashboardRow> ConsolidatedRows { get; set; } = [];
public List<NetSalesReferenceRow> NetSalesReferenceRows { get; set; } = [];
}
public sealed class DashboardRow
@@ -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<HrKpiAccessOptions> 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);
}
}
@@ -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<CentralAggregationRow> ApplyCentralDimensionFilters(
IEnumerable<CentralAggregationRow> 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<string> GetCandidateDirectories(ExportSettings settings)
{
yield return Path.Combine(AppContext.BaseDirectory, "output");
@@ -456,7 +473,10 @@ public class ManagementCockpitService : IManagementCockpitService
};
}
private static List<string> BuildCentralNotices(AggregationSelection aggregation, int missingExchangeRateCount)
private static List<string> BuildCentralNotices(
AggregationSelection aggregation,
int missingExchangeRateCount,
ManagementCockpitAnalysisOptions? options)
{
var notices = new List<string>
{
@@ -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<CentralAggregationRow> groupRows,
AggregationSelection aggregation,