Die Kostenstelle liess sich an genau einer Stelle aendern: auf der Seite Positionen, ueber die Karte einer Vakanz. Diese Seite zeigt aber nur unbesetzte und kuenftige Planstellen. Bei einer besetzten war die Kontierung damit nirgends zu erreichen -- im Bestand betrifft das 784 von 788, und dasselbe galt fuer ihre Taetigkeit. Die Angaben zu einer Einheit fuehren jetzt ihre Planstellen auf, besetzte eingeschlossen, mit Inhaber:in und Kostenstelle. Ein Klick oeffnet denselben Dialog wie auf Positionen, kein zweiter daneben. Was sich aendern laesst, entscheidet weiterhin die Datenbank: bei einer besetzten Stelle bleiben Einheit und Gueltigkeitsende gesperrt, weil ein Abteilungswechsel ueber eine Versetzung gehoert und nicht ueber die Stelle. Geladen wird erst beim Oeffnen und nur fuer diese eine Einheit. Alle 788 Planstellen in jede Zeichnung des Organigramms zu legen hiesse, fuer achtzig Einheiten zu laden, was man fuer eine braucht. EditPositionModal verlangt dafuer nicht mehr eine OpenPositionResolved, sondern nur noch die Felder, die er tatsaechlich liest. Die alte Signatur war der Grund, warum er nur dort aufzumachen war, wo unbesetzte Stellen entstehen.
117 lines
5.2 KiB
TypeScript
117 lines
5.2 KiB
TypeScript
import { render as rtlRender, screen, within } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { buildUnitTree, PositionTree } from "@/components/orgchart/PositionTree";
|
|
import type { OrgEmployee, OrgUnitNode, OrgVacancy } from "@/components/orgchart/types";
|
|
import { ToastProvider } from "@/components/ui/Toast";
|
|
|
|
vi.mock("@/actions/org", () => ({
|
|
createOrgUnit: vi.fn(async () => ({ success: true })),
|
|
deleteOrgUnit: vi.fn(async () => ({ success: true })),
|
|
}));
|
|
// Die Angaben zu einer Einheit laden ihre Planstellen nach; das Aktionsmodul
|
|
// zieht über lib/db das Paket `server-only` nach und ist im Test ein
|
|
// Importfehler. Geprüft wird hier, was die Ansicht anbietet, nicht was die
|
|
// Datenbank liefert.
|
|
vi.mock("@/actions/positions", () => ({
|
|
ladePlanstellenDerEinheit: vi.fn(async () => ({ planstellen: [], kostenstellen: [], heute: "2026-09-29" })),
|
|
updatePosition: vi.fn(async () => ({ success: true })),
|
|
setPositionCostCenter: vi.fn(async () => ({ success: true })),
|
|
}));
|
|
vi.mock("next/navigation", () => ({ useRouter: () => ({ refresh: vi.fn(), push: vi.fn() }) }));
|
|
|
|
function render(ui: React.ReactElement) {
|
|
return rtlRender(<ToastProvider>{ui}</ToastProvider>);
|
|
}
|
|
|
|
// Der Knopf „Einheit darunter anlegen" hängt allein daran, dass ein Knoten
|
|
// eine `unitId` trägt. Verschwindet die still — etwa weil jemand den Baum
|
|
// umbaut —, bleibt die Oberfläche fehlerfrei und die Handlung ist weg.
|
|
|
|
const UNITS: OrgUnitNode[] = [
|
|
{ id: "u1", org_number: "OE-0001", name: "Gesellschaft", parent_id: null, unit_type: "Gesellschaft" },
|
|
{ id: "u2", org_number: "OE-0010", name: "Produktion", parent_id: "u1", unit_type: "Bereich" },
|
|
];
|
|
|
|
const EMPLOYEES: OrgEmployee[] = [
|
|
{
|
|
id: "p1",
|
|
personnel_number: 1,
|
|
first_name: "Anna",
|
|
last_name: "Huber",
|
|
job_title: "Maschinenbedienung",
|
|
manager_id: null,
|
|
formal_manager_id: null,
|
|
absent: false,
|
|
absence_type: null,
|
|
org_unit_id: "u2",
|
|
is_chief: false,
|
|
position_id: "s1",
|
|
position_number: "S-0001",
|
|
},
|
|
];
|
|
|
|
const VACANCIES: OrgVacancy[] = [
|
|
{ position_id: "s2", position_number: "S-0002", job_title: "Schlosser:in", org_unit_id: "u2", is_chief: false },
|
|
];
|
|
|
|
describe("buildUnitTree", () => {
|
|
const baum = buildUnitTree(UNITS, EMPLOYEES, VACANCIES);
|
|
|
|
it("trägt die Einheit an jedem Einheitenknoten", () => {
|
|
expect(baum[0].unitId).toBe("u1");
|
|
expect(baum[0].children[0].unitId).toBe("u2");
|
|
});
|
|
|
|
it("trägt sie an nichts anderem", () => {
|
|
// Eine Person, eine Tätigkeitsgruppe oder eine Vakanz ist keine Einheit —
|
|
// ein Plus daran würde etwas anlegen, das woanders hinge.
|
|
const bereich = baum[0].children[0];
|
|
for (const kind of bereich.children.filter((k) => k.kind !== "role")) {
|
|
expect(kind.unitId, `„${kind.label}" darf keine Einheit tragen`).toBeUndefined();
|
|
for (const enkel of kind.children) expect(enkel.unitId).toBeUndefined();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("PositionTree", () => {
|
|
it("bietet je Einheit einen Knopf zum Anlegen an", async () => {
|
|
render(<PositionTree employees={EMPLOYEES} units={UNITS} vacancies={VACANCIES} />);
|
|
// Die Wurzel ist aufgeklappt; ihr Kind ist damit sichtbar.
|
|
expect(await screen.findByRole("button", { name: /Einheit unter .*Gesellschaft anlegen/ })).toBeInTheDocument();
|
|
expect(screen.getByRole("button", { name: /Einheit unter .*Produktion anlegen/ })).toBeInTheDocument();
|
|
});
|
|
|
|
it("bietet ihn nicht an einer Person an", () => {
|
|
render(<PositionTree employees={EMPLOYEES} units={UNITS} vacancies={VACANCIES} />);
|
|
expect(screen.queryByRole("button", { name: /Einheit unter Huber/ })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("bietet den Papierkorb nur an, wo nichts darunter hängt", () => {
|
|
// „Produktion" trägt eine Person und eine Vakanz, die Gesellschaft trägt
|
|
// „Produktion" — entfernbar ist keine von beiden. Ein Knopf, der beim
|
|
// Klick eine Absage erteilt, ist schlechter als kein Knopf.
|
|
render(<PositionTree employees={EMPLOYEES} units={UNITS} vacancies={VACANCIES} />);
|
|
expect(screen.queryByRole("button", { name: /Einheit .* entfernen/ })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("bietet ihn an einer leeren Einheit an", () => {
|
|
const leer: OrgUnitNode = { id: "u3", org_number: "OE-0411", name: "Neu", parent_id: "u1", unit_type: "Bereich" };
|
|
render(<PositionTree employees={EMPLOYEES} units={[...UNITS, leer]} vacancies={VACANCIES} />);
|
|
expect(screen.getByRole("button", { name: /Einheit .*OE-0411.* entfernen/ })).toBeInTheDocument();
|
|
});
|
|
|
|
it("öffnet beim Klick auf eine Einheit deren Angaben", async () => {
|
|
// Der Klick auf die Karte war lange ohne Wirkung — die naheliegendste
|
|
// Geste in dieser Ansicht, und sie antwortete nicht.
|
|
render(<PositionTree employees={EMPLOYEES} units={UNITS} vacancies={VACANCIES} />);
|
|
await userEvent.click(screen.getByRole("button", { name: /Angaben zu .*Produktion/ }));
|
|
|
|
const panel = await screen.findByRole("dialog");
|
|
expect(within(panel).getByText("OE-0010 · Bereich")).toBeInTheDocument();
|
|
// Eine Person, keine Leitung, eine offene Planstelle — genau der Bestand
|
|
// aus den Fixtures.
|
|
expect(within(panel).getByText("vakant")).toBeInTheDocument();
|
|
});
|
|
});
|