@using Microsoft.AspNetCore.Components
@using TrafagSalesExporter.Models
@using TrafagSalesExporter.Services
@inject IUiTextService UiText
@inject IJSRuntime JsRuntime
@PrintToolbar("hr-kpi-print-overview", T("Ueberblick als PDF", "Overview as PDF"))
@PrintHeader(T("Ueberblick", "Overview"))
@MetricGrid(Result.Metrics)
@TrafficLightPanel(Result.TrafficLights)
@MetricGrid(Result.PeriodComparisonMetrics)
@HeadcountByOrganisationTable(Result.HeadcountByOrganisation)
@CriticalBalancesTable(Result.CriticalTimeBalances)
@PrintToolbar("hr-kpi-print-turnover", T("Fluktuation als PDF", "Turnover as PDF"))
@PrintHeader(T("Fluktuation", "Turnover"))
@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)
@PrintToolbar("hr-kpi-print-status", T("Ampel als PDF", "Status as PDF"))
@PrintHeader(T("Ampel", "Status"))
@TrafficLightPanel(Result.TrafficLights)
@MetricGrid(Result.PeriodComparisonMetrics)
@PrintToolbar("hr-kpi-print-absences", T("Absenzen als PDF", "Absences as PDF"))
@PrintHeader(T("Absenzen", "Absences"))
@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")
@PrintToolbar("hr-kpi-print-time-vacation", T("Zeit/Ferien als PDF", "Time/vacation as PDF"))
@PrintHeader(T("Zeit / Ferien", "Time / Vacation"))
@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
@PrintToolbar("hr-kpi-print-employees", T("Mitarbeitende als PDF", "Employees as PDF"))
@PrintHeader(T("Mitarbeitende", "Employees"))
@EmployeesTable(Result.Employees)
@PrintToolbar("hr-kpi-print-data-status", T("Datenstatus als PDF", "Data status as PDF"))
@PrintHeader(T("Datenstatus", "Data status"))
@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 RenderFragment PrintToolbar(string targetId, string label) => @
@label
;
private RenderFragment PrintHeader(string title) => @
;
private async Task PrintSectionAsync(string targetId)
{
await JsRuntime.InvokeVoidAsync("trafagDownload.printElement", targetId);
}
private string BuildFilterSummary()
{
var parts = new List();
if (Result.Options.FromDate.HasValue || Result.Options.ToDate.HasValue)
parts.Add($"{T("Zeitraum", "Period")}: {FormatDate(Result.Options.FromDate)} - {FormatDate(Result.Options.ToDate)}");
if (Result.Options.Year.HasValue)
parts.Add($"{T("Austrittsjahr", "Leaver year")}: {Result.Options.Year.Value}");
parts.Add($"{T("Organisation", "Organisation")}: {BlankAsAll(Result.Options.Organisationseinheit)}");
parts.Add($"{T("Mitarbeitertyp", "Employee type")}: {BlankAsAll(Result.Options.Mitarbeitertyp)}");
if (!string.IsNullOrWhiteSpace(Result.Options.KostenstelleText))
parts.Add($"{T("Kostenstelle", "Cost center")}: {Result.Options.KostenstelleText}");
return string.Join(" | ", parts);
}
private string BlankAsAll(string? value)
=> string.IsNullOrWhiteSpace(value) ? T("Alle", "All") : value;
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("Neue Auswertungen im Cockpit", "New cockpit views")
- @T("Managementsicht anonymisiert Personendaten fuer Fuehrungsberichte.", "Management view anonymizes personal data for management reports.")
- @T("Dateistatus zeigt Pfad, Zeilen, Aenderungsdatum, Alter und Frische.", "File status shows path, rows, modification date, age and freshness.")
- @T("HR-Ampel fasst Fluktuation, Krankheit, GLZ, Restferien und Datenqualitaet zusammen.", "HR status summarizes turnover, sickness, time balance, vacation balance and data quality.")
- @T("GLZ- und Restferien-Ampeln koennen gefiltert werden.", "Time-balance and vacation status can be filtered.")
- @T("Periodenvergleich zeigt die wichtigsten Vorjahreswerte, soweit Daten vorhanden sind.", "Period comparison shows key prior-year values where data is available.")
- @T("Datenqualitaet markiert fehlende Dateien, alte Dateien und auffaellige Werte.", "Data quality flags missing files, old files and suspicious values.")
- @T("Austritte werden nach Austrittsart und Organisation gruppiert.", "Leavers are grouped by exit type and organisation.")
- @T("Absenzen werden nach Organisation ausgewertet.", "Absences are evaluated by organisation.")
- @T("Top-Absenzen und kritische Detailtabellen helfen bei der operativen Pruefung.", "Top absences and critical detail tables support operational checks.")
- @T("Drucken/PDF erzeugt eine weitergebbare Ansicht aus dem Browser.", "Print/PDF creates a shareable browser view.")
@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)})";
}
}