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", () => {