From 06fb56075f638fac838d39e77aed82cfc01e2c77 Mon Sep 17 00:00:00 2001 From: metacube Date: Wed, 20 May 2026 15:27:03 +0200 Subject: [PATCH] Expand HR KPI cockpit and add user guides --- .../Components/HrKpi/HrKpiDashboardTabs.razor | 150 +++++++++++++- .../Components/Pages/HrKpi.razor | 20 +- TrafagSalesExporter/Models/HrKpiModels.cs | 28 +++ .../Services/HrKpi/HrKpiDashboardBuilder.cs | 193 +++++++++++++++++- ...E_COCKPIT_ANLEITUNG_FINANZ_2026-05-20.docx | Bin 0 -> 2075 bytes .../docs/HR_KPI_ANLEITUNG_HR_2026-05-20.docx | Bin 0 -> 2133 bytes 6 files changed, 384 insertions(+), 7 deletions(-) create mode 100644 TrafagSalesExporter/docs/FINANCE_COCKPIT_ANLEITUNG_FINANZ_2026-05-20.docx create mode 100644 TrafagSalesExporter/docs/HR_KPI_ANLEITUNG_HR_2026-05-20.docx diff --git a/TrafagSalesExporter/Components/HrKpi/HrKpiDashboardTabs.razor b/TrafagSalesExporter/Components/HrKpi/HrKpiDashboardTabs.razor index b0a8c6e..992508f 100644 --- a/TrafagSalesExporter/Components/HrKpi/HrKpiDashboardTabs.razor +++ b/TrafagSalesExporter/Components/HrKpi/HrKpiDashboardTabs.razor @@ -7,6 +7,15 @@ @MetricGrid(Result.Metrics) + + + @TrafficLightPanel(Result.TrafficLights) + + + @MetricGrid(Result.PeriodComparisonMetrics) + + + @HeadcountByOrganisationTable(Result.HeadcountByOrganisation) @@ -29,6 +38,15 @@ + + + @GroupValueTable(T("Austritte nach Austrittsart", "Leavers by exit type"), Result.LeaversByType, T("Austritte", "Leavers")) + + + @GroupValueTable(T("Austritte nach Organisation", "Leavers by organisation"), Result.LeaversByOrganisation, T("Austritte", "Leavers")) + + + @TurnoverGauge(Result.TurnoverVisuals) @@ -51,8 +69,21 @@ + + @TrafficLightPanel(Result.TrafficLights) + @MetricGrid(Result.PeriodComparisonMetrics) + + @MetricGrid(Result.AbsenceMetrics) + + + @GroupValueTable(T("Absenzen nach Organisation", "Absences by organisation"), Result.AbsenceByOrganisation, T("Krankheitstage", "Sick days")) + + + @TopAbsencesTable(Result.Absences) + + @T("Absenzen je Mitarbeiter", "Absences by employee") @@ -67,7 +98,7 @@ @context.Personalnummer - @context.Name + @DisplayPersonName(context.Name, context.Personalnummer, Result.Options.ManagementView) @context.Organisationseinheit @context.KrankheitstageKurz.ToString("N1") @context.KrankheitstageLang.ToString("N1") @@ -100,7 +131,7 @@ @T("Ampel", "Status") - @context.NameVoll + @DisplayPersonName(context.NameVoll, context.Personalnummer, Result.Options.ManagementView) @context.Organisationseinheit @context.UrlaubRest.ToString("N1") @context.FerienAusstehend.ToString("N1") @@ -122,6 +153,11 @@ @FileStatusTable(Result.FileStatuses) + + + @DataQualityTable(Result.DataQualityIssues) + + @@ -145,6 +181,19 @@ _ => Color.Success }; + private static Color MapQualityColor(string severity) + => severity switch + { + "Error" => Color.Error, + "Warning" => Color.Warning, + _ => Color.Info + }; + + private static string DisplayPersonName(string name, int? personalnummer, bool managementView) + => managementView + ? (personalnummer.HasValue ? $"Personalnr. {personalnummer.Value}" : "Person anonymisiert") + : name; + private static string FormatDate(DateTime? value) => value?.ToString("dd.MM.yyyy") ?? "-"; @@ -177,6 +226,93 @@ ; + private RenderFragment> TrafficLightPanel => items => @ + @T("HR-Ampel", "HR status") + + @foreach (var item in items) + { + + + + + @item.Status + + @item.Area + + @item.Value + @item.Detail + + + } + + ; + + private RenderFragment> DataQualityTable => items => @ + @T("Datenqualitaet", "Data quality") + + + @T("Schwere", "Severity") + @T("Bereich", "Area") + @T("Pruefpunkt", "Check") + @T("Anzahl", "Count") + @T("Hinweis", "Note") + + + + + @context.Severity + + + @context.Area + @context.Issue + @context.Count.ToString("N0") + @context.Detail + + + @T("Keine Datenqualitaetswarnungen.", "No data quality warnings.") + + + ; + + private RenderFragment<(string Title, IReadOnlyList Items, string ValueLabel)> GroupValueTableTuple => data => @ + @data.Title + + + @T("Gruppe", "Group") + @data.ValueLabel + % + + + @context.Label + @(context.Value != 0 ? context.Value.ToString("N1") : context.Count.ToString("N0")) + @context.Percent.ToString("N1") + + + ; + + private RenderFragment GroupValueTable(string title, IReadOnlyList items, string valueLabel) + => GroupValueTableTuple((title, items, valueLabel)); + + private RenderFragment> TopAbsencesTable => items => @ + @T("Hoechste Absenzen", "Highest absences") + + + @T("Name", "Name") + @T("Organisation", "Organisation") + @T("Kurz", "Short") + @T("Lang", "Long") + @T("Gesamt", "Total") + + + @DisplayPersonName(context.Name, context.Personalnummer, Result.Options.ManagementView) + @context.Organisationseinheit + @context.KrankheitstageKurz.ToString("N1") + @context.KrankheitstageLang.ToString("N1") + @context.KrankheitstageGesamt.ToString("N1") + + + ; + private RenderFragment> CriticalBalancesTable => items => @ @T("Kritische GLZ-Saldi", "Critical time balances") @@ -187,7 +323,7 @@ @T("Ampel", "Status") - @context.NameVoll + @DisplayPersonName(context.NameVoll, context.Personalnummer, Result.Options.ManagementView) @context.Organisationseinheit @context.StundenSaldo.ToString("N1") @@ -209,7 +345,7 @@ @T("Austrittsart", "Exit type") - @context.NameVoll + @DisplayPersonName(context.NameVoll, context.Personalnummer, Result.Options.ManagementView) @FormatDate(context.Austrittsdatum) @context.Organisationseinheit @context.Austrittsart @@ -246,7 +382,7 @@ @context.Personalnummer - @context.NameVoll + @DisplayPersonName(context.NameVoll, context.Personalnummer, Result.Options.ManagementView) @context.Organisationseinheit @context.KostenstelleText @context.Fte.ToString("N2") @@ -266,6 +402,8 @@ @T("Quelle", "Source") @T("Status", "Status") + @T("Stand", "Modified") + @T("Alter", "Age") @T("Zeilen", "Rows") @@ -278,6 +416,8 @@ @(context.Message ?? "-") + @FormatDate(context.LastModified) + @(context.AgeDays.HasValue ? $"{context.AgeDays:N0} Tage / {context.FreshnessStatus}" : "-") @context.RowCount.ToString("N0") diff --git a/TrafagSalesExporter/Components/Pages/HrKpi.razor b/TrafagSalesExporter/Components/Pages/HrKpi.razor index 4a212f0..500de21 100644 --- a/TrafagSalesExporter/Components/Pages/HrKpi.razor +++ b/TrafagSalesExporter/Components/Pages/HrKpi.razor @@ -7,6 +7,7 @@ @inject IHrKpiAccessService HrKpiAccess @inject ISnackbar Snackbar @inject IUiTextService UiText +@inject IJSRuntime JsRuntime @T("HR KPI", "HR KPI") @@ -65,6 +66,10 @@ else @(_loading ? T("Lade...", "Loading...") : T("Laden", "Load")) + + + @@ -128,6 +133,12 @@ else @T("Sperren", "Lock") + + + @T("Drucken/PDF", "Print/PDF") + + } @@ -161,6 +172,7 @@ else private string? _glzAmpel; private string? _restferienAmpel; private string? _searchText; + private bool _managementView; private string? _hrUsername; private string? _hrPassword; private bool _loading; @@ -207,7 +219,8 @@ else FluktuationFilter = _fluktuationFilter, GlzAmpel = _glzAmpel, RestferienAmpel = _restferienAmpel, - SearchText = _searchText + SearchText = _searchText, + ManagementView = _managementView }); } catch (Exception ex) @@ -239,6 +252,11 @@ else _hrPassword = string.Empty; } + private async Task PrintAsync() + { + await JsRuntime.InvokeVoidAsync("print"); + } + private bool CanShowHrKpi => !HrKpiAccess.IsEnabled || HrKpiAccess.IsUnlocked; private string T(string german, string english) => UiText.Text(german, english); diff --git a/TrafagSalesExporter/Models/HrKpiModels.cs b/TrafagSalesExporter/Models/HrKpiModels.cs index 2e689da..8a7ae19 100644 --- a/TrafagSalesExporter/Models/HrKpiModels.cs +++ b/TrafagSalesExporter/Models/HrKpiModels.cs @@ -14,6 +14,7 @@ public sealed class HrKpiOptions public string? GlzAmpel { get; set; } public string? RestferienAmpel { get; set; } public string? SearchText { get; set; } + public bool ManagementView { get; set; } } public sealed class HrKpiDataSourceOptions @@ -57,6 +58,13 @@ public sealed class HrKpiResult public List TurnoverMetrics { get; set; } = []; public List AbsenceMetrics { get; set; } = []; public List TimeVacationMetrics { get; set; } = []; + public List PeriodComparisonMetrics { get; set; } = []; + public List TrafficLights { get; set; } = []; + public List DataQualityIssues { get; set; } = []; + public List LeaversByType { get; set; } = []; + public List LeaversByOrganisation { get; set; } = []; + public List AbsenceByOrganisation { get; set; } = []; + public List CriticalAbsences { get; set; } = []; public List Employees { get; set; } = []; public List Absences { get; set; } = []; public List Leavers { get; set; } = []; @@ -73,6 +81,26 @@ public sealed class HrKpiFileStatus public bool Exists { get; set; } public int RowCount { get; set; } public string? Message { get; set; } + public DateTime? LastModified { get; set; } + public int? AgeDays { get; set; } + public string FreshnessStatus { get; set; } = "Unbekannt"; +} + +public sealed class HrKpiTrafficLight +{ + public string Area { get; set; } = string.Empty; + public string Status { get; set; } = "Gruen"; + public string Value { get; set; } = string.Empty; + public string Detail { get; set; } = string.Empty; +} + +public sealed class HrKpiDataQualityIssue +{ + public string Severity { get; set; } = "Info"; + public string Area { get; set; } = string.Empty; + public string Issue { get; set; } = string.Empty; + public int Count { get; set; } + public string Detail { get; set; } = string.Empty; } public sealed class HrKpiMetric diff --git a/TrafagSalesExporter/Services/HrKpi/HrKpiDashboardBuilder.cs b/TrafagSalesExporter/Services/HrKpi/HrKpiDashboardBuilder.cs index 39c3537..7a21ebb 100644 --- a/TrafagSalesExporter/Services/HrKpi/HrKpiDashboardBuilder.cs +++ b/TrafagSalesExporter/Services/HrKpi/HrKpiDashboardBuilder.cs @@ -38,7 +38,8 @@ internal sealed class HrKpiDashboardBuilder FluktuationFilter = NormalizeFilter(options.FluktuationFilter), GlzAmpel = NormalizeFilter(options.GlzAmpel), RestferienAmpel = NormalizeFilter(options.RestferienAmpel), - SearchText = NormalizeFilter(options.SearchText) + SearchText = NormalizeFilter(options.SearchText), + ManagementView = options.ManagementView }; var result = new HrKpiResult { Options = normalizedOptions }; @@ -107,6 +108,23 @@ internal sealed class HrKpiDashboardBuilder result.TurnoverMetrics = BuildTurnoverMetrics(turnoverEmployees, turnoverHeadcountLeavers, leavers, turnoverPeriod); result.AbsenceMetrics = BuildAbsenceMetrics(employees, absences); result.TimeVacationMetrics = BuildTimeVacationMetrics(employees); + result.PeriodComparisonMetrics = BuildPeriodComparisonMetrics(turnoverEmployees, turnoverHeadcountLeavers, leavers, turnoverPeriod); + result.TrafficLights = BuildTrafficLights(result.Metrics, result.TurnoverMetrics, result.AbsenceMetrics, result.TimeVacationMetrics, context); + result.DataQualityIssues = BuildDataQualityIssues(employees, absences, leavers, sapRows, context); + result.LeaversByType = BuildLeaverTypeGroups(leavers); + result.LeaversByOrganisation = BuildLeaverOrganisationGroups(leavers); + result.AbsenceByOrganisation = BuildAbsenceOrganisationGroups(absences); + result.CriticalAbsences = absences + .Where(x => x.KrankheitstageGesamt > 0) + .OrderByDescending(x => x.KrankheitstageGesamt) + .Select(absence => employees.FirstOrDefault(employee => employee.Personalnummer == absence.Personalnummer) ?? new HrKpiEmployeeRow + { + Personalnummer = absence.Personalnummer, + NameVoll = absence.Name, + Organisationseinheit = absence.Organisationseinheit + }) + .Take(25) + .ToList(); result.TurnoverVisuals = BuildTurnoverVisuals(turnoverEmployees, turnoverHeadcountLeavers, leavers, turnoverPeriod); result.HeadcountByOrganisation = employees .GroupBy(x => BlankAsUnknown(x.Organisationseinheit), StringComparer.OrdinalIgnoreCase) @@ -587,6 +605,171 @@ internal sealed class HrKpiDashboardBuilder ]; } + private static List BuildPeriodComparisonMetrics( + IReadOnlyCollection employees, + IReadOnlyCollection turnoverHeadcountLeavers, + IReadOnlyCollection leavers, + TurnoverPeriodScope period) + { + var selectedYear = period.BreakdownYear ?? leavers + .Where(x => x.Austrittsjahr.HasValue) + .Select(x => x.Austrittsjahr!.Value) + .DefaultIfEmpty(DateTime.Today.Year) + .Max(); + var previousYear = selectedYear - 1; + var intervals = BuildTurnoverIntervals(employees, turnoverHeadcountLeavers); + var selectedHeadcount = CalculateAverageFixedHeadcount(intervals, Enumerable.Range(1, 12).Select(month => (selectedYear, month))); + var previousHeadcount = CalculateAverageFixedHeadcount(intervals, Enumerable.Range(1, 12).Select(month => (previousYear, month))); + var selectedLeavers = CountDistinctPersons(leavers + .Where(x => x.IstFluktuationsrelevant && x.Austrittsjahr == selectedYear) + .Select(x => x.Personalnummer)); + var previousLeavers = CountDistinctPersons(leavers + .Where(x => x.IstFluktuationsrelevant && x.Austrittsjahr == previousYear) + .Select(x => x.Personalnummer)); + var selectedRate = selectedHeadcount == 0 ? 0 : selectedLeavers / selectedHeadcount; + var previousRate = previousHeadcount == 0 ? 0 : previousLeavers / previousHeadcount; + var deltaRate = selectedRate - previousRate; + var selectedAbs = leavers.Count(x => x.Austrittsjahr == selectedYear); + var previousAbs = leavers.Count(x => x.Austrittsjahr == previousYear); + + return + [ + new() { Label = $"Headcount {selectedYear}", Value = FormatHeadcount(selectedHeadcount), Detail = $"Vorjahr {FormatHeadcount(previousHeadcount)}", Severity = "Normal" }, + new() { Label = $"Austritte {selectedYear}", Value = selectedAbs.ToString("N0"), Detail = $"Vorjahr {previousAbs:N0}", Severity = selectedAbs > previousAbs ? "Warning" : "Normal" }, + new() { Label = $"Fluktuation {selectedYear}", Value = selectedRate.ToString("P1"), Detail = $"Vorjahr {previousRate:P1}", Severity = selectedRate > 0.12m ? "Warning" : "Normal" }, + new() { Label = "Delta Fluktuation", Value = deltaRate.ToString("+0.0%;-0.0%;0.0%"), Detail = $"{selectedYear} gegen {previousYear}", Severity = deltaRate > 0.02m ? "Warning" : "Normal" } + ]; + } + + private static List BuildTrafficLights( + IReadOnlyList overviewMetrics, + IReadOnlyList turnoverMetrics, + IReadOnlyList absenceMetrics, + IReadOnlyList timeVacationMetrics, + ImportContext context) + { + var turnover = FindMetric(turnoverMetrics, "Fluktuation Jahr Effektiv %") ?? FindMetric(overviewMetrics, "Fluktuation"); + var absence = FindMetric(absenceMetrics, "Krankenquote"); + var glzRed = FindMetric(timeVacationMetrics, "GLZ Rot"); + var vacationRed = FindMetric(timeVacationMetrics, "Restferien Rot"); + var missingFiles = context.FileStatuses.Count(x => !x.Exists); + + return + [ + BuildTrafficLight("Fluktuation", turnover?.Value ?? "-", turnover?.Detail ?? string.Empty, turnover?.Severity == "Warning"), + BuildTrafficLight("Krankenquote", absence?.Value ?? "-", absence?.Detail ?? string.Empty, absence?.Severity == "Warning"), + BuildTrafficLight("GLZ-Saldi", glzRed?.Value ?? "0", glzRed?.Detail ?? string.Empty, ParseInt(glzRed?.Value) > 0), + BuildTrafficLight("Restferien", vacationRed?.Value ?? "0", vacationRed?.Detail ?? string.Empty, ParseInt(vacationRed?.Value) > 0), + new() + { + Area = "Datenqualitaet", + Status = missingFiles == 0 ? "Gruen" : "Rot", + Value = missingFiles.ToString("N0"), + Detail = missingFiles == 0 ? "Alle erwarteten Dateien gefunden" : "Erwartete Dateien fehlen" + } + ]; + } + + private static List BuildDataQualityIssues( + IReadOnlyCollection employees, + IReadOnlyCollection absences, + IReadOnlyCollection leavers, + IReadOnlyDictionary sapRows, + ImportContext context) + { + var employeeNumbers = employees + .Where(x => x.Personalnummer.HasValue) + .Select(x => x.Personalnummer!.Value) + .ToHashSet(); + var duplicateEmployeeNumbers = employees + .Where(x => x.Personalnummer.HasValue) + .GroupBy(x => x.Personalnummer!.Value) + .Count(g => g.Count() > 1); + var sapNumbers = sapRows.Keys + .Select(key => int.TryParse(key, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsed) ? parsed : (int?)null) + .Where(x => x.HasValue) + .Select(x => x!.Value) + .ToHashSet(); + + return new[] + { + CreateQualityIssue("Error", "Dateien", "Fehlende Dateien", context.FileStatuses.Count(x => !x.Exists), "Erwartete HR-KPI-Datei wurde im Datenordner nicht gefunden."), + CreateQualityIssue("Warning", "Mitarbeitende", "Fehlende Personalnummer", employees.Count(x => !x.Personalnummer.HasValue), "Diese Zeilen zaehlen nicht in Distinct-Headcount-Kennzahlen."), + CreateQualityIssue("Warning", "Mitarbeitende", "Doppelte Personalnummer", duplicateEmployeeNumbers, "Mehrere aktive Zeilen mit gleicher Personalnummer."), + CreateQualityIssue("Warning", "Rexx/SAP", "Rexx ohne SAP", employeeNumbers.Count(number => !sapNumbers.Contains(number)), "Aktive Mitarbeitende ohne passende SAP-Zusatzzeile."), + CreateQualityIssue("Info", "Rexx/SAP", "SAP ohne Rexx", sapNumbers.Count(number => !employeeNumbers.Contains(number)), "SAP-Zeile ohne aktive Rexx-Mitarbeiterzeile."), + CreateQualityIssue("Warning", "Mitarbeitende", "Fehlende Organisation", employees.Count(x => string.IsNullOrWhiteSpace(x.Organisationseinheit)), "Organisationseinheit fehlt."), + CreateQualityIssue("Warning", "Mitarbeitende", "Fehlende Kostenstelle", employees.Count(x => string.IsNullOrWhiteSpace(x.KostenstelleText)), "Kostenstelle fehlt."), + CreateQualityIssue("Warning", "Mitarbeitende", "Fehlender Beschaeftigungsgrad", employees.Count(x => !x.BeschaeftigungsgradProzent.HasValue), "FTE verwendet Rexx-Fallback."), + CreateQualityIssue("Info", "Absenzen", "Absenzen ohne aktive Person", absences.Count(x => x.Personalnummer.HasValue && !employeeNumbers.Contains(x.Personalnummer.Value)), "Absenzzeile passt nicht auf aktuell aktive Mitarbeitendenfilter."), + CreateQualityIssue("Info", "Austritte", "Austritte ohne Personalnummer", leavers.Count(x => !x.Personalnummer.HasValue), "Austritt kann nicht eindeutig per Personalnummer gruppiert werden.") + }.Where(x => x.Count > 0).ToList(); + } + + private static List BuildLeaverTypeGroups(IReadOnlyCollection leavers) + => leavers + .GroupBy(x => BlankAsUnknown(string.IsNullOrWhiteSpace(x.AustrittsartNormalisiert) ? x.Austrittsart : x.AustrittsartNormalisiert), StringComparer.OrdinalIgnoreCase) + .Select(g => new HrKpiGroupValue { Label = g.Key, Count = CountDistinctPersons(g.Select(x => x.Personalnummer)), Value = CountDistinctPersons(g.Select(x => x.Personalnummer)) }) + .OrderByDescending(x => x.Count) + .ThenBy(x => x.Label, StringComparer.OrdinalIgnoreCase) + .ToList(); + + private static List BuildLeaverOrganisationGroups(IReadOnlyCollection leavers) + => leavers + .GroupBy(x => BlankAsUnknown(x.Organisationseinheit), StringComparer.OrdinalIgnoreCase) + .Select(g => new HrKpiGroupValue { Label = g.Key, Count = CountDistinctPersons(g.Select(x => x.Personalnummer)), Value = CountDistinctPersons(g.Select(x => x.Personalnummer)) }) + .OrderByDescending(x => x.Count) + .ThenBy(x => x.Label, StringComparer.OrdinalIgnoreCase) + .ToList(); + + private static List BuildAbsenceOrganisationGroups(IReadOnlyCollection absences) + { + var total = absences.Sum(x => x.KrankheitstageGesamt); + return absences + .GroupBy(x => BlankAsUnknown(x.Organisationseinheit), StringComparer.OrdinalIgnoreCase) + .Select(g => + { + var value = g.Sum(x => x.KrankheitstageGesamt); + return new HrKpiGroupValue + { + Label = g.Key, + Count = g.Count(), + Value = value, + Percent = total == 0 ? 0 : value / total * 100m + }; + }) + .OrderByDescending(x => x.Value) + .ThenBy(x => x.Label, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + private static HrKpiMetric? FindMetric(IEnumerable metrics, string labelPart) + => metrics.FirstOrDefault(x => x.Label.Contains(labelPart, StringComparison.OrdinalIgnoreCase)); + + private static HrKpiTrafficLight BuildTrafficLight(string area, string value, string detail, bool warning) + => new() + { + Area = area, + Status = warning ? "Gelb" : "Gruen", + Value = value, + Detail = detail + }; + + private static HrKpiDataQualityIssue CreateQualityIssue(string severity, string area, string issue, int count, string detail) + => new() + { + Severity = severity, + Area = area, + Issue = issue, + Count = count, + Detail = detail + }; + + private static int ParseInt(string? value) + => int.TryParse((value ?? string.Empty).Replace("'", string.Empty), NumberStyles.Any, CultureInfo.CurrentCulture, out var parsed) + ? parsed + : 0; + private static HrTurnoverVisuals BuildTurnoverVisuals( IReadOnlyCollection employees, IReadOnlyCollection turnoverHeadcountLeavers, @@ -1219,6 +1402,8 @@ internal sealed class HrKpiDashboardBuilder public bool HasFile(string fileName) => File.Exists(BuildPath(fileName)); + public IReadOnlyList FileStatuses => _result.FileStatuses; + public List ReadRows(string fileName, string label, Func, T> map) { var path = BuildPath(fileName); @@ -1228,6 +1413,12 @@ internal sealed class HrKpiDashboardBuilder Path = path, Exists = File.Exists(path) }; + if (status.Exists) + { + status.LastModified = File.GetLastWriteTime(path); + status.AgeDays = Math.Max(0, (DateTime.Today - status.LastModified.Value.Date).Days); + status.FreshnessStatus = status.AgeDays <= 7 ? "Aktuell" : status.AgeDays <= 31 ? "Aelter" : "Alt"; + } _result.FileStatuses.Add(status); if (!status.Exists) diff --git a/TrafagSalesExporter/docs/FINANCE_COCKPIT_ANLEITUNG_FINANZ_2026-05-20.docx b/TrafagSalesExporter/docs/FINANCE_COCKPIT_ANLEITUNG_FINANZ_2026-05-20.docx new file mode 100644 index 0000000000000000000000000000000000000000..f6b076edf776536e57708e07d20ad02cf3d92995 GIT binary patch literal 2075 zcmZ`)c{CL28y<}%OlGVjdpG;oT}%^&(uBbfVysz`WsI#sU0E{Rgk)q1m5VVLSsF36 zo3TWeh!$j<5pGk+HOP`X{dMo{_q*SB&i9=6obP$hd7t;3=RsQoxy1kgz+nLDI@LWk zRR8TVr>fxs07NqK=#FhAFGDXB|y@~fCIwt z9I)gBvAfDB_Wi4GCbn+ZObN_EH}=l!nUnRUgND33O<2C9q|DwF%g&{p##lfn&rLHZ zPz?_03R-@fdRC*#ur7&~v!SZQDlm?a7A2&m>t5Gj4-!#iBlz7mR?GEDUsW-y3ld<- zodBtDR$9!&+pS8Z@^gg;w%tnBT%-j`uR}OZaPgv5r0tdJ^}hKD<&H}3tc#)&3#$oF zR@#M#HOIar(%MRDHNJ{JxvTuKruM~Vd?Cz)3CGw#_Zhhnrf}crV+}_THI6WX9Q2Ct4~f*)`u=9TJSjxi0rAdNpPdQB@Ft_jv<_Fo)cNW} zjB2jPS zS^O#FT-!(``W!R-XCySv98hjiU-zb|&TD7{1CYM{JZgQ0p zg@Gff?K3m;wyWL&wN2Bgy`A|3)6Tk;P?H1Pt*YtyVZzO&(TfOz<4^AVnh~HtsVgOm zLYETtkg_XU(Vx@(9AY3&63+ZeIKpoMH%f{}s;mjfX#!4MLZW|N%05D^7(5k0FVq9) zDd&mikERvT*L2MqBde0Ps*jWgA;j@Baq*x+U^Y0DEL#K}JHk!>%7XBJs0mM=vv}Y( zsDwEHz19NdAgc*^U>#wMzItP^aBSEk3kM47+mZN^y5bC${)JK3#Wmnk;Q$n2FWwUi zYBGvG)2B=PM6%u6sekyc`KV%th=5FSfgQg-RqeyZe&kwqxTdR(SZj&Ej5h|@JU$g; zBW1{Zf7+iJv1(HuMvko1)gclm6lue{^(PfvmY8<-XbG*#W#FT&q?jHDJtE$ma`>LP za>~N+BK*y(I6$@$=`ewB$zfr#4Va~^LC^P=x^Bwb>+674OrLBNxkvP^EQ2mvQh>yl zM}z}>(7}40{obm*PXN>vNO>8Cww&M{1{o&Ql5BbGb*Qf>M)`gy_yozj{>=Dc#g-B9Ipu9X?DqL0YhyOXQz$!V{udWm7fEr} z+m_y?C{1pFl#3v?&m!+-CMJ$&2mwC(oXb2AuRD-@X@!PEJAjwnBN;{wM(Xh4qJBNv zBf}ngv&ab&mkp=`l2%~hFtM(vO;smr(L5Y8LU;mk#*akUO?JW-pP~U&K{YL2`}K!l{Vt2f3~kvz5RRVf(B`K z)-u{}PJ6Bna4nCt_+vYi)}c-Ni|$ZQRW(l;MFam^IkWe{Qm7Kw%mD{`X9d?yRS(z8 zLyqKJKHBc6iok*-u}vSCq!&Fm=wqcnWXGg=y^*AD=*Ya5n+w@s733ApyY8JfZYTKO ztY;jWktv-dJeevV%hMS6nOnm5xQ0#TC71F>%*USO+m{Kwjy=%{aYGq_g8MaGG61oz)sk9*(jE>WpM`I3U;(C z_)r!jpy)TAt1k3>xf`0kxJ$E25eISBqHPtz2{02=iBxBDpDGN`*PHM2q5eTWdwqy5 zeT{En;wP$jki3g3)dOzvhgFbTTxinXHF3v>8DEU+KixuIT0uzHWVKc#r)O74o-dIJ ziF)o7Kxx)rAJ4sUUVN!Yd;QDPH?2~^Y7zHUdmSo{k!@nhyObFWOuGW{R181ssuBYi z9Y7XLvnDbP5(6Ljs>eK=l1l07)-LHs3LZ-F+=!0;eM9K@j>pJq*UN{ekOrAw@6}O1 z+E}u7cTH||zi3-y4b*?>Ca@dn6q=jw`|aC>zXKU;uOli40JJsNAu+&zrzyu_|8f3L vw*J`Q$JF_oeUy_!|Hz;pi9Z_q-^5i8@qcU{ZOy~`{R@zDX&glHeXsrkE}Na> literal 0 HcmV?d00001 diff --git a/TrafagSalesExporter/docs/HR_KPI_ANLEITUNG_HR_2026-05-20.docx b/TrafagSalesExporter/docs/HR_KPI_ANLEITUNG_HR_2026-05-20.docx new file mode 100644 index 0000000000000000000000000000000000000000..428400e331da473d381f6ef0b85c55b7f4c8080e GIT binary patch literal 2133 zcmZ{lc{CJiAIFEW6C*Pwkx0fewwo+bBiUk_;fj>4hS4Ci7#cKXChIkKlI^0`P>#J} zkYt-~wn(`t(b%_1iIknVlRN!&@9llx=Q-#3p5Hmo_c`bH{hjlC+>k&qFaQ9M0-T{f z^o=2y^5%q9l?VW!BvcXih)+j)CY9mET-&J1M3`9>DjI2;El7qq`jE%mZxB9H*+fC#orr7oqA>I-RZf}gFD&YE( zHG3_pg4JExiyCx*KnFSIBN{DxVI$-+r3|El6ZdeCW>!XqKJacWvB7V&2ApD(DA{5L z;6nVo=^wc(E^0alFPMjcO-kP>u$+!%w4JwfHFuSO%9Z*`{BhzrJalIC!PxRrNb3kT ze=o?&Q)B_^x;sSJ2Xl7PAtr@whtI&%gXI2`K~I)tu%lX@87!5)XG8O#|L)4gotYIZ z4gjnu004@@uA+!!tTC1tcoi?4`S)Shd-)MZEkS?66AJJzoT;|1!S0QyesBoKk)`ZF zI%$i9rv(fvrD)+X-IwnkJ_U0ofUM|2+z(J4q_f!+;L=j+&3*` zbj+;|e%$ixtmNzS-QiZux7Z>Jr|QweZ$i$dljS6`P$M1XksS2QP^q@(7&;Oq^M#Dp z|4h2VXdHyGZ=rjK-OQPHY*Ms- zY}Ix<>>OuWUQfMtxIbeA#F?OR)r}i_#m=QE7N08~NXp3oU-9TlZPHqI?I-oyeEX*YRczSz#94|Qq!`%RU7&t*$|KRh|NDgt*G`d zRZ+)n@(g_}-L-YKvJwwrM`t5)-!hOO)i5;Dwxi{MR?IQX#iKRL2r04sGlldg#%l;F z>5S39r7n0eHZiwlkIdJ#Pa=?;M2*qv*Ed{#-oun4P>jmzkgl<%dVQYNY6zI_o2zH( zxH+Adhi+ZQC9Rc^x^K)fvme0e`OZ=L#X4JQDJ-PNbz8ih43;FFXZBWQnk9ING!5Y0 zOPfZSox*4uVW_qzcy6a418I(*L<6K#;!dhGaA>dmbw5jy1O3$39k97$B~vNMhs6-#f|y%e7m=99Oyez_I6a=^IANLArJ_92v;jKrBK?A@iue@NPK+;uHFAjnC-y(T~fHhGQo2v)~cA=f3A zHWSa>yaP8hT$9;H0GOFa)(2ii#_Fr;>#rJJH~rF?fl=!%5E+r>XcWh~)cboxi9z`7 z3Q4aJ_1<3XGLwR4gy_lZqbpTwxoXLcOC>pCQFI99URDgGyQwXH7h*cFpi#M;G zHWxXMp#bsh<2Y_bzLWFePiIqpp3*6DEzgYgo`ca|=%d6tpN!SUhxOIuPiB%Te)H98 zG+C)Hr*9%4{dUI#hyt@RwP5N6Lnd^x;V%ElM6CSRT;#;2-6Q*R0x0TreW7}1|@$p4>>oRr=HRa4&)B#UP+VYa~fyo=3g92GMq=+aO z@c%h1{G0!={s)zReBei3{q0r}66_!J`qB6!68<)R5gPvs5N=3uiSJi{!rdg)B+2j9 EUlVDy4FCWD literal 0 HcmV?d00001