@using Microsoft.AspNetCore.Components @using TrafagSalesExporter.Models @using TrafagSalesExporter.Services @inject IUiTextService UiText @MetricGrid(Result.Metrics) @TrafficLightPanel(Result.TrafficLights) @MetricGrid(Result.PeriodComparisonMetrics) @HeadcountByOrganisationTable(Result.HeadcountByOrganisation) @CriticalBalancesTable(Result.CriticalTimeBalances) @MetricGrid(Result.TurnoverMetrics) @TurnoverRelevantTable(Result.FluctuationRelevantLeavers) @LeaverExclusionTable(Result.Leavers) @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) @TurnoverFunnel(Result.TurnoverVisuals.FunnelSteps) @TurnoverDonut(Result.TurnoverVisuals.ExclusionReasons) @HorizontalBars(Result.TurnoverVisuals.RelevantByOrganisation) @MonthlyBars(Result.TurnoverVisuals) @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") @T("Personalnr.", "Personnel no.") @T("Name", "Name") @T("Organisation", "Organisation") @T("Kurz", "Short") @T("Lang", "Long") @T("Gesamt", "Total") @T("Quote", "Rate") @context.Personalnummer @DisplayPersonName(context.Name, context.Personalnummer, Result.Options.ManagementView) @context.Organisationseinheit @context.KrankheitstageKurz.ToString("N1") @context.KrankheitstageLang.ToString("N1") @context.KrankheitstageGesamt.ToString("N1") @context.KrankenquoteMa.ToString("P1") @MetricGrid(Result.TimeVacationMetrics) @CriticalBalancesTable(Result.CriticalTimeBalances) @T("Kritische Restferien", "Critical vacation balance") @T("Name", "Name") @T("Organisation", "Organisation") @T("Rest", "Left") @T("Ausstehend", "Open") @T("Ampel", "Status") @DisplayPersonName(context.NameVoll, context.Personalnummer, Result.Options.ManagementView) @context.Organisationseinheit @context.UrlaubRest.ToString("N1") @context.FerienAusstehend.ToString("N1") @context.RestferienAmpel @EmployeesTable(Result.Employees) @FileStatusTable(Result.FileStatuses) @DataQualityTable(Result.DataQualityIssues) @GuidePanel() @code { [Parameter, EditorRequired] public HrKpiResult Result { get; set; } = new(); private string T(string german, string english) => UiText.Text(german, english); private static Color MetricColor(string severity) => severity == "Warning" ? Color.Warning : Color.Default; private static Color TrafficLightColor(string value) => value switch { "Rot" => Color.Error, "Gelb" => Color.Warning, _ => 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") ?? "-"; private RenderFragment> MetricGrid => metrics => @ @foreach (var metric in metrics) { @metric.Label @metric.Value @metric.Detail } ; private RenderFragment> HeadcountByOrganisationTable => items => @ @T("Headcount nach Organisation", "Headcount by organisation") @T("Organisation", "Organisation") @T("Headcount", "Headcount") FTE @context.Label @context.Count.ToString("N0") @context.Value.ToString("N1") ; 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") @T("Name", "Name") @T("Organisation", "Organisation") @T("Saldo", "Balance") @T("Ampel", "Status") @DisplayPersonName(context.NameVoll, context.Personalnummer, Result.Options.ManagementView) @context.Organisationseinheit @context.StundenSaldo.ToString("N1") @context.GlzAmpel ; private RenderFragment> TurnoverRelevantTable => items => @ @T("Fluktuationsrelevante Austritte", "Turnover relevant leavers") @T("Name", "Name") @T("Austritt", "Exit") @T("Organisation", "Organisation") @T("Austrittsart", "Exit type") @DisplayPersonName(context.NameVoll, context.Personalnummer, Result.Options.ManagementView) @FormatDate(context.Austrittsdatum) @context.Organisationseinheit @context.Austrittsart ; private RenderFragment> LeaverExclusionTable => items => @ @T("Ausschlussgruende", "Exclusion reasons") @T("Grund", "Reason") @T("Anzahl", "Count") @context.Label @context.Count.ToString("N0") ; private RenderFragment> EmployeesTable => items => @ @T("Mitarbeitende", "Employees") @T("Personalnr.", "Personnel no.") @T("Name", "Name") @T("Organisation", "Organisation") @T("Kostenstelle", "Cost center") FTE @T("Alter", "Age") @T("Dienstjahre", "Service years") @T("Typ", "Type") @context.Personalnummer @DisplayPersonName(context.NameVoll, context.Personalnummer, Result.Options.ManagementView) @context.Organisationseinheit @context.KostenstelleText @context.Fte.ToString("N2") @context.AlterJahre @context.Dienstjahre @context.Mitarbeitertyp ; private RenderFragment> FileStatusTable => items => @ @T("Dateistatus", "File status") @T("Quelle", "Source") @T("Status", "Status") @T("Stand", "Modified") @T("Alter", "Age") @T("Zeilen", "Rows") @context.Label @context.Path @(context.Message ?? "-") @FormatDate(context.LastModified) @(context.AgeDays.HasValue ? $"{context.AgeDays:N0} Tage / {context.FreshnessStatus}" : "-") @context.RowCount.ToString("N0") ; private RenderFragment GuidePanel() => @ @T("Ablauf fuer HR", "HR workflow")
1 @T("Rexx exportieren", "Export from Rexx")

@T("Die benoetigten Rexx-Abfragen manuell herunterladen. Excel/XLSX verwenden, nicht PDF.", "Download the required Rexx queries manually. Use Excel/XLSX, not PDF.")

2 @T("Dateien ablegen", "Place files")

@T("Downloads in den Datenordner kopieren und exakt wie unten benennen.", "Copy downloads into the data folder and name them exactly as listed below.")

3 @T("Cockpit laden", "Load cockpit")

@T("Im HR-KPI-Cockpit den Datenordner kontrollieren und Laden klicken.", "Check the data folder in the HR KPI cockpit and click Load.")

4 @T("Datenstatus pruefen", "Check data status")

@T("Im Reiter Datenstatus muessen die erwarteten Dateien gruen erscheinen.", "In the Data status tab, the expected files should be green.")

@T("Datenordner", "Data folder") @Result.Options.DataFolder @T("Der Standardordner ist konfigurierbar. Fuer einen anderen Ordner oben im HR-KPI-Filter den Datenordner anpassen und neu laden.", "The default folder is configurable. To use another folder, change the data folder in the HR KPI filter above and reload.") @T("HR-Dateien enthalten Personendaten. Nicht per E-Mail weiterleiten und keine Kopien in ungeschuetzten Ordnern liegen lassen.", "HR files contain personal data. Do not forward them by email and do not leave copies in unprotected folders.") @T("Erwartete Dateien", "Expected files") @T("Inhalt", "Content") @T("Datei/Pfad", "File/path") @T("Status", "Status") @context.Label @context.Path @(context.Exists ? T("gefunden", "found") : T("fehlt", "missing"))
; private static IEnumerable BuildLeaverExclusionRows(IReadOnlyList items) => items .GroupBy(x => x.FluktuationAusschlussgrund ?? "Relevant") .Select(g => new HrKpiGroupValue { Label = g.Key, Count = g.Count(), Value = g.Count() }) .OrderByDescending(x => x.Count); private RenderFragment TurnoverGauge => visual => @ @visual.RateTitle
@visual.YearRateLabel
0-20%
0% 8% 12% 20%+
; private RenderFragment> TurnoverFunnel => items => @ @T("Austritts-Funnel", "Leaver funnel")
@foreach (var item in items) {
@item.Label
@item.Count.ToString("N0")
}
; private RenderFragment> TurnoverDonut => items => @ @T("Ausschlussgruende", "Exclusion reasons")
@items.Sum(x => x.Count).ToString("N0")
@foreach (var item in items.Take(7)) {
@item.Label @item.Count.ToString("N0")
}
; private RenderFragment> HorizontalBars => items => @ @T("Relevante Austritte nach Organisation", "Relevant leavers by organisation")
@foreach (var item in items) {
@item.Label
@item.Count.ToString("N0")
}
; private RenderFragment MonthlyBars => visual => @ @visual.TimelineTitle
@foreach (var item in visual.MonthlyRelevantLeavers) {
0 ? 8 : 1).ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)}%; background:{item.Color}")">
@item.Count
@item.Label
}
; private static string BuildDonutStyle(IReadOnlyList items) { var total = items.Sum(x => x.Count); if (total <= 0) return "background:#e0e0e0"; var current = 0m; var segments = new List(); foreach (var item in items) { var start = current; current += item.Count / (decimal)total * 100m; segments.Add($"{item.Color} {start.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)}% {current.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)}%"); } return $"background:conic-gradient({string.Join(", ", segments)})"; } }