From 3664b9a435aef4fdb19a74f872d1369728c1450b Mon Sep 17 00:00:00 2001 From: Andrei Laas Date: Tue, 29 Sep 2026 18:26:38 +0200 Subject: [PATCH] Neuer Slogan, und Hay-Grades stehen als Leiter statt nach Haeufigkeit Auf der Anmeldeseite steht jetzt "Alles im Blick. Alles Manner." statt des bisherigen Satzes. Die Berichte sortieren Gruppen nach der Kennzahl, gross zuerst. Bei Bereich oder Standort ist das die Antwort auf die Frage, die der Bericht stellt. Bei einer Leiter ist es keine: HG09 vor HG10 ist die Reihenfolge, in der die Werte sind, und nach Haeufigkeit umgestellt liest sie sich als Zufall. Die Sonderbehandlung, die es fuer Wochentage schon gab, ist dafuer zu einer Liste "Dimensionen mit eigener Reihenfolge" verallgemeinert -- Wochentage und Hay-Grades stehen darin, und die naechste Leiter ist ein Eintrag statt einer dritten Verzweigung. Gilt damit auch fuer die Aufschluesselung innerhalb einer Gruppe und fuer die Spalten im Berichtsexport, die dieselbe Funktion nutzen. Der Generic Grade steht vor HG09: er ist keine Stufe, sondern ihr Fehlen, und vor der niedrigsten faellt das am wenigsten als Aussage auf. --- app/(auth)/login/page.tsx | 2 +- lib/reports.ts | 42 ++++++++++++++++++++++++++++++-------- tests/unit/reports.test.ts | 24 ++++++++++++++++++++++ 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 937a8d0..6b6745b 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -157,7 +157,7 @@ function BrandPanel() {

- Die Organisation, so wie sie heute wirklich aussieht. + Alles im Blick. Alles Manner.

Stammdaten, Planstellen und Berichtslinien der Alpenwerk Industrie GmbH — jederzeit auch zu einem beliebigen diff --git a/lib/reports.ts b/lib/reports.ts index 3ca4f24..1cda040 100644 --- a/lib/reports.ts +++ b/lib/reports.ts @@ -1,6 +1,7 @@ import { ABSENCE_TYPES } from "./absence"; import { BEENDIGUNGSART_WERTE } from "./beendigung"; import { fmtName, todayIso, yearsBetweenIso } from "./format"; +import { HAY_GRADE_WERTE } from "./hay-grade"; import { WOCHENTAGE } from "./wochentage"; import type { EmploymentStatus, HistoryEventType, Weekday } from "./types"; @@ -235,20 +236,43 @@ export function groupKeyFor(e: ReportEmployee, dim: GroupDimension, lookups: Org } } -function weekdayRank(key: string): number { - const i = WOCHENTAGE.indexOf(key as Weekday); - return i === -1 ? WOCHENTAGE.length : i; +/** + * Dimensionen, deren Werte eine eigene Reihenfolge haben. + * + * Alles andere wird nach der Kennzahl sortiert, gross zuerst — bei „Bereich" + * oder „Standort" ist das die Antwort auf die Frage, die der Bericht stellt. + * Bei einer Leiter ist es keine: Montag vor Dienstag und HG09 vor HG10 sind + * die Reihenfolge, in der die Werte *sind*, und eine nach Häufigkeit + * umgestellte Leiter liest sich als Zufall. + * + * HG09 ist die unterste Stufe, HG20 die oberste. Der „Generic Grade" (`-`) + * steht vor allen: er ist keine Stufe, sondern ihr Fehlen, und vor der + * niedrigsten ist der Platz, an dem das am wenigsten nach einer Aussage + * aussieht. + */ +const EIGENE_REIHENFOLGE: Partial> = { + weekday: WOCHENTAGE, + paygrade: HAY_GRADE_WERTE, +}; + +function rang(dim: GroupDimension, key: string): number { + const liste = EIGENE_REIHENFOLGE[dim]; + if (!liste) return 0; + const i = liste.indexOf(key); + // Unbekanntes hinten, nicht vorn: ein Wert, den die Liste nicht kennt, soll + // auffallen und nicht die Leiter anführen. + return i === -1 ? liste.length : i; } -function sortByWeekday(items: T[]): T[] { - return [...items].sort((a, b) => weekdayRank(a.key) - weekdayRank(b.key)); +function sortiereNachReihenfolge(items: T[], dim: GroupDimension): T[] { + return [...items].sort((a, b) => rang(dim, a.key) - rang(dim, b.key)); } // Reused by ReportsPageClient (split legend) and the report export route -// (split columns) to render a `weekday` split chronologically rather than +// (split columns) to render a split in the order its values have rather than // in first-encountered order; a no-op for every other dimension. export function sortKeysForDimension(keys: string[], dim: GroupDimension): string[] { - return dim === "weekday" ? [...keys].sort((a, b) => weekdayRank(a) - weekdayRank(b)) : keys; + return EIGENE_REIHENFOLGE[dim] ? [...keys].sort((a, b) => rang(dim, a) - rang(dim, b)) : keys; } // Every dimension other than `weekday` is a strict single-key partition @@ -326,11 +350,11 @@ export function aggregateReport( value: measureValue(sRows, measure, asOf), count: sRows.length, })); - row.split = split === "weekday" ? sortByWeekday(splitRows) : splitRows; + row.split = EIGENE_REIHENFOLGE[split] ? sortiereNachReihenfolge(splitRows, split) : splitRows; } rows.push(row); } - return group === "weekday" ? sortByWeekday(rows) : rows.sort((a, b) => b.value - a.value); + return EIGENE_REIHENFOLGE[group] ? sortiereNachReihenfolge(rows, group) : rows.sort((a, b) => b.value - a.value); } export function sumValues(rows: { value: number }[]): number { diff --git a/tests/unit/reports.test.ts b/tests/unit/reports.test.ts index 6421e1a..746e077 100644 --- a/tests/unit/reports.test.ts +++ b/tests/unit/reports.test.ts @@ -251,6 +251,30 @@ describe("aggregateReport", () => { const rows = aggregateReport(employees, "headcount", "division", "weekday", lookups); expect(rows[0].split?.map((s) => s.key)).toEqual(["Mo", "Fr"]); }); + + it("sorts Hay-Grades aufsteigend statt nach Häufigkeit", () => { + // Die Leiter ist die Aussage: HG09 unten, HG20 oben. Nach Kennzahl + // sortiert stünde hier HG13 vorn, weil dort zufällig zwei Personen + // sitzen — und die Reihenfolge läse sich als Zufall. + const employees = [ + emp({ id: "1", paygrade: "HG20" }), + emp({ id: "2", paygrade: "HG09" }), + emp({ id: "3", paygrade: "HG13" }), + emp({ id: "4", paygrade: "HG13" }), + emp({ id: "5", paygrade: "HG19P" }), + emp({ id: "6", paygrade: "HG19" }), + ]; + const rows = aggregateReport(employees, "headcount", "paygrade", null, lookups); + expect(rows.map((r) => r.key)).toEqual(["HG09", "HG13", "HG19", "HG19P", "HG20"]); + }); + + it("stellt den Generic Grade vor die unterste Stufe", () => { + // Er ist keine Stufe, sondern ihr Fehlen. Zwischen HG13 und HG14 stünde + // er wie eine. + const employees = [emp({ id: "1", paygrade: "HG13" }), emp({ id: "2", paygrade: "-" })]; + const rows = aggregateReport(employees, "headcount", "paygrade", null, lookups); + expect(rows.map((r) => r.key)).toEqual(["-", "HG13"]); + }); }); describe("deriveStatusAsOf", () => {