Expand HR KPI cockpit and add user guides
This commit is contained in:
@@ -7,6 +7,15 @@
|
|||||||
<MudTabPanel Text="@T("Ueberblick", "Overview")" Icon="@Icons.Material.Filled.Dashboard">
|
<MudTabPanel Text="@T("Ueberblick", "Overview")" Icon="@Icons.Material.Filled.Dashboard">
|
||||||
@MetricGrid(Result.Metrics)
|
@MetricGrid(Result.Metrics)
|
||||||
|
|
||||||
|
<MudGrid Class="mt-4">
|
||||||
|
<MudItem xs="12" md="7">
|
||||||
|
@TrafficLightPanel(Result.TrafficLights)
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="5">
|
||||||
|
@MetricGrid(Result.PeriodComparisonMetrics)
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
<MudGrid Class="mt-4">
|
<MudGrid Class="mt-4">
|
||||||
<MudItem xs="12" md="6">
|
<MudItem xs="12" md="6">
|
||||||
@HeadcountByOrganisationTable(Result.HeadcountByOrganisation)
|
@HeadcountByOrganisationTable(Result.HeadcountByOrganisation)
|
||||||
@@ -29,6 +38,15 @@
|
|||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
|
|
||||||
|
<MudGrid Class="mt-4">
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
@GroupValueTable(T("Austritte nach Austrittsart", "Leavers by exit type"), Result.LeaversByType, T("Austritte", "Leavers"))
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
@GroupValueTable(T("Austritte nach Organisation", "Leavers by organisation"), Result.LeaversByOrganisation, T("Austritte", "Leavers"))
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
<MudGrid Class="mt-4">
|
<MudGrid Class="mt-4">
|
||||||
<MudItem xs="12" md="4">
|
<MudItem xs="12" md="4">
|
||||||
@TurnoverGauge(Result.TurnoverVisuals)
|
@TurnoverGauge(Result.TurnoverVisuals)
|
||||||
@@ -51,8 +69,21 @@
|
|||||||
</MudGrid>
|
</MudGrid>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
|
|
||||||
|
<MudTabPanel Text="@T("Ampel", "Status")" Icon="@Icons.Material.Filled.Traffic">
|
||||||
|
@TrafficLightPanel(Result.TrafficLights)
|
||||||
|
@MetricGrid(Result.PeriodComparisonMetrics)
|
||||||
|
</MudTabPanel>
|
||||||
|
|
||||||
<MudTabPanel Text="@T("Absenzen", "Absences")" Icon="@Icons.Material.Filled.Sick">
|
<MudTabPanel Text="@T("Absenzen", "Absences")" Icon="@Icons.Material.Filled.Sick">
|
||||||
@MetricGrid(Result.AbsenceMetrics)
|
@MetricGrid(Result.AbsenceMetrics)
|
||||||
|
<MudGrid Class="mt-4">
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
@GroupValueTable(T("Absenzen nach Organisation", "Absences by organisation"), Result.AbsenceByOrganisation, T("Krankheitstage", "Sick days"))
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
@TopAbsencesTable(Result.Absences)
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||||
<MudText Typo="Typo.h6" Class="mb-2">@T("Absenzen je Mitarbeiter", "Absences by employee")</MudText>
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Absenzen je Mitarbeiter", "Absences by employee")</MudText>
|
||||||
<MudTable Items="Result.Absences.OrderByDescending(x => x.KrankheitstageGesamt).Take(100)" Dense Hover Striped>
|
<MudTable Items="Result.Absences.OrderByDescending(x => x.KrankheitstageGesamt).Take(100)" Dense Hover Striped>
|
||||||
@@ -67,7 +98,7 @@
|
|||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
<RowTemplate>
|
<RowTemplate>
|
||||||
<MudTd>@context.Personalnummer</MudTd>
|
<MudTd>@context.Personalnummer</MudTd>
|
||||||
<MudTd>@context.Name</MudTd>
|
<MudTd>@DisplayPersonName(context.Name, context.Personalnummer, Result.Options.ManagementView)</MudTd>
|
||||||
<MudTd>@context.Organisationseinheit</MudTd>
|
<MudTd>@context.Organisationseinheit</MudTd>
|
||||||
<MudTd>@context.KrankheitstageKurz.ToString("N1")</MudTd>
|
<MudTd>@context.KrankheitstageKurz.ToString("N1")</MudTd>
|
||||||
<MudTd>@context.KrankheitstageLang.ToString("N1")</MudTd>
|
<MudTd>@context.KrankheitstageLang.ToString("N1")</MudTd>
|
||||||
@@ -100,7 +131,7 @@
|
|||||||
<MudTh>@T("Ampel", "Status")</MudTh>
|
<MudTh>@T("Ampel", "Status")</MudTh>
|
||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
<RowTemplate>
|
<RowTemplate>
|
||||||
<MudTd>@context.NameVoll</MudTd>
|
<MudTd>@DisplayPersonName(context.NameVoll, context.Personalnummer, Result.Options.ManagementView)</MudTd>
|
||||||
<MudTd>@context.Organisationseinheit</MudTd>
|
<MudTd>@context.Organisationseinheit</MudTd>
|
||||||
<MudTd>@context.UrlaubRest.ToString("N1")</MudTd>
|
<MudTd>@context.UrlaubRest.ToString("N1")</MudTd>
|
||||||
<MudTd>@context.FerienAusstehend.ToString("N1")</MudTd>
|
<MudTd>@context.FerienAusstehend.ToString("N1")</MudTd>
|
||||||
@@ -122,6 +153,11 @@
|
|||||||
|
|
||||||
<MudTabPanel Text="@T("Datenstatus", "Data status")" Icon="@Icons.Material.Filled.FactCheck">
|
<MudTabPanel Text="@T("Datenstatus", "Data status")" Icon="@Icons.Material.Filled.FactCheck">
|
||||||
@FileStatusTable(Result.FileStatuses)
|
@FileStatusTable(Result.FileStatuses)
|
||||||
|
<MudGrid Class="mt-4">
|
||||||
|
<MudItem xs="12">
|
||||||
|
@DataQualityTable(Result.DataQualityIssues)
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
|
|
||||||
<MudTabPanel Text="@T("Anleitung", "Guide")" Icon="@Icons.Material.Filled.HelpOutline">
|
<MudTabPanel Text="@T("Anleitung", "Guide")" Icon="@Icons.Material.Filled.HelpOutline">
|
||||||
@@ -145,6 +181,19 @@
|
|||||||
_ => Color.Success
|
_ => 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)
|
private static string FormatDate(DateTime? value)
|
||||||
=> value?.ToString("dd.MM.yyyy") ?? "-";
|
=> value?.ToString("dd.MM.yyyy") ?? "-";
|
||||||
|
|
||||||
@@ -177,6 +226,93 @@
|
|||||||
</MudTable>
|
</MudTable>
|
||||||
</MudPaper>;
|
</MudPaper>;
|
||||||
|
|
||||||
|
private RenderFragment<IReadOnlyList<HrKpiTrafficLight>> TrafficLightPanel => items => @<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-2">@T("HR-Ampel", "HR status")</MudText>
|
||||||
|
<MudGrid>
|
||||||
|
@foreach (var item in items)
|
||||||
|
{
|
||||||
|
<MudItem xs="12" sm="6" md="4">
|
||||||
|
<MudPaper Class="pa-3" Elevation="0">
|
||||||
|
<MudStack Row AlignItems="AlignItems.Center" Spacing="2">
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="@TrafficLightColor(item.Status)" Variant="Variant.Filled">
|
||||||
|
@item.Status
|
||||||
|
</MudChip>
|
||||||
|
<MudText Typo="Typo.subtitle2">@item.Area</MudText>
|
||||||
|
</MudStack>
|
||||||
|
<MudText Typo="Typo.h6">@item.Value</MudText>
|
||||||
|
<MudText Typo="Typo.body2">@item.Detail</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
}
|
||||||
|
</MudGrid>
|
||||||
|
</MudPaper>;
|
||||||
|
|
||||||
|
private RenderFragment<IReadOnlyList<HrKpiDataQualityIssue>> DataQualityTable => items => @<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Datenqualitaet", "Data quality")</MudText>
|
||||||
|
<MudTable Items="items" Dense Hover Striped>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>@T("Schwere", "Severity")</MudTh>
|
||||||
|
<MudTh>@T("Bereich", "Area")</MudTh>
|
||||||
|
<MudTh>@T("Pruefpunkt", "Check")</MudTh>
|
||||||
|
<MudTh>@T("Anzahl", "Count")</MudTh>
|
||||||
|
<MudTh>@T("Hinweis", "Note")</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="@MapQualityColor(context.Severity)" Variant="Variant.Outlined">
|
||||||
|
@context.Severity
|
||||||
|
</MudChip>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>@context.Area</MudTd>
|
||||||
|
<MudTd>@context.Issue</MudTd>
|
||||||
|
<MudTd>@context.Count.ToString("N0")</MudTd>
|
||||||
|
<MudTd>@context.Detail</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudText Typo="Typo.body2">@T("Keine Datenqualitaetswarnungen.", "No data quality warnings.")</MudText>
|
||||||
|
</NoRecordsContent>
|
||||||
|
</MudTable>
|
||||||
|
</MudPaper>;
|
||||||
|
|
||||||
|
private RenderFragment<(string Title, IReadOnlyList<HrKpiGroupValue> Items, string ValueLabel)> GroupValueTableTuple => data => @<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-2">@data.Title</MudText>
|
||||||
|
<MudTable Items="data.Items" Dense Hover Striped>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>@T("Gruppe", "Group")</MudTh>
|
||||||
|
<MudTh>@data.ValueLabel</MudTh>
|
||||||
|
<MudTh>%</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@context.Label</MudTd>
|
||||||
|
<MudTd>@(context.Value != 0 ? context.Value.ToString("N1") : context.Count.ToString("N0"))</MudTd>
|
||||||
|
<MudTd>@context.Percent.ToString("N1")</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
</MudPaper>;
|
||||||
|
|
||||||
|
private RenderFragment GroupValueTable(string title, IReadOnlyList<HrKpiGroupValue> items, string valueLabel)
|
||||||
|
=> GroupValueTableTuple((title, items, valueLabel));
|
||||||
|
|
||||||
|
private RenderFragment<IReadOnlyList<HrAbsenceRow>> TopAbsencesTable => items => @<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Hoechste Absenzen", "Highest absences")</MudText>
|
||||||
|
<MudTable Items="items.OrderByDescending(x => x.KrankheitstageGesamt).Take(25)" Dense Hover Striped>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>@T("Name", "Name")</MudTh>
|
||||||
|
<MudTh>@T("Organisation", "Organisation")</MudTh>
|
||||||
|
<MudTh>@T("Kurz", "Short")</MudTh>
|
||||||
|
<MudTh>@T("Lang", "Long")</MudTh>
|
||||||
|
<MudTh>@T("Gesamt", "Total")</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@DisplayPersonName(context.Name, context.Personalnummer, Result.Options.ManagementView)</MudTd>
|
||||||
|
<MudTd>@context.Organisationseinheit</MudTd>
|
||||||
|
<MudTd>@context.KrankheitstageKurz.ToString("N1")</MudTd>
|
||||||
|
<MudTd>@context.KrankheitstageLang.ToString("N1")</MudTd>
|
||||||
|
<MudTd>@context.KrankheitstageGesamt.ToString("N1")</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
</MudPaper>;
|
||||||
|
|
||||||
private RenderFragment<IReadOnlyList<HrKpiEmployeeRow>> CriticalBalancesTable => items => @<MudPaper Class="pa-4" Elevation="1">
|
private RenderFragment<IReadOnlyList<HrKpiEmployeeRow>> CriticalBalancesTable => items => @<MudPaper Class="pa-4" Elevation="1">
|
||||||
<MudText Typo="Typo.h6" Class="mb-2">@T("Kritische GLZ-Saldi", "Critical time balances")</MudText>
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Kritische GLZ-Saldi", "Critical time balances")</MudText>
|
||||||
<MudTable Items="items" Dense Hover Striped>
|
<MudTable Items="items" Dense Hover Striped>
|
||||||
@@ -187,7 +323,7 @@
|
|||||||
<MudTh>@T("Ampel", "Status")</MudTh>
|
<MudTh>@T("Ampel", "Status")</MudTh>
|
||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
<RowTemplate>
|
<RowTemplate>
|
||||||
<MudTd>@context.NameVoll</MudTd>
|
<MudTd>@DisplayPersonName(context.NameVoll, context.Personalnummer, Result.Options.ManagementView)</MudTd>
|
||||||
<MudTd>@context.Organisationseinheit</MudTd>
|
<MudTd>@context.Organisationseinheit</MudTd>
|
||||||
<MudTd>@context.StundenSaldo.ToString("N1")</MudTd>
|
<MudTd>@context.StundenSaldo.ToString("N1")</MudTd>
|
||||||
<MudTd>
|
<MudTd>
|
||||||
@@ -209,7 +345,7 @@
|
|||||||
<MudTh>@T("Austrittsart", "Exit type")</MudTh>
|
<MudTh>@T("Austrittsart", "Exit type")</MudTh>
|
||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
<RowTemplate>
|
<RowTemplate>
|
||||||
<MudTd>@context.NameVoll</MudTd>
|
<MudTd>@DisplayPersonName(context.NameVoll, context.Personalnummer, Result.Options.ManagementView)</MudTd>
|
||||||
<MudTd>@FormatDate(context.Austrittsdatum)</MudTd>
|
<MudTd>@FormatDate(context.Austrittsdatum)</MudTd>
|
||||||
<MudTd>@context.Organisationseinheit</MudTd>
|
<MudTd>@context.Organisationseinheit</MudTd>
|
||||||
<MudTd>@context.Austrittsart</MudTd>
|
<MudTd>@context.Austrittsart</MudTd>
|
||||||
@@ -246,7 +382,7 @@
|
|||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
<RowTemplate>
|
<RowTemplate>
|
||||||
<MudTd>@context.Personalnummer</MudTd>
|
<MudTd>@context.Personalnummer</MudTd>
|
||||||
<MudTd>@context.NameVoll</MudTd>
|
<MudTd>@DisplayPersonName(context.NameVoll, context.Personalnummer, Result.Options.ManagementView)</MudTd>
|
||||||
<MudTd>@context.Organisationseinheit</MudTd>
|
<MudTd>@context.Organisationseinheit</MudTd>
|
||||||
<MudTd>@context.KostenstelleText</MudTd>
|
<MudTd>@context.KostenstelleText</MudTd>
|
||||||
<MudTd>@context.Fte.ToString("N2")</MudTd>
|
<MudTd>@context.Fte.ToString("N2")</MudTd>
|
||||||
@@ -266,6 +402,8 @@
|
|||||||
<HeaderContent>
|
<HeaderContent>
|
||||||
<MudTh>@T("Quelle", "Source")</MudTh>
|
<MudTh>@T("Quelle", "Source")</MudTh>
|
||||||
<MudTh>@T("Status", "Status")</MudTh>
|
<MudTh>@T("Status", "Status")</MudTh>
|
||||||
|
<MudTh>@T("Stand", "Modified")</MudTh>
|
||||||
|
<MudTh>@T("Alter", "Age")</MudTh>
|
||||||
<MudTh>@T("Zeilen", "Rows")</MudTh>
|
<MudTh>@T("Zeilen", "Rows")</MudTh>
|
||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
<RowTemplate>
|
<RowTemplate>
|
||||||
@@ -278,6 +416,8 @@
|
|||||||
@(context.Message ?? "-")
|
@(context.Message ?? "-")
|
||||||
</MudChip>
|
</MudChip>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
|
<MudTd>@FormatDate(context.LastModified)</MudTd>
|
||||||
|
<MudTd>@(context.AgeDays.HasValue ? $"{context.AgeDays:N0} Tage / {context.FreshnessStatus}" : "-")</MudTd>
|
||||||
<MudTd>@context.RowCount.ToString("N0")</MudTd>
|
<MudTd>@context.RowCount.ToString("N0")</MudTd>
|
||||||
</RowTemplate>
|
</RowTemplate>
|
||||||
</MudTable>
|
</MudTable>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
@inject IHrKpiAccessService HrKpiAccess
|
@inject IHrKpiAccessService HrKpiAccess
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject IUiTextService UiText
|
@inject IUiTextService UiText
|
||||||
|
@inject IJSRuntime JsRuntime
|
||||||
|
|
||||||
<PageTitle>@T("HR KPI", "HR KPI")</PageTitle>
|
<PageTitle>@T("HR KPI", "HR KPI")</PageTitle>
|
||||||
|
|
||||||
@@ -65,6 +66,10 @@ else
|
|||||||
@(_loading ? T("Lade...", "Loading...") : T("Laden", "Load"))
|
@(_loading ? T("Lade...", "Loading...") : T("Laden", "Load"))
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="2">
|
||||||
|
<MudSwitch T="bool" @bind-Value="_managementView" Color="Color.Primary"
|
||||||
|
Label="@T("Managementsicht", "Management view")" />
|
||||||
|
</MudItem>
|
||||||
<MudItem xs="12" md="3">
|
<MudItem xs="12" md="3">
|
||||||
<MudDatePicker @bind-Date="_fromDate" Label="@T("Von Austritt", "Exit from")" Clearable DateFormat="dd.MM.yyyy" />
|
<MudDatePicker @bind-Date="_fromDate" Label="@T("Von Austritt", "Exit from")" Clearable DateFormat="dd.MM.yyyy" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
@@ -128,6 +133,12 @@ else
|
|||||||
@T("Sperren", "Lock")
|
@T("Sperren", "Lock")
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="2">
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Primary" OnClick="PrintAsync"
|
||||||
|
StartIcon="@Icons.Material.Filled.Print" FullWidth>
|
||||||
|
@T("Drucken/PDF", "Print/PDF")
|
||||||
|
</MudButton>
|
||||||
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
}
|
}
|
||||||
@@ -161,6 +172,7 @@ else
|
|||||||
private string? _glzAmpel;
|
private string? _glzAmpel;
|
||||||
private string? _restferienAmpel;
|
private string? _restferienAmpel;
|
||||||
private string? _searchText;
|
private string? _searchText;
|
||||||
|
private bool _managementView;
|
||||||
private string? _hrUsername;
|
private string? _hrUsername;
|
||||||
private string? _hrPassword;
|
private string? _hrPassword;
|
||||||
private bool _loading;
|
private bool _loading;
|
||||||
@@ -207,7 +219,8 @@ else
|
|||||||
FluktuationFilter = _fluktuationFilter,
|
FluktuationFilter = _fluktuationFilter,
|
||||||
GlzAmpel = _glzAmpel,
|
GlzAmpel = _glzAmpel,
|
||||||
RestferienAmpel = _restferienAmpel,
|
RestferienAmpel = _restferienAmpel,
|
||||||
SearchText = _searchText
|
SearchText = _searchText,
|
||||||
|
ManagementView = _managementView
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -239,6 +252,11 @@ else
|
|||||||
_hrPassword = string.Empty;
|
_hrPassword = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task PrintAsync()
|
||||||
|
{
|
||||||
|
await JsRuntime.InvokeVoidAsync("print");
|
||||||
|
}
|
||||||
|
|
||||||
private bool CanShowHrKpi => !HrKpiAccess.IsEnabled || HrKpiAccess.IsUnlocked;
|
private bool CanShowHrKpi => !HrKpiAccess.IsEnabled || HrKpiAccess.IsUnlocked;
|
||||||
|
|
||||||
private string T(string german, string english) => UiText.Text(german, english);
|
private string T(string german, string english) => UiText.Text(german, english);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public sealed class HrKpiOptions
|
|||||||
public string? GlzAmpel { get; set; }
|
public string? GlzAmpel { get; set; }
|
||||||
public string? RestferienAmpel { get; set; }
|
public string? RestferienAmpel { get; set; }
|
||||||
public string? SearchText { get; set; }
|
public string? SearchText { get; set; }
|
||||||
|
public bool ManagementView { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class HrKpiDataSourceOptions
|
public sealed class HrKpiDataSourceOptions
|
||||||
@@ -57,6 +58,13 @@ public sealed class HrKpiResult
|
|||||||
public List<HrKpiMetric> TurnoverMetrics { get; set; } = [];
|
public List<HrKpiMetric> TurnoverMetrics { get; set; } = [];
|
||||||
public List<HrKpiMetric> AbsenceMetrics { get; set; } = [];
|
public List<HrKpiMetric> AbsenceMetrics { get; set; } = [];
|
||||||
public List<HrKpiMetric> TimeVacationMetrics { get; set; } = [];
|
public List<HrKpiMetric> TimeVacationMetrics { get; set; } = [];
|
||||||
|
public List<HrKpiMetric> PeriodComparisonMetrics { get; set; } = [];
|
||||||
|
public List<HrKpiTrafficLight> TrafficLights { get; set; } = [];
|
||||||
|
public List<HrKpiDataQualityIssue> DataQualityIssues { get; set; } = [];
|
||||||
|
public List<HrKpiGroupValue> LeaversByType { get; set; } = [];
|
||||||
|
public List<HrKpiGroupValue> LeaversByOrganisation { get; set; } = [];
|
||||||
|
public List<HrKpiGroupValue> AbsenceByOrganisation { get; set; } = [];
|
||||||
|
public List<HrKpiEmployeeRow> CriticalAbsences { get; set; } = [];
|
||||||
public List<HrKpiEmployeeRow> Employees { get; set; } = [];
|
public List<HrKpiEmployeeRow> Employees { get; set; } = [];
|
||||||
public List<HrAbsenceRow> Absences { get; set; } = [];
|
public List<HrAbsenceRow> Absences { get; set; } = [];
|
||||||
public List<HrLeaverRow> Leavers { get; set; } = [];
|
public List<HrLeaverRow> Leavers { get; set; } = [];
|
||||||
@@ -73,6 +81,26 @@ public sealed class HrKpiFileStatus
|
|||||||
public bool Exists { get; set; }
|
public bool Exists { get; set; }
|
||||||
public int RowCount { get; set; }
|
public int RowCount { get; set; }
|
||||||
public string? Message { 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
|
public sealed class HrKpiMetric
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ internal sealed class HrKpiDashboardBuilder
|
|||||||
FluktuationFilter = NormalizeFilter(options.FluktuationFilter),
|
FluktuationFilter = NormalizeFilter(options.FluktuationFilter),
|
||||||
GlzAmpel = NormalizeFilter(options.GlzAmpel),
|
GlzAmpel = NormalizeFilter(options.GlzAmpel),
|
||||||
RestferienAmpel = NormalizeFilter(options.RestferienAmpel),
|
RestferienAmpel = NormalizeFilter(options.RestferienAmpel),
|
||||||
SearchText = NormalizeFilter(options.SearchText)
|
SearchText = NormalizeFilter(options.SearchText),
|
||||||
|
ManagementView = options.ManagementView
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = new HrKpiResult { Options = normalizedOptions };
|
var result = new HrKpiResult { Options = normalizedOptions };
|
||||||
@@ -107,6 +108,23 @@ internal sealed class HrKpiDashboardBuilder
|
|||||||
result.TurnoverMetrics = BuildTurnoverMetrics(turnoverEmployees, turnoverHeadcountLeavers, leavers, turnoverPeriod);
|
result.TurnoverMetrics = BuildTurnoverMetrics(turnoverEmployees, turnoverHeadcountLeavers, leavers, turnoverPeriod);
|
||||||
result.AbsenceMetrics = BuildAbsenceMetrics(employees, absences);
|
result.AbsenceMetrics = BuildAbsenceMetrics(employees, absences);
|
||||||
result.TimeVacationMetrics = BuildTimeVacationMetrics(employees);
|
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.TurnoverVisuals = BuildTurnoverVisuals(turnoverEmployees, turnoverHeadcountLeavers, leavers, turnoverPeriod);
|
||||||
result.HeadcountByOrganisation = employees
|
result.HeadcountByOrganisation = employees
|
||||||
.GroupBy(x => BlankAsUnknown(x.Organisationseinheit), StringComparer.OrdinalIgnoreCase)
|
.GroupBy(x => BlankAsUnknown(x.Organisationseinheit), StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -587,6 +605,171 @@ internal sealed class HrKpiDashboardBuilder
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<HrKpiMetric> BuildPeriodComparisonMetrics(
|
||||||
|
IReadOnlyCollection<HrKpiEmployeeRow> employees,
|
||||||
|
IReadOnlyCollection<HrLeaverRow> turnoverHeadcountLeavers,
|
||||||
|
IReadOnlyCollection<HrLeaverRow> 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<HrKpiTrafficLight> BuildTrafficLights(
|
||||||
|
IReadOnlyList<HrKpiMetric> overviewMetrics,
|
||||||
|
IReadOnlyList<HrKpiMetric> turnoverMetrics,
|
||||||
|
IReadOnlyList<HrKpiMetric> absenceMetrics,
|
||||||
|
IReadOnlyList<HrKpiMetric> 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<HrKpiDataQualityIssue> BuildDataQualityIssues(
|
||||||
|
IReadOnlyCollection<HrKpiEmployeeRow> employees,
|
||||||
|
IReadOnlyCollection<HrAbsenceRow> absences,
|
||||||
|
IReadOnlyCollection<HrLeaverRow> leavers,
|
||||||
|
IReadOnlyDictionary<string, SapRow> 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<HrKpiGroupValue> BuildLeaverTypeGroups(IReadOnlyCollection<HrLeaverRow> 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<HrKpiGroupValue> BuildLeaverOrganisationGroups(IReadOnlyCollection<HrLeaverRow> 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<HrKpiGroupValue> BuildAbsenceOrganisationGroups(IReadOnlyCollection<HrAbsenceRow> 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<HrKpiMetric> 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(
|
private static HrTurnoverVisuals BuildTurnoverVisuals(
|
||||||
IReadOnlyCollection<HrKpiEmployeeRow> employees,
|
IReadOnlyCollection<HrKpiEmployeeRow> employees,
|
||||||
IReadOnlyCollection<HrLeaverRow> turnoverHeadcountLeavers,
|
IReadOnlyCollection<HrLeaverRow> turnoverHeadcountLeavers,
|
||||||
@@ -1219,6 +1402,8 @@ internal sealed class HrKpiDashboardBuilder
|
|||||||
public bool HasFile(string fileName)
|
public bool HasFile(string fileName)
|
||||||
=> File.Exists(BuildPath(fileName));
|
=> File.Exists(BuildPath(fileName));
|
||||||
|
|
||||||
|
public IReadOnlyList<HrKpiFileStatus> FileStatuses => _result.FileStatuses;
|
||||||
|
|
||||||
public List<T> ReadRows<T>(string fileName, string label, Func<IXLRow, IReadOnlyDictionary<string, int>, T> map)
|
public List<T> ReadRows<T>(string fileName, string label, Func<IXLRow, IReadOnlyDictionary<string, int>, T> map)
|
||||||
{
|
{
|
||||||
var path = BuildPath(fileName);
|
var path = BuildPath(fileName);
|
||||||
@@ -1228,6 +1413,12 @@ internal sealed class HrKpiDashboardBuilder
|
|||||||
Path = path,
|
Path = path,
|
||||||
Exists = File.Exists(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);
|
_result.FileStatuses.Add(status);
|
||||||
|
|
||||||
if (!status.Exists)
|
if (!status.Exists)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user