From 0039ce5069a0acec5e3ec4bde1781fa8b233216b Mon Sep 17 00:00:00 2001 From: Andrei Laas Date: Tue, 29 Sep 2026 20:36:22 +0200 Subject: [PATCH] Headcount heisst ueberall dasselbe "Headcount" stand auf der Uebersicht fuer die Aktiven und im Bericht fuer eine andere Zahl -- 785 gegen 788, dasselbe Wort fuer Verschiedenes (N.02). Max hat die Begriffe festgelegt: Headcount Aktiv fuer die Aktiven, Headcount Aktives Dienstverhaeltnis fuer Aktive plus Langzeitabwesende. Die beiden Kacheln auf der Uebersicht tragen jetzt genau diese Namen. Beide Zahlen gab es dort schon, nur hiess die eine "Aktives Dienstverhaeltnis" und die andere "Aktive Mitarbeiter:innen (HC)". Im Bericht ginge ein fester Name nicht: die Kennzahl zaehlt, was gerade ausgewaehlt ist, auch Geplante oder Ausgetretene. Trifft die Auswahl eine der beiden Groessen, steht ihr Name in der Ueberschrift; sonst steht dabei, welche Status gezaehlt wurden -- mit den Anzeigenamen, also "Langzeitabwesenheit" und nicht "Karenz". --- app/(app)/page.tsx | 11 +++++---- components/reports/ReportsPageClient.tsx | 3 ++- lib/reports.ts | 29 +++++++++++++++++++++++- tests/unit/reports.test.ts | 28 +++++++++++++++++++++++ 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/app/(app)/page.tsx b/app/(app)/page.tsx index 4484e7c..fa204e7 100644 --- a/app/(app)/page.tsx +++ b/app/(app)/page.tsx @@ -198,8 +198,13 @@ export default async function DashboardPage({ // wer heute arbeitet, hier, mit wem ein Vertrag läuft — // Langzeitabwesende eingeschlossen. Sichtbar waren 805 und 10, // addieren musste man selbst. + // Die Namen der beiden stehen so fest: Max am 30.09. — „Es gibt zwei + // Headcounts: Headcount Aktiv (nur Aktive) und Headcount Aktives + // Dienstverhältnis (Aktiv + Langzeitabwesenheit)". Vorher hiess nur die + // rechte Kachel „(HC)", und im Bericht stand daneben „Headcount" für + // eine dritte Zahl — dasselbe Wort für Verschiedenes (N.02). { - label: "Aktives Dienstverhältnis", + label: "Headcount Aktives Dienstverhältnis", value: activeCount + karenzCount, tone: "default", // Mit Komma, nicht zweimal `status=`: die Liste liest den Parameter @@ -207,9 +212,7 @@ export default async function DashboardPage({ // Next daraus ein Array, und die Seite scheiterte an `.split(",")`. href: "/employees?status=Aktiv,Karenz", }, - // „(HC)" für Headcount — der Unterschied zur Kachel links ist genau - // der, den die Abkürzung benennt: Köpfe, die heute arbeiten. - { label: "Aktive Mitarbeiter:innen (HC)", value: activeCount, tone: "default", href: "/employees?status=Aktiv" }, + { label: "Headcount Aktiv", value: activeCount, tone: "default", href: "/employees?status=Aktiv" }, { label: "Langzeitabwesende", value: karenzCount, tone: "warning", href: "/employees?status=Karenz" }, // Ohne Zeitgrenze — anders als die Karte „Anstehend" darunter, die nur // den eingestellten Vorschauzeitraum zeigt. Ein Eintritt in vier diff --git a/components/reports/ReportsPageClient.tsx b/components/reports/ReportsPageClient.tsx index 6816692..9380a23 100644 --- a/components/reports/ReportsPageClient.tsx +++ b/components/reports/ReportsPageClient.tsx @@ -29,6 +29,7 @@ import { EVENT_TYPE_LABELS, GROUP_LABELS, MEASURE_LABELS, + messgroesseLabel, parseStatuses, REPORT_PRESETS, sortKeysForDimension, @@ -413,7 +414,7 @@ export function ReportsPageClient(props: ReportsPageClientProps) { const heading = mode === "snapshot" - ? `${MEASURE_LABELS[props.measure]} nach ${GROUP_LABELS[props.group]}` + ? `${messgroesseLabel(props.measure, selectedStatuses)} nach ${GROUP_LABELS[props.group]}` : `${props.eventType ? EVENT_TYPE_LABELS[props.eventType as HistoryEventType] : "Ereignisse"} nach ${EVENT_GROUP_LABELS[props.eventGroup]}`; const totalDisplay = mode === "snapshot" ? formatValue(props.measure, total) : String(Math.round(total)); diff --git a/lib/reports.ts b/lib/reports.ts index 1cda040..6dbafcd 100644 --- a/lib/reports.ts +++ b/lib/reports.ts @@ -1,4 +1,4 @@ -import { ABSENCE_TYPES } from "./absence"; +import { ABSENCE_TYPES, statusLabel } from "./absence"; import { BEENDIGUNGSART_WERTE } from "./beendigung"; import { fmtName, todayIso, yearsBetweenIso } from "./format"; import { HAY_GRADE_WERTE } from "./hay-grade"; @@ -32,6 +32,33 @@ export type GroupDimension = | "teilzeit_art" | "weekday"; +/** + * Die Kennzahl mit dem, was sie tatsächlich zählt. + * + * „Headcount" stand auf der Übersicht für die Aktiven und im Bericht für eine + * andere Zahl — dasselbe Wort für Verschiedenes, gemeldet als N.02. Der Kunde + * führt zwei Grössen mit festen Namen (Max, 30.09.): **Headcount Aktiv** für + * die Aktiven und **Headcount Aktives Dienstverhältnis** für Aktive plus + * Langzeitabwesende. + * + * Die Kennzahl im Bericht zählt aber, was gerade ausgewählt ist — auch + * Geplante oder Ausgetretene. Ein fester Name wäre dort also schlicht falsch. + * Deshalb: trifft die Auswahl eine der beiden Grössen, steht ihr Name da; + * sonst steht dabei, welche Status gezählt wurden. + */ +export function messgroesseLabel(measure: Measure, statuses: readonly string[]): string { + const basis = MEASURE_LABELS[measure]; + if (measure !== "headcount") return basis; + + const gewaehlt = new Set(statuses); + if (gewaehlt.size === 1 && gewaehlt.has("Aktiv")) return "Headcount Aktiv"; + if (gewaehlt.size === 2 && gewaehlt.has("Aktiv") && gewaehlt.has("Karenz")) { + return "Headcount Aktives Dienstverhältnis"; + } + if (gewaehlt.size === 0) return basis; + return `${basis} (${statuses.map((s) => statusLabel(s as EmploymentStatus)).join(", ")})`; +} + export const MEASURE_LABELS: Record = { headcount: "Headcount", fte: "FTE", diff --git a/tests/unit/reports.test.ts b/tests/unit/reports.test.ts index 746e077..bc77e92 100644 --- a/tests/unit/reports.test.ts +++ b/tests/unit/reports.test.ts @@ -7,6 +7,7 @@ import { groupKeysFor, measureValue, MEASURE_LABELS, + messgroesseLabel, type OrgLookups, type ReportEmployee, type ReportEvent, @@ -179,6 +180,33 @@ describe("measureValue", () => { }); }); +describe("messgroesseLabel", () => { + // „Headcount" stand auf der Übersicht für die Aktiven und im Bericht für + // eine andere Zahl (N.02). Der Kunde führt zwei Grössen mit festen Namen; + // die Kennzahl im Bericht zählt aber, was ausgewählt ist. + it("nennt die beiden Grössen des Kunden beim Namen", () => { + expect(messgroesseLabel("headcount", ["Aktiv"])).toBe("Headcount Aktiv"); + expect(messgroesseLabel("headcount", ["Aktiv", "Karenz"])).toBe("Headcount Aktives Dienstverhältnis"); + expect(messgroesseLabel("headcount", ["Karenz", "Aktiv"])).toBe("Headcount Aktives Dienstverhältnis"); + }); + + it("schreibt sonst dazu, was gezählt wurde", () => { + // Mit Anzeigenamen: „Karenz" heisst in der Oberfläche überall + // „Langzeitabwesenheit". + expect(messgroesseLabel("headcount", ["Aktiv", "Karenz", "Geplant"])).toBe( + "Headcount (Aktiv, Langzeitabwesenheit, Geplant)" + ); + expect(messgroesseLabel("headcount", ["Ausgetreten"])).toBe("Headcount (Ausgetreten)"); + }); + + it("lässt die übrigen Kennzahlen unberührt", () => { + // Nur beim Headcount hing an der Auswahl ein zweiter Begriff. „Ø Alter" + // der Aktiven heisst weiter „Ø Alter". + expect(messgroesseLabel("fte", ["Aktiv"])).toBe("FTE"); + expect(messgroesseLabel("avg_age", ["Aktiv", "Karenz"])).toBe("Ø Alter"); + }); +}); + describe("aggregateReport", () => { it("groups rows, sorts groups descending by value, and never includes a salary field", () => { const employees = [