divisionOf nahm "die oberste Einheit unterhalb der Gesellschaft" -- eine Aussage ueber die Stellung im Baum statt ueber unit_type. Im Altmodell fiel beides zusammen, weil unter der Gesellschaft genau die Bereiche hingen. Bei Manner liegt dort allein "CEO", darunter erst die sechs C-Level-Bereiche: die Funktion gab fuer alle 784 Personen "CEO" zurueck. In der Mitarbeiterliste stand es in jeder Zeile, auf der Uebersicht lag die ganze Belegschaft im Balken "CEO", waehrend die sechs uebrigen Bereiche auf null standen. Gesucht wird jetzt der naechste Vorfahre mit unit_type = 'Bereich', die Einheit selbst eingeschlossen. Der naechste und nicht der oberste: "CEO" ist selbst ein Bereich und liegt ueber den anderen, sonst stuende er wieder ueberall. Damit meinen Liste, Uebersicht, Sortierung (lib/employee-sort.ts) und Berichte (lib/reports-data.ts) denselben Bereich -- vorher sortierte die Liste bereits richtig, zeigte aber in der Spalte etwas anderes an.
131 lines
5.8 KiB
TypeScript
131 lines
5.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { ancestorsOf, breadcrumbLabel, buildOrgMaps, divisionOf, subtreeOf, type OrgUnit } from "@/lib/org";
|
||
|
||
// Der Baum ersetzt die drei festen Ebenen des Altmodells. Damit hängt an
|
||
// dieser Datei mehr als vorher: eine Einheit falsch verkettet, und ein Filter
|
||
// „Bereich Produktion" liefert stillschweigend zu wenig — nicht gar nichts,
|
||
// was auffallen würde.
|
||
|
||
const units: OrgUnit[] = [
|
||
{ id: "gmbh", org_number: "10000000", name: "Alpenwerk", parent_id: null, unit_type: "Gesellschaft" },
|
||
{ id: "prod", org_number: "20100000", name: "Produktion", parent_id: "gmbh", unit_type: "Bereich" },
|
||
{ id: "fert", org_number: "21100000", name: "Fertigung", parent_id: "prod", unit_type: "Abteilung" },
|
||
{ id: "mont", org_number: "22001000", name: "Montage", parent_id: "fert", unit_type: "Team" },
|
||
{ id: "cnc", org_number: "22002000", name: "CNC", parent_id: "fert", unit_type: "Team" },
|
||
{ id: "it", org_number: "20200000", name: "IT", parent_id: "gmbh", unit_type: "Bereich" },
|
||
];
|
||
const locations = [{ id: "loc-1", name: "Wien-Hernals", country: "Österreich" }];
|
||
const maps = buildOrgMaps(units, locations);
|
||
|
||
describe("buildOrgMaps", () => {
|
||
it("listet Eltern vor ihren Kindern", () => {
|
||
// Die Reihenfolge ist eine Zusage: der Filter im Mitarbeiterlisten-Select
|
||
// rückt danach ein, und ein Einfügen in die Datenbank verlässt sich darauf.
|
||
const position = new Map(maps.unitList.map((u, i) => [u.id, i]));
|
||
for (const u of maps.unitList) {
|
||
if (!u.parent_id) continue;
|
||
expect(position.get(u.parent_id)!, `${u.name} steht vor ${u.parent_id}`).toBeLessThan(position.get(u.id)!);
|
||
}
|
||
});
|
||
|
||
it("misst die Tiefe ab der Wurzel", () => {
|
||
expect(maps.depthOf.get("gmbh")).toBe(0);
|
||
expect(maps.depthOf.get("prod")).toBe(1);
|
||
expect(maps.depthOf.get("mont")).toBe(3);
|
||
});
|
||
});
|
||
|
||
describe("ancestorsOf", () => {
|
||
it("gibt die Kette von der Wurzel bis zur Einheit selbst", () => {
|
||
expect(ancestorsOf(maps, "mont").map((u) => u.name)).toEqual(["Alpenwerk", "Produktion", "Fertigung", "Montage"]);
|
||
});
|
||
|
||
it("bricht bei einem Ring ab, statt endlos zu laufen", () => {
|
||
// parent_id ist eine gewöhnliche Spalte; ein fehlerhafter Import kann
|
||
// einen Ring erzeugen, und jede Auswertung hier ist rekursiv.
|
||
const ringMaps = buildOrgMaps(
|
||
[
|
||
{ id: "a", org_number: "1", name: "A", parent_id: "b", unit_type: "Bereich" },
|
||
{ id: "b", org_number: "2", name: "B", parent_id: "a", unit_type: "Bereich" },
|
||
],
|
||
[]
|
||
);
|
||
expect(ancestorsOf(ringMaps, "a")).toHaveLength(2);
|
||
});
|
||
|
||
it("liefert nichts für eine unbekannte Einheit", () => {
|
||
expect(ancestorsOf(maps, "gibtesnicht")).toEqual([]);
|
||
expect(ancestorsOf(maps, null)).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe("subtreeOf", () => {
|
||
it("schliesst die Einheit selbst und alles darunter ein", () => {
|
||
// Genau das meint der Filter „Bereich Produktion": im Bereich selbst
|
||
// sitzt nur die Bereichsleitung, alle anderen hängen tiefer.
|
||
expect(new Set(subtreeOf(maps, "prod"))).toEqual(new Set(["prod", "fert", "mont", "cnc"]));
|
||
});
|
||
|
||
it("ist für ein Blatt die Einheit allein", () => {
|
||
expect(subtreeOf(maps, "mont")).toEqual(["mont"]);
|
||
});
|
||
});
|
||
|
||
describe("divisionOf", () => {
|
||
it("findet den Bereich einer tief hängenden Einheit", () => {
|
||
expect(divisionOf(maps, "mont")?.name).toBe("Produktion");
|
||
});
|
||
|
||
it("gibt für eine Bereichsleitung ihren eigenen Bereich", () => {
|
||
expect(divisionOf(maps, "prod")?.name).toBe("Produktion");
|
||
});
|
||
|
||
it("hat für die Gesellschaft selbst keinen Bereich", () => {
|
||
// Die Geschäftsführung sitzt über allen Bereichen, nicht in einem.
|
||
expect(divisionOf(maps, "gmbh")).toBeUndefined();
|
||
});
|
||
|
||
// Der Fall, an dem die frühere Fassung scheiterte: sie nahm die oberste
|
||
// Einheit unterhalb der Gesellschaft. Liegt dort eine weitere Ebene — bei
|
||
// Manner „CEO" über den sechs C-Level-Bereichen —, gab sie für **jede**
|
||
// Person dieselbe Einheit zurück. In der Mitarbeiterliste stand „CEO" in
|
||
// jeder Zeile, auf der Übersicht lag die ganze Belegschaft in einem Balken.
|
||
describe("wenn über den Bereichen noch eine Einheit liegt", () => {
|
||
const tief: OrgUnit[] = [
|
||
{ id: "ag", org_number: "0000", name: "Manner AG", parent_id: null, unit_type: "Gesellschaft" },
|
||
{ id: "ceo", org_number: "0080", name: "CEO", parent_id: "ag", unit_type: "Bereich" },
|
||
{ id: "coo", org_number: "0083", name: "COO", parent_id: "ceo", unit_type: "Bereich" },
|
||
{ id: "w17", org_number: "0088", name: "Standortleitung W17", parent_id: "coo", unit_type: "Abteilung" },
|
||
{ id: "schnitte", org_number: "0101", name: "Leitung Schnitte", parent_id: "w17", unit_type: "Team" },
|
||
{ id: "tl", org_number: "0150", name: "Teamleitung A", parent_id: "schnitte", unit_type: "Team" },
|
||
];
|
||
const tiefeMaps = buildOrgMaps(tief, locations);
|
||
|
||
it("nimmt den nächsten Bereich, nicht den obersten", () => {
|
||
expect(divisionOf(tiefeMaps, "tl")?.name).toBe("COO");
|
||
expect(divisionOf(tiefeMaps, "schnitte")?.name).toBe("COO");
|
||
expect(divisionOf(tiefeMaps, "w17")?.name).toBe("COO");
|
||
});
|
||
|
||
it("lässt die oberste Einheit ihren eigenen Bereich behalten", () => {
|
||
// Wer direkt am CEO sitzt, gehört dorthin — und nur diese Personen.
|
||
expect(divisionOf(tiefeMaps, "ceo")?.name).toBe("CEO");
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("breadcrumbLabel", () => {
|
||
it("lässt die Gesellschaft weg, die in jeder Zeile gleich wäre", () => {
|
||
expect(breadcrumbLabel(maps, "mont")).toBe("Produktion › Fertigung › Montage");
|
||
});
|
||
|
||
it("endet bei der Einheit, an der die Person tatsächlich hängt", () => {
|
||
expect(breadcrumbLabel(maps, "prod")).toBe("Produktion");
|
||
});
|
||
|
||
it("fällt auf einen Strich zurück, wenn nichts auflösbar ist", () => {
|
||
expect(breadcrumbLabel(maps, null)).toBe("–");
|
||
expect(breadcrumbLabel(maps, "gmbh")).toBe("–");
|
||
});
|
||
});
|