@page "/hr-kpi" @using TrafagSalesExporter.Services @inject IHrKpiService HrKpiService @inject ISnackbar Snackbar @inject IUiTextService UiText @T("HR KPI", "HR KPI") @T("HR KPI", "HR KPI") @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 } @if (_result is not null) { @if (_result.Notices.Count > 0) { @foreach (var notice in _result.Notices) { @notice } } @MetricGrid(_result.Metrics) @HeadcountByOrganisationTable(_result.HeadcountByOrganisation) @CriticalBalancesTable(_result.CriticalTimeBalances) @MetricGrid(_result.TurnoverMetrics) @TurnoverRelevantTable(_result.FluctuationRelevantLeavers) @LeaverExclusionTable(_result.Leavers) @TurnoverGauge(_result.TurnoverVisuals) @TurnoverFunnel(_result.TurnoverVisuals.FunnelSteps) @TurnoverDonut(_result.TurnoverVisuals.ExclusionReasons) @HorizontalBars(_result.TurnoverVisuals.RelevantByOrganisation) @MonthlyBars(_result.TurnoverVisuals.MonthlyRelevantLeavers) @MetricGrid(_result.AbsenceMetrics) @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 @context.Name @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") @context.NameVoll @context.Organisationseinheit @context.UrlaubRest.ToString("N1") @context.FerienAusstehend.ToString("N1") @context.RestferienAmpel @EmployeesTable(_result.Employees) @FileStatusTable(_result.FileStatuses) } @code { private string _dataFolder = @"C:\temp"; private int _year = DateTime.Today.Year; private DateTime? _fromDate; private DateTime? _toDate; private int? _entryYear; private string? _organisation; private string? _kostenstelle; private string? _mitarbeitertyp; private string _fluktuationFilter = "Alle"; private string? _glzAmpel; private string? _restferienAmpel; private string? _searchText; private bool _loading; private HrKpiResult? _result; private readonly List<(string Key, string Label)> _fluktuationOptions = [ ("Alle", "Alle"), ("Fluktuationsrelevant", "Relevant"), ("Arbeitnehmerkuendigung", "Arbeitnehmerkuendigung"), ("Ausgeschlossen", "Ausgeschlossen") ]; private readonly List _ampelOptions = ["Gruen", "Gelb", "Rot"]; private readonly List _restferienOptions = ["Gruen", "Rot"]; protected override async Task OnInitializedAsync() { await LoadAsync(); } private async Task LoadAsync() { _loading = true; try { _result = await HrKpiService.BuildAsync(new HrKpiOptions { DataFolder = _dataFolder, Year = _year, FromDate = _fromDate, ToDate = _toDate, EntryYear = _entryYear, Organisationseinheit = _organisation, KostenstelleText = _kostenstelle, Mitarbeitertyp = _mitarbeitertyp, FluktuationFilter = _fluktuationFilter, GlzAmpel = _glzAmpel, RestferienAmpel = _restferienAmpel, SearchText = _searchText }); } catch (Exception ex) { Snackbar.Add(ex.Message, Severity.Error); } finally { _loading = false; } } 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 string FormatDate(DateTime? value) => value?.ToString("dd.MM.yyyy") ?? "-"; } @code { 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> CriticalBalancesTable => items => @ @T("Kritische GLZ-Saldi", "Critical time balances") @T("Name", "Name") @T("Organisation", "Organisation") @T("Saldo", "Balance") @T("Ampel", "Status") @context.NameVoll @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") @context.NameVoll @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 @context.NameVoll @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("Zeilen", "Rows") @context.Label @context.Path @(context.Message ?? "-") @context.RowCount.ToString("N0") ; 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 => @ @T("Jahres-Fluktuation", "Annual turnover")
@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 => items => @ @T("Relevante Austritte pro Monat", "Relevant leavers per month")
@foreach (var item in items) {
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)})"; } }