import { describe, expect, it } from "vitest"; import { buildOrg, type DivisionDef } from "@/supabase/build-org"; // Die Konstruktion des Org-Baums ist die Stelle, an der sich Elternbezüge // und Leitungsplanstellen falsch verdrahten lassen, ohne dass es auffällt: // ein Team unter dem falschen Bereich sieht im Organigramm plausibel aus. const DIVISIONS: DivisionDef[] = [ { name: "Produktion", headTitle: "Bereichsleitung Produktion", departments: [ { name: "Fertigung", leadTitle: "Abteilungsleitung Fertigung", teams: [ { name: "Montage", leadTitle: "Teamleitung Montage", icTitles: ["Monteur:in", "Anlagenführer:in"], baseSize: 3 }, { name: "CNC", leadTitle: "Teamleitung CNC", icTitles: ["CNC-Fräser:in"], baseSize: 2 }, ], }, { name: "Instandhaltung", leadTitle: "Abteilungsleitung Instandhaltung", teams: [{ name: "Mechanik", leadTitle: "Teamleitung Mechanik", icTitles: ["Schlosser:in"], baseSize: 2 }], }, ], }, { name: "IT", headTitle: "Bereichsleitung IT", departments: [ { name: "Infrastruktur", leadTitle: "Abteilungsleitung Infrastruktur", teams: [{ name: "IT-Support", leadTitle: "Teamleitung IT-Support", icTitles: ["Systemadministrator:in"], baseSize: 2 }], }, ], }, ]; function build() { let n = 0; return buildOrg("Alpenwerk Industrie GmbH", DIVISIONS, () => `id-${++n}`); } describe("Struktur", () => { it("hat genau eine Wurzel, und das ist die Gesellschaft", () => { const { units } = build(); const roots = units.filter((u) => u.parent_id === null); expect(roots).toHaveLength(1); expect(roots[0].unit_type).toBe("Gesellschaft"); }); it("hängt jede Einheit unter den richtigen Typ", () => { const { units } = build(); const byId = new Map(units.map((u) => [u.id, u])); const erwartetesElternteil = { Bereich: "Gesellschaft", Abteilung: "Bereich", Team: "Abteilung" } as const; for (const u of units) { if (u.unit_type === "Gesellschaft") continue; const parent = byId.get(u.parent_id!); expect(parent?.unit_type, `${u.name} hängt falsch`).toBe(erwartetesElternteil[u.unit_type]); } }); it("baut die Ebenen vollständig auf", () => { const { units } = build(); const zahl = (t: string) => units.filter((u) => u.unit_type === t).length; expect(zahl("Gesellschaft")).toBe(1); expect(zahl("Bereich")).toBe(2); expect(zahl("Abteilung")).toBe(3); expect(zahl("Team")).toBe(4); }); it("führt jede Einheit über parent_id auf die Wurzel zurück", () => { // Ein abgehängter Teilbaum würde im Organigramm nie gerendert. const { units } = build(); const byId = new Map(units.map((u) => [u.id, u])); for (const start of units) { const gesehen = new Set(); let cur = start; while (cur.parent_id && !gesehen.has(cur.id)) { gesehen.add(cur.id); cur = byId.get(cur.parent_id)!; } expect(cur.parent_id, `${start.name} erreicht die Wurzel nicht`).toBeNull(); } }); it("vergibt eindeutige Org-Nummern im richtigen Nummernkreis", () => { const { units } = build(); expect(new Set(units.map((u) => u.org_number)).size).toBe(units.length); const prefix = { Gesellschaft: "10", Bereich: "20", Abteilung: "21", Team: "22" } as const; for (const u of units) expect(u.org_number.startsWith(prefix[u.unit_type]), u.name).toBe(true); }); }); describe("Planstellen", () => { it("gibt jeder Einheit genau eine Leitungsplanstelle", () => { // Der Unique-Index in der Datenbank erzwingt das ebenfalls; hier soll // der Seed gar nicht erst dagegenlaufen. const { units, positions } = build(); for (const u of units) { const chiefs = positions.filter((p) => p.org_unit_id === u.id && p.is_chief); expect(chiefs, `${u.name}`).toHaveLength(1); } }); it("legt auch für die Abteilung eine Leitung an", () => { // Die Ebene, die im Altmodell gefehlt hat. const { units, positions } = build(); const abteilungen = units.filter((u) => u.unit_type === "Abteilung"); expect(abteilungen.length).toBeGreaterThan(0); for (const a of abteilungen) { expect(positions.some((p) => p.org_unit_id === a.id && p.is_chief)).toBe(true); } }); it("erzeugt für jedes Team so viele Mitarbeiter-Planstellen wie vorgesehen", () => { const { units, icPositionsByTeam } = build(); const montage = units.find((u) => u.name === "Montage")!; expect(icPositionsByTeam.get(montage.id)).toHaveLength(3); expect(icPositionsByTeam.get(montage.id)!.every((p) => !p.is_chief)).toBe(true); }); it("vergibt eindeutige Planstellennummern nach dem bestehenden Muster", () => { const { positions } = build(); expect(new Set(positions.map((p) => p.position_number)).size).toBe(positions.length); for (const p of positions) expect(p.position_number).toMatch(/^6\d{7}$/); }); it("hängt jede Planstelle an eine existierende Einheit", () => { const { units, positions } = build(); const ids = new Set(units.map((u) => u.id)); for (const p of positions) expect(ids.has(p.org_unit_id), p.position_number).toBe(true); }); }); describe("Job-Katalog", () => { it("führt jede Tätigkeit genau einmal", () => { // "Schlosser:in" darf nicht als zwei Einträge existieren, sonst ist eine // Auswertung nach Tätigkeit wertlos. const { jobs } = build(); expect(new Set(jobs.map((j) => j.title)).size).toBe(jobs.length); expect(new Set(jobs.map((j) => j.code)).size).toBe(jobs.length); }); it("teilt denselben Job über mehrere Planstellen", () => { const { positions, jobs } = build(); const jobById = new Map(jobs.map((j) => [j.id, j])); const monteur = jobs.find((j) => j.title === "Monteur:in")!; const stellen = positions.filter((p) => p.job_id === monteur.id); expect(stellen.length).toBeGreaterThan(1); expect(jobById.get(stellen[0].job_id)!.title).toBe("Monteur:in"); }); it("verweist jede Planstelle auf einen existierenden Job", () => { const { positions, jobs } = build(); const ids = new Set(jobs.map((j) => j.id)); for (const p of positions) expect(ids.has(p.job_id), p.position_number).toBe(true); }); });