Add published HR KPI workflow fixes
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
internal static class AccessUnlockCookie
|
||||
{
|
||||
public const string FinanceCookieName = "TrafagFinanceUnlocked";
|
||||
public const string AdminCookieName = "TrafagAdminUnlocked";
|
||||
public const string HrCookieName = "TrafagHrUnlocked";
|
||||
|
||||
public static bool IsUnlocked(HttpContext? httpContext, string cookieName, string passwordHash)
|
||||
{
|
||||
if (httpContext is null ||
|
||||
string.IsNullOrWhiteSpace(passwordHash) ||
|
||||
!httpContext.Request.Cookies.TryGetValue(cookieName, out var value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(
|
||||
Encoding.UTF8.GetBytes(value),
|
||||
Encoding.UTF8.GetBytes(CreateValue(cookieName, passwordHash)));
|
||||
}
|
||||
|
||||
public static void SetUnlocked(HttpContext httpContext, string cookieName, string passwordHash)
|
||||
{
|
||||
httpContext.Response.Cookies.Append(cookieName, CreateValue(cookieName, passwordHash), new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
IsEssential = true,
|
||||
SameSite = SameSiteMode.Strict,
|
||||
Secure = httpContext.Request.IsHttps,
|
||||
Path = string.IsNullOrWhiteSpace(httpContext.Request.PathBase) ? "/" : httpContext.Request.PathBase.Value!,
|
||||
Expires = DateTimeOffset.UtcNow.AddHours(12)
|
||||
});
|
||||
}
|
||||
|
||||
private static string CreateValue(string cookieName, string passwordHash)
|
||||
{
|
||||
var input = $"TrafagSalesExporter|{cookieName}|{passwordHash.Trim()}";
|
||||
return AccessPasswordSettingsWriter.HashPassword(input);
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,19 @@ public sealed class AdminAccessService : IAdminAccessService
|
||||
{
|
||||
private readonly AdminAccessOptions _options;
|
||||
private readonly IHostEnvironment _environment;
|
||||
private readonly ILogger<AdminAccessService> _logger;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public AdminAccessService(IOptions<AdminAccessOptions> options, IHostEnvironment environment)
|
||||
public AdminAccessService(
|
||||
IOptions<AdminAccessOptions> options,
|
||||
IHostEnvironment environment,
|
||||
ILogger<AdminAccessService> logger,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_options = options.Value;
|
||||
_environment = environment;
|
||||
_logger = logger;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public bool IsEnabled => _options.Enabled;
|
||||
@@ -33,13 +41,21 @@ public sealed class AdminAccessService : IAdminAccessService
|
||||
!string.IsNullOrWhiteSpace(_options.Username) &&
|
||||
(!string.IsNullOrWhiteSpace(_options.PasswordHash) || !string.IsNullOrEmpty(_options.Password));
|
||||
|
||||
public bool IsUnlocked { get; private set; }
|
||||
public bool IsUnlocked =>
|
||||
_isUnlocked ||
|
||||
AccessUnlockCookie.IsUnlocked(
|
||||
_httpContextAccessor.HttpContext,
|
||||
AccessUnlockCookie.AdminCookieName,
|
||||
_options.PasswordHash);
|
||||
|
||||
private bool _isUnlocked;
|
||||
|
||||
public bool TryUnlock(string username, string password)
|
||||
{
|
||||
if (!IsEnabled)
|
||||
{
|
||||
IsUnlocked = true;
|
||||
_isUnlocked = true;
|
||||
_logger.LogInformation("Admin access unlocked because AdminAccess is disabled.");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -48,6 +64,12 @@ public sealed class AdminAccessService : IAdminAccessService
|
||||
string.IsNullOrEmpty(password) ||
|
||||
!FixedEquals(username.Trim(), _options.Username.Trim()))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Admin access unlock failed before password check. IsConfigured={IsConfigured}, HasUsername={HasUsername}, PasswordLength={PasswordLength}, UsernameMatches={UsernameMatches}",
|
||||
IsConfigured,
|
||||
!string.IsNullOrWhiteSpace(username),
|
||||
password?.Length ?? 0,
|
||||
!string.IsNullOrWhiteSpace(username) && FixedEquals(username.Trim(), _options.Username.Trim()));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -55,7 +77,14 @@ public sealed class AdminAccessService : IAdminAccessService
|
||||
? VerifyPasswordHash(password, _options.PasswordHash)
|
||||
: FixedEquals(password, _options.Password);
|
||||
|
||||
IsUnlocked = valid;
|
||||
_isUnlocked = valid;
|
||||
_logger.Log(
|
||||
valid ? LogLevel.Information : LogLevel.Warning,
|
||||
"Admin access password check completed. Success={Success}, Username={Username}, PasswordLength={PasswordLength}, UsesHash={UsesHash}",
|
||||
valid,
|
||||
username.Trim(),
|
||||
password.Length,
|
||||
!string.IsNullOrWhiteSpace(_options.PasswordHash));
|
||||
return valid;
|
||||
}
|
||||
|
||||
@@ -74,11 +103,11 @@ public sealed class AdminAccessService : IAdminAccessService
|
||||
AccessPasswordSettingsWriter.SavePasswordHash(_environment.ContentRootPath, AdminAccessOptions.SectionName, passwordHash);
|
||||
_options.PasswordHash = passwordHash;
|
||||
_options.Password = string.Empty;
|
||||
IsUnlocked = true;
|
||||
_isUnlocked = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Lock() => IsUnlocked = false;
|
||||
public void Lock() => _isUnlocked = false;
|
||||
|
||||
private static bool VerifyPasswordHash(string password, string configuredHash)
|
||||
{
|
||||
|
||||
@@ -21,18 +21,21 @@ public sealed class FinanceCockpitAccessService : IFinanceCockpitAccessService,
|
||||
private readonly IHostEnvironment _environment;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IAccessSessionTracker _sessionTracker;
|
||||
private readonly ILogger<FinanceCockpitAccessService> _logger;
|
||||
private readonly string _sessionId = Guid.NewGuid().ToString("N");
|
||||
|
||||
public FinanceCockpitAccessService(
|
||||
IOptions<FinanceCockpitAccessOptions> options,
|
||||
IHostEnvironment environment,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IAccessSessionTracker sessionTracker)
|
||||
IAccessSessionTracker sessionTracker,
|
||||
ILogger<FinanceCockpitAccessService> logger)
|
||||
{
|
||||
_options = options.Value;
|
||||
_environment = environment;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_sessionTracker = sessionTracker;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool IsEnabled => _options.Enabled;
|
||||
@@ -42,13 +45,21 @@ public sealed class FinanceCockpitAccessService : IFinanceCockpitAccessService,
|
||||
!string.IsNullOrWhiteSpace(_options.Username) &&
|
||||
(!string.IsNullOrWhiteSpace(_options.PasswordHash) || !string.IsNullOrEmpty(_options.Password));
|
||||
|
||||
public bool IsUnlocked { get; private set; }
|
||||
public bool IsUnlocked =>
|
||||
_isUnlocked ||
|
||||
AccessUnlockCookie.IsUnlocked(
|
||||
_httpContextAccessor.HttpContext,
|
||||
AccessUnlockCookie.FinanceCookieName,
|
||||
_options.PasswordHash);
|
||||
|
||||
private bool _isUnlocked;
|
||||
|
||||
public bool TryUnlock(string username, string password)
|
||||
{
|
||||
if (!IsEnabled)
|
||||
{
|
||||
IsUnlocked = true;
|
||||
_isUnlocked = true;
|
||||
_logger.LogInformation("Finance Cockpit access unlocked because FinanceCockpitAccess is disabled.");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -57,6 +68,12 @@ public sealed class FinanceCockpitAccessService : IFinanceCockpitAccessService,
|
||||
string.IsNullOrEmpty(password) ||
|
||||
!FixedEquals(username.Trim(), _options.Username.Trim()))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Finance Cockpit unlock failed before password check. IsConfigured={IsConfigured}, HasUsername={HasUsername}, PasswordLength={PasswordLength}, UsernameMatches={UsernameMatches}",
|
||||
IsConfigured,
|
||||
!string.IsNullOrWhiteSpace(username),
|
||||
password?.Length ?? 0,
|
||||
!string.IsNullOrWhiteSpace(username) && FixedEquals(username.Trim(), _options.Username.Trim()));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -64,7 +81,14 @@ public sealed class FinanceCockpitAccessService : IFinanceCockpitAccessService,
|
||||
? VerifyPasswordHash(password, _options.PasswordHash)
|
||||
: FixedEquals(password, _options.Password);
|
||||
|
||||
IsUnlocked = valid;
|
||||
_isUnlocked = valid;
|
||||
_logger.Log(
|
||||
valid ? LogLevel.Information : LogLevel.Warning,
|
||||
"Finance Cockpit password check completed. Success={Success}, Username={Username}, PasswordLength={PasswordLength}, UsesHash={UsesHash}",
|
||||
valid,
|
||||
username.Trim(),
|
||||
password.Length,
|
||||
!string.IsNullOrWhiteSpace(_options.PasswordHash));
|
||||
if (valid)
|
||||
_sessionTracker.Register(_sessionId, "Finance Cockpit", username.Trim(), GetRemoteAddress());
|
||||
return valid;
|
||||
@@ -72,7 +96,7 @@ public sealed class FinanceCockpitAccessService : IFinanceCockpitAccessService,
|
||||
|
||||
public void Lock()
|
||||
{
|
||||
IsUnlocked = false;
|
||||
_isUnlocked = false;
|
||||
_sessionTracker.Unregister(_sessionId);
|
||||
}
|
||||
|
||||
@@ -91,7 +115,7 @@ public sealed class FinanceCockpitAccessService : IFinanceCockpitAccessService,
|
||||
AccessPasswordSettingsWriter.SavePasswordHash(_environment.ContentRootPath, FinanceCockpitAccessOptions.SectionName, passwordHash);
|
||||
_options.PasswordHash = passwordHash;
|
||||
_options.Password = string.Empty;
|
||||
IsUnlocked = true;
|
||||
_isUnlocked = true;
|
||||
_sessionTracker.Register(_sessionId, "Finance Cockpit", username.Trim(), GetRemoteAddress());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ internal sealed class HrKpiDashboardBuilder
|
||||
|
||||
var turnoverEmployees = ApplyTurnoverEmployeeFilters(employees, normalizedOptions).ToList();
|
||||
var turnoverHeadcountLeavers = ApplyTurnoverHeadcountLeaverFilters(leavers, normalizedOptions).ToList();
|
||||
var analysisPeriod = ResolveAnalysisPeriod(normalizedOptions);
|
||||
var filteredEmployees = ApplyEmployeeFilters(employees, normalizedOptions).ToList();
|
||||
var filteredEmployeeNumbers = filteredEmployees
|
||||
.Where(x => x.Personalnummer.HasValue)
|
||||
@@ -97,6 +98,7 @@ internal sealed class HrKpiDashboardBuilder
|
||||
.ToHashSet();
|
||||
|
||||
employees = filteredEmployees;
|
||||
var absenceRowsWithoutDates = absences.Count(x => !x.VonDatum.HasValue && !x.BisDatum.HasValue);
|
||||
absences = ApplyAbsenceFilters(absences, normalizedOptions, filteredEmployeeNumbers).ToList();
|
||||
leavers = ApplyLeaverFilters(leavers, normalizedOptions).ToList();
|
||||
var turnoverPeriod = ResolveTurnoverPeriodScope(normalizedOptions, leavers);
|
||||
@@ -104,9 +106,9 @@ internal sealed class HrKpiDashboardBuilder
|
||||
result.Employees = employees;
|
||||
result.Absences = absences;
|
||||
result.Leavers = leavers;
|
||||
result.Metrics = BuildOverviewMetrics(employees, absences, turnoverEmployees, turnoverHeadcountLeavers, leavers, turnoverPeriod);
|
||||
result.Metrics = BuildOverviewMetrics(employees, absences, turnoverEmployees, turnoverHeadcountLeavers, leavers, turnoverPeriod, analysisPeriod);
|
||||
result.TurnoverMetrics = BuildTurnoverMetrics(turnoverEmployees, turnoverHeadcountLeavers, leavers, turnoverPeriod);
|
||||
result.AbsenceMetrics = BuildAbsenceMetrics(employees, absences);
|
||||
result.AbsenceMetrics = BuildAbsenceMetrics(employees, absences, analysisPeriod);
|
||||
result.TimeVacationMetrics = BuildTimeVacationMetrics(employees);
|
||||
result.PeriodComparisonMetrics = BuildPeriodComparisonMetrics(turnoverEmployees, turnoverHeadcountLeavers, leavers, turnoverPeriod);
|
||||
result.TrafficLights = BuildTrafficLights(result.Metrics, result.TurnoverMetrics, result.AbsenceMetrics, result.TimeVacationMetrics, context);
|
||||
@@ -158,6 +160,8 @@ internal sealed class HrKpiDashboardBuilder
|
||||
result.Notices.Add($"{missingFteCount:N0} aktive Mitarbeitendenzeilen ohne SAP-Beschaeftigungsgrad verwenden einen FTE-Fallback aus Rexx-Arbeitszeitmodell/Sollzeit.");
|
||||
if (HasEmployeeOnlyTurnoverFilters(normalizedOptions))
|
||||
result.Notices.Add("Kostenstelle, GLZ und Restferien filtern aktive Mitarbeitende und Absenzen, aber nicht die Fluktuation. Die Austrittsdatei enthaelt diese Felder nicht stabil genug fuer denselben Schnitt.");
|
||||
if (analysisPeriod.HasPeriod && absenceRowsWithoutDates > 0)
|
||||
result.Notices.Add("Rexx-Absenzen enthalten keine Datumsfelder. Der Zeitraumfilter setzt voraus, dass Abwesenheitinstunden.xlsx bereits fuer den gewaehlten Zeitraum exportiert wurde; die Absenzquote nutzt den gewaehlten Zeitraum als Nenner.");
|
||||
if (!context.HasFile(_dataSources.MainFile))
|
||||
result.Notices.Add($"Hauptdatei fehlt: {_dataSources.MainFile}. Ohne diese Datei sind keine HR-KPIs moeglich.");
|
||||
if (!context.HasFile(_dataSources.SapFile))
|
||||
@@ -299,6 +303,8 @@ internal sealed class HrKpiDashboardBuilder
|
||||
{
|
||||
return context.ReadRows(_dataSources.AbsenceFile, "Rexx #744 Absenzen", (row, headers) =>
|
||||
{
|
||||
var fromDate = ReadDate(row, headers, "Von Datum", "Von", "Beginn", "Startdatum", "Abwesenheit von", "Datum");
|
||||
var toDate = ReadDate(row, headers, "Bis Datum", "Bis", "Ende", "Enddatum", "Abwesenheit bis", "Datum");
|
||||
var kurz = ReadDecimal(row, headers, "Krankheit angetreten (Stunden Ind.)", "Krankheit_Kurz_Std");
|
||||
var lang = ReadDecimal(row, headers, "Krank nicht buchbar angetreten (Stunden Ind.)", "Krankheit_Lang_Std");
|
||||
var gesamt = kurz + lang;
|
||||
@@ -310,6 +316,8 @@ internal sealed class HrKpiDashboardBuilder
|
||||
Organisationseinheit = ReadString(row, headers, "Organisation"),
|
||||
Stelle = ReadString(row, headers, "Stelle"),
|
||||
Status = ReadString(row, headers, "Personal Status", "Status"),
|
||||
VonDatum = fromDate,
|
||||
BisDatum = toDate ?? fromDate,
|
||||
KrankheitKurzStd = kurz,
|
||||
KrankheitLangStd = lang,
|
||||
KrankheitGesamtStd = gesamt,
|
||||
@@ -406,6 +414,7 @@ internal sealed class HrKpiDashboardBuilder
|
||||
=> rows.Where(x => MatchesFilter(x.Organisationseinheit, options.Organisationseinheit) &&
|
||||
x.Personalnummer.HasValue &&
|
||||
filteredEmployeeNumbers.Contains(x.Personalnummer.Value) &&
|
||||
MatchesAbsencePeriodFilter(x, options) &&
|
||||
MatchesTextSearch(options.SearchText, x.Name, x.Personalnummer?.ToString(CultureInfo.InvariantCulture) ?? string.Empty));
|
||||
|
||||
private static IEnumerable<HrLeaverRow> ApplyLeaverFilters(IEnumerable<HrLeaverRow> rows, HrKpiOptions options)
|
||||
@@ -429,7 +438,8 @@ internal sealed class HrKpiDashboardBuilder
|
||||
IReadOnlyCollection<HrKpiEmployeeRow> turnoverEmployees,
|
||||
IReadOnlyCollection<HrLeaverRow> turnoverHeadcountLeavers,
|
||||
IReadOnlyCollection<HrLeaverRow> leavers,
|
||||
TurnoverPeriodScope period)
|
||||
TurnoverPeriodScope period,
|
||||
AnalysisPeriod analysisPeriod)
|
||||
{
|
||||
var activeCount = CountDistinctPersons(employees.Select(x => x.Personalnummer));
|
||||
var activeFixedCount = CountDistinctPersons(employees
|
||||
@@ -439,7 +449,8 @@ internal sealed class HrKpiDashboardBuilder
|
||||
var turnoverDenominator = ResolveTurnoverDenominator(turnoverEmployees, turnoverIntervals, period);
|
||||
var fte = employees.Sum(x => x.Fte);
|
||||
var sickDays = absences.Sum(x => x.KrankheitstageGesamt);
|
||||
var absenceRate = fte <= 0 ? 0 : sickDays / (fte * 21m);
|
||||
var absenceDenominator = fte * analysisPeriod.Workdays;
|
||||
var absenceRate = absenceDenominator <= 0 ? 0 : sickDays / absenceDenominator;
|
||||
var relevantLeavers = CountDistinctPersons(leavers.Where(x => x.IstFluktuationsrelevant).Select(x => x.Personalnummer));
|
||||
var employeeLeavers = CountDistinctPersons(leavers.Where(x => x.IstArbeitnehmerkuendigung).Select(x => x.Personalnummer));
|
||||
var turnover = turnoverDenominator == 0 ? 0 : relevantLeavers / turnoverDenominator;
|
||||
@@ -558,13 +569,15 @@ internal sealed class HrKpiDashboardBuilder
|
||||
|
||||
private static List<HrKpiMetric> BuildAbsenceMetrics(
|
||||
IReadOnlyCollection<HrKpiEmployeeRow> employees,
|
||||
IReadOnlyCollection<HrAbsenceRow> absences)
|
||||
IReadOnlyCollection<HrAbsenceRow> absences,
|
||||
AnalysisPeriod analysisPeriod)
|
||||
{
|
||||
var totalSick = absences.Sum(x => x.KrankheitstageGesamt);
|
||||
var shortSick = absences.Sum(x => x.KrankheitstageKurz);
|
||||
var longSick = absences.Sum(x => x.KrankheitstageLang);
|
||||
var fte = employees.Sum(x => x.Fte);
|
||||
var absenceRate = fte <= 0 ? 0 : totalSick / (fte * 21m);
|
||||
var denominator = fte * analysisPeriod.Workdays;
|
||||
var absenceRate = denominator <= 0 ? 0 : totalSick / denominator;
|
||||
var bu = employees.Sum(x => x.BuTage);
|
||||
var nbu = employees.Sum(x => x.NbuTage);
|
||||
|
||||
@@ -573,7 +586,7 @@ internal sealed class HrKpiDashboardBuilder
|
||||
new() { Label = "Krankheitstage Gesamt", Value = totalSick.ToString("N1"), Detail = $"{absences.Count:N0} aktive Absenzenzeilen", Severity = absenceRate > 0.05m ? "Warning" : "Normal" },
|
||||
new() { Label = "Krankheit Kurz", Value = shortSick.ToString("N1"), Detail = "Rexx kurz / 8.4h", Severity = "Normal" },
|
||||
new() { Label = "Krankheit Lang", Value = longSick.ToString("N1"), Detail = "Rexx lang / 8.4h", Severity = longSick > shortSick ? "Warning" : "Normal" },
|
||||
new() { Label = "Krankenquote", Value = absenceRate.ToString("P1"), Detail = "Krankheitstage / (FTE * 21 Tage)", Severity = absenceRate > 0.05m ? "Warning" : "Normal" },
|
||||
new() { Label = "Krankenquote", Value = absenceRate.ToString("P1"), Detail = $"Krankheitstage / (FTE * {analysisPeriod.Workdays:N0} Arbeitstage), {analysisPeriod.Label}", Severity = absenceRate > 0.05m ? "Warning" : "Normal" },
|
||||
new() { Label = "BU-Tage", Value = bu.ToString("N1"), Detail = "SAP HR KPI", Severity = "Normal" },
|
||||
new() { Label = "NBU-Tage", Value = nbu.ToString("N1"), Detail = "SAP HR KPI", Severity = "Normal" },
|
||||
new() { Label = "Unfalltage Total", Value = (bu + nbu).ToString("N1"), Detail = "BU + NBU", Severity = "Normal" }
|
||||
@@ -1052,6 +1065,23 @@ internal sealed class HrKpiDashboardBuilder
|
||||
(row.Austrittsjahr.HasValue && row.Austrittsjahr.Value == options.Year.Value);
|
||||
}
|
||||
|
||||
private static bool MatchesAbsencePeriodFilter(HrAbsenceRow row, HrKpiOptions options)
|
||||
{
|
||||
var period = ResolveEmploymentPeriod(options);
|
||||
if (!period.HasValue)
|
||||
return true;
|
||||
|
||||
if (!row.VonDatum.HasValue && !row.BisDatum.HasValue)
|
||||
return true;
|
||||
|
||||
var start = row.VonDatum?.Date ?? row.BisDatum!.Value.Date;
|
||||
var end = row.BisDatum?.Date ?? start;
|
||||
if (end < start)
|
||||
(start, end) = (end, start);
|
||||
|
||||
return start <= period.Value.End && end >= period.Value.Start;
|
||||
}
|
||||
|
||||
private static bool MatchesLeaverEmploymentPeriodFilter(HrLeaverRow row, HrKpiOptions options)
|
||||
{
|
||||
var period = ResolveEmploymentPeriod(options);
|
||||
@@ -1078,6 +1108,34 @@ internal sealed class HrKpiDashboardBuilder
|
||||
return start <= end ? (start, end) : (end, start);
|
||||
}
|
||||
|
||||
private static AnalysisPeriod ResolveAnalysisPeriod(HrKpiOptions options)
|
||||
{
|
||||
var period = ResolveEmploymentPeriod(options);
|
||||
if (!period.HasValue)
|
||||
{
|
||||
return new AnalysisPeriod(null, null, 21m, "ohne Zeitraumfilter", false);
|
||||
}
|
||||
|
||||
var workdays = CountWeekdays(period.Value.Start, period.Value.End);
|
||||
var label = $"{period.Value.Start:dd.MM.yyyy} - {period.Value.End:dd.MM.yyyy}";
|
||||
return new AnalysisPeriod(period.Value.Start, period.Value.End, Math.Max(1, workdays), label, true);
|
||||
}
|
||||
|
||||
private static int CountWeekdays(DateTime start, DateTime end)
|
||||
{
|
||||
if (end < start)
|
||||
(start, end) = (end, start);
|
||||
|
||||
var days = 0;
|
||||
for (var date = start.Date; date <= end.Date; date = date.AddDays(1))
|
||||
{
|
||||
if (date.DayOfWeek is not DayOfWeek.Saturday and not DayOfWeek.Sunday)
|
||||
days++;
|
||||
}
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
private static int CountDistinctPersons(IEnumerable<int?> personalNumbers)
|
||||
=> personalNumbers
|
||||
.Where(x => x.HasValue)
|
||||
@@ -1368,6 +1426,8 @@ internal sealed class HrKpiDashboardBuilder
|
||||
|
||||
private sealed record TurnoverPeriodScope(int? BreakdownYear, DateTime AnchorDate, string Label, bool ShowPeriodMetrics);
|
||||
|
||||
private sealed record AnalysisPeriod(DateTime? Start, DateTime? End, decimal Workdays, string Label, bool HasPeriod);
|
||||
|
||||
private sealed record TurnoverEmploymentInterval(int Personalnummer, DateTime? Eintrittsdatum, DateTime? Austrittsdatum);
|
||||
|
||||
private sealed record TimeRow(string NameKey, DateTime? Geburtsdatum, string Arbeitszeitmodell, decimal AvgSollzeitTag);
|
||||
|
||||
@@ -42,13 +42,20 @@ public sealed class HrKpiAccessService : IHrKpiAccessService, IDisposable
|
||||
!string.IsNullOrWhiteSpace(_options.Username) &&
|
||||
(!string.IsNullOrWhiteSpace(_options.PasswordHash) || !string.IsNullOrEmpty(_options.Password));
|
||||
|
||||
public bool IsUnlocked { get; private set; }
|
||||
public bool IsUnlocked =>
|
||||
_isUnlocked ||
|
||||
AccessUnlockCookie.IsUnlocked(
|
||||
_httpContextAccessor.HttpContext,
|
||||
AccessUnlockCookie.HrCookieName,
|
||||
_options.PasswordHash);
|
||||
|
||||
private bool _isUnlocked;
|
||||
|
||||
public bool TryUnlock(string username, string password)
|
||||
{
|
||||
if (!IsEnabled)
|
||||
{
|
||||
IsUnlocked = true;
|
||||
_isUnlocked = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -64,7 +71,7 @@ public sealed class HrKpiAccessService : IHrKpiAccessService, IDisposable
|
||||
? VerifyPasswordHash(password, _options.PasswordHash)
|
||||
: FixedEquals(password, _options.Password);
|
||||
|
||||
IsUnlocked = valid;
|
||||
_isUnlocked = valid;
|
||||
if (valid)
|
||||
_sessionTracker.Register(_sessionId, "HR KPI", username.Trim(), GetRemoteAddress());
|
||||
return valid;
|
||||
@@ -72,7 +79,7 @@ public sealed class HrKpiAccessService : IHrKpiAccessService, IDisposable
|
||||
|
||||
public void Lock()
|
||||
{
|
||||
IsUnlocked = false;
|
||||
_isUnlocked = false;
|
||||
_sessionTracker.Unregister(_sessionId);
|
||||
}
|
||||
|
||||
@@ -91,7 +98,7 @@ public sealed class HrKpiAccessService : IHrKpiAccessService, IDisposable
|
||||
AccessPasswordSettingsWriter.SavePasswordHash(_environment.ContentRootPath, HrKpiAccessOptions.SectionName, passwordHash);
|
||||
_options.PasswordHash = passwordHash;
|
||||
_options.Password = string.Empty;
|
||||
IsUnlocked = true;
|
||||
_isUnlocked = true;
|
||||
_sessionTracker.Register(_sessionId, "HR KPI", username.Trim(), GetRemoteAddress());
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user