diff --git a/components/reports/ReportsPageClient.tsx b/components/reports/ReportsPageClient.tsx index f467d3b..98e2a42 100644 --- a/components/reports/ReportsPageClient.tsx +++ b/components/reports/ReportsPageClient.tsx @@ -13,6 +13,7 @@ import { fmtDate } from "@/lib/format"; import { anzahlKriterien, AUSWAHL_KRITERIEN, + beschreibeKriterien, JANEIN_KRITERIEN, kriterienParameter, ZEITRAUM_KRITERIEN, @@ -212,7 +213,8 @@ function KriterienBlock({

- Alle Kriterien gelten für den Bericht und für beide Exporte. + Alle Kriterien gelten für den Bericht und für jeden Export dieser Seite — auch für den + Honestly- und den Cornerstone-Report. Was gerade eingestellt ist, steht auf der jeweiligen Karte.

)} @@ -220,6 +222,27 @@ function KriterienBlock({ ); } +/** + * Was gerade in die Datei geht — Stichtag, Status, Einheit, Standort und jedes + * gesetzte Kriterium, dazu die Zeilenzahl. + * + * Auf den Karten stand nur der Status. Alles andere schränkt den Export + * genauso ein, steht aber eingeklappt weiter oben: eine Datei mit 40 statt 785 + * Personen sah genauso aus wie eine vollständige. Für einen Bericht ist das + * ärgerlich, für ein Load-File ins Zielsystem ist es ein Datenstand, den dort + * niemand mehr hinterfragt. + */ +function AuswahlHinweis({ texte, anzahl }: { texte: string[]; anzahl: number }) { + return ( +
+ Auswahl: {texte.join(" · ")} + + {anzahl} {anzahl === 1 ? "Person" : "Personen"} + +
+ ); +} + function formatValue(measure: Measure, value: number): string { if (measure === "headcount") return String(Math.round(value)); if (measure === "fte") return value.toFixed(1); @@ -355,6 +378,20 @@ export function ReportsPageClient(props: ReportsPageClientProps) { const showWeekdayMultiCountNote = mode === "snapshot" && (props.group === "weekday" || props.split === "weekday"); const selectedStatuses = mode === "snapshot" ? parseStatuses(props.filters.status) : []; const statusExportLabel = selectedStatuses.length === STATUS_OPTIONS.length ? "Alle" : selectedStatuses.join(", "); + // Einheit und Standort stehen in der Adresszeile als Kennung; auf der Karte + // hilft nur der Name. + const einheitName = mode === "snapshot" ? units.find((u) => u.id === props.filters.division)?.name : undefined; + const standortName = mode === "snapshot" ? locations.find((l) => l.id === props.filters.location)?.name : undefined; + const auswahlTexte = + mode === "snapshot" + ? [ + `Stichtag ${fmtDate(props.asOf || todayIso())}`, + `Status ${statusExportLabel}`, + ...(einheitName ? [`Einheit ${einheitName}`] : []), + ...(standortName ? [`Standort ${standortName}`] : []), + ...beschreibeKriterien(props.criteria), + ] + : []; const currentYear = new Date().getFullYear(); const defaultEventFrom = `${currentYear}-01-01`; const defaultEventTo = `${currentYear}-12-31`; @@ -619,8 +656,8 @@ export function ReportsPageClient(props: ReportsPageClientProps) {

Vollständiger Datenexport

{mode === "snapshot" ? ( <> -

Alle Mitarbeiterdaten (nicht nur die Kennzahl){props.asOf ? ` zum Stichtag ${fmtDate(props.asOf)}` : ""}.

-

Status im Export: {statusExportLabel}

+

Alle Mitarbeiterdaten, nicht nur die Kennzahl.

+ ) : (

Alle Ereignisse im gewählten Zeitraum als Rohdaten (eine Zeile pro Ereignis).

@@ -643,7 +680,7 @@ export function ReportsPageClient(props: ReportsPageClientProps) { {mode === "snapshot" && (

Honestly-Report

-

Status im Export: {statusExportLabel}

+
@@ -665,7 +702,7 @@ export function ReportsPageClient(props: ReportsPageClientProps) { TT.MM.JJJJ, Systemwerte statt Anzeigenamen (Active, Working, Employee). Ohne Location ID und Position ID — dafür fehlen die Kennungen des Zielsystems.

-

Status im Export: {statusExportLabel}

+
diff --git a/lib/report-criteria.ts b/lib/report-criteria.ts index ef083eb..8db6f2a 100644 --- a/lib/report-criteria.ts +++ b/lib/report-criteria.ts @@ -1,5 +1,6 @@ import { ABSENCE_TYPES } from "./absence"; import { AUSTRITTSART_LABELS, AUSTRITTSARTEN, BEENDIGUNGSART_WERTE } from "./beendigung"; +import { fmtDate } from "./format"; import { HAY_GRADE_WERTE } from "./hay-grade"; import { KUENDIGUNGSSCHUTZ_GRUENDE } from "./kuendigungsschutz"; import { MITARBEITERARTEN } from "./mitarbeiterart"; @@ -179,6 +180,49 @@ export function anzahlKriterien(c: Criteria): number { return n; } +/** + * Die gesetzten Kriterien in Worten. + * + * Die Exportkarten nannten nur den Status. Die übrigen Kriterien stehen + * eingeklappt weiter oben und wirken auf jeden Export dieser Seite genauso — + * wer eines gesetzt und vergessen hatte, lud eine Datei herunter, die nach dem + * ganzen Haus aussah und einen Ausschnitt enthielt. Bei einer Datei für ein + * Zielsystem merkt das niemand mehr, sobald sie dort eingespielt ist: dort + * fehlen dann Personen, und es sieht aus wie ein Datenstand, nicht wie ein + * Filter. + * + * `anzahlKriterien` daneben zählt dasselbe; hier steht, *was* es ist. + */ +export function beschreibeKriterien(c: Criteria): string[] { + const teile: string[] = []; + + for (const k of AUSWAHL_KRITERIEN) { + const wert = c.auswahl[k.key]; + if (!wert) continue; + teile.push(`${k.label}: ${k.optionen.find((o) => o.wert === wert)?.label ?? wert}`); + } + + for (const k of JANEIN_KRITERIEN) { + const wert = c.jaNein[k.key]; + if (wert) teile.push(`${k.label}: ${wert}`); + } + + for (const k of ZEITRAUM_KRITERIEN) { + const z = c.zeitraum[k.key]; + if (!z) continue; + if (z.von && z.bis) teile.push(`${k.label} ${fmtDate(z.von)} bis ${fmtDate(z.bis)}`); + else if (z.von) teile.push(`${k.label} ab ${fmtDate(z.von)}`); + else if (z.bis) teile.push(`${k.label} bis ${fmtDate(z.bis)}`); + } + + const { stundenVon: von, stundenBis: bis } = c; + if (von !== undefined && bis !== undefined) teile.push(`Wochenstunden ${von} bis ${bis}`); + else if (von !== undefined) teile.push(`Wochenstunden ab ${von}`); + else if (bis !== undefined) teile.push(`Wochenstunden bis ${bis}`); + + return teile; +} + /** Alle Parameternamen, die zu den Kriterien gehören — zum Zurücksetzen. */ export function kriterienParameter(): string[] { return [ diff --git a/tests/unit/report-criteria.test.ts b/tests/unit/report-criteria.test.ts index 9668e1c..4afcdb4 100644 --- a/tests/unit/report-criteria.test.ts +++ b/tests/unit/report-criteria.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { anzahlKriterien, AUSWAHL_KRITERIEN, + beschreibeKriterien, JANEIN_KRITERIEN, kriterienParameter, LEERE_CRITERIA, @@ -58,6 +59,52 @@ describe("parseCriteria", () => { }); }); +describe("beschreibeKriterien", () => { + it("sagt nichts, wenn nichts eingeschränkt ist", () => { + expect(beschreibeKriterien(LEERE_CRITERIA)).toEqual([]); + }); + + it("nennt jedes gesetzte Kriterium beim Namen", () => { + const c = aus({ employment: "Teilzeit", dienstwagen: "ja", entryFrom: "2024-01-01", hoursTo: "30" }); + expect(beschreibeKriterien(c)).toEqual([ + "Beschäftigungsart: Teilzeit", + "Dienstwagen: ja", + "Eintritt ab 01.01.2024", + "Wochenstunden bis 30", + ]); + }); + + it("unterscheidet ab, bis und den geschlossenen Zeitraum", () => { + expect(beschreibeKriterien(aus({ entryFrom: "2020-06-01" }))).toEqual(["Eintritt ab 01.06.2020"]); + expect(beschreibeKriterien(aus({ entryTo: "2020-06-01" }))).toEqual(["Eintritt bis 01.06.2020"]); + expect(beschreibeKriterien(aus({ entryFrom: "2020-01-01", entryTo: "2020-12-31" }))).toEqual([ + "Eintritt 01.01.2020 bis 31.12.2020", + ]); + }); + + it("zeigt die Beschriftung, nicht den gespeicherten Wert", () => { + // Bei der Austrittsart gehen die beiden auseinander; auf der Karte nützt + // nur das, was auch im Auswahlfeld steht. + const kriterium = AUSWAHL_KRITERIEN.find((k) => k.key === "austrittsart")!; + const option = kriterium.optionen[0]; + expect(beschreibeKriterien(aus({ austrittsart: option.wert }))).toEqual([`${kriterium.label}: ${option.label}`]); + }); + + it("beschreibt so viele Kriterien, wie anzahlKriterien zählt", () => { + // Die Zahl am Aufklapper und der Text auf der Exportkarte dürfen nicht + // auseinanderlaufen: sonst meldet die eine Stelle eine Einschränkung, die + // die andere nicht benennt. + const alle = { + ...Object.fromEntries(AUSWAHL_KRITERIEN.map((k) => [k.key, k.optionen[0].wert])), + ...Object.fromEntries(JANEIN_KRITERIEN.map((k) => [k.key, "ja"])), + ...Object.fromEntries(ZEITRAUM_KRITERIEN.map((k) => [`${k.key}From`, "2024-01-01"])), + hoursFrom: "10", + }; + const c = aus(alle); + expect(beschreibeKriterien(c)).toHaveLength(anzahlKriterien(c)); + }); +}); + describe("kriterienParameter", () => { it("nennt jeden Parameter, den parseCriteria liest", () => { // „Zurücksetzen" räumt anhand dieser Liste auf. Fehlt darin ein Name,