Eine Organisationseinheit aus dem Organigramm heraus anlegen
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m30s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m22s

Bisher gab es dafuer nur den Import. Jetzt sitzt auf jeder Einheit im
Organigramm -- in der Grafik wie in der Liste -- ein Plus, das eine
untergeordnete Einheit anlegt, wahlweise gleich mit Leitungsplanstelle.

Bewusst nur dieser eine Fall. Umbenennen, verschieben und schliessen fehlen
nicht aus Zeitmangel: org_units.parent_id traegt kein Datum, ein Verschieben
aenderte damit auch jede Auswertung auf einen vergangenen Stichtag, und der
fruehere Stand waere danach nirgends mehr ablesbar. Anlegen stellt diese Frage
nicht, weil vorher nichts da war -- und es kann dabei auch kein Kreis
entstehen, was hier mehr wiegt als es klingt: die rekursive Abfrage in
om_reporting_lines hat weder Tiefenbegrenzung noch Kreiserkennung.

Die Leitungsplanstelle entsteht ueber create_position statt durch eine zweite
Fassung derselben Logik -- dort haengen Jobkatalog, Nummernvergabe und die
Pruefung "je Einheit genau eine Leitung".

Die Orgnummer wird eingetragen, nicht vergeben. Vorgeschlagen wird die naechste
freie Nummer der bestehenden Reihe, und nur dann, wenn sich im Bestand genau
eine Systematik ablesen laesst -- eine plausibel aussehende, aber erfundene
Nummer prueft niemand nach.

Das Datum kommt nicht aus dem Stichtag der Ansicht. Wer sich die Struktur zum
letzten Jahresende ansieht und auf Plus drueckt, will in aller Regel eine
Einheit von heute anlegen.
This commit is contained in:
2026-09-28 16:28:33 +02:00
parent 8b7e32f14a
commit a6b6a7d67c
11 changed files with 547 additions and 9 deletions

View File

@@ -0,0 +1,71 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { buildUnitTree, PositionTree } from "@/components/orgchart/PositionTree";
import type { OrgEmployee, OrgUnitNode, OrgVacancy } from "@/components/orgchart/types";
vi.mock("@/actions/org", () => ({ createOrgUnit: vi.fn(async () => ({ success: true })) }));
vi.mock("next/navigation", () => ({ useRouter: () => ({ refresh: vi.fn(), push: vi.fn() }) }));
// 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();
});
});

View File

@@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import { naechsteOrgnummer } from "@/lib/org-nummer";
// Der Vorschlag darf lieber schweigen als raten: eine erfundene, aber
// plausibel aussehende Nummer prüft niemand nach, und sie landet im führenden
// System des Kunden als Kennung, die es dort nicht gibt.
describe("naechsteOrgnummer", () => {
it("zählt die höchste Nummer hoch und behält die Breite", () => {
expect(naechsteOrgnummer(["OE-0001", "OE-0013", "OE-0080"])).toBe("OE-0081");
});
it("richtet sich nach der höchsten, nicht nach der letzten in der Liste", () => {
expect(naechsteOrgnummer(["OE-0080", "OE-0001", "OE-0013"])).toBe("OE-0081");
});
it("kommt auch ohne Vorspann aus", () => {
expect(naechsteOrgnummer(["10000001", "10000002"])).toBe("10000003");
});
it("verlängert, statt eine vergebene Nummer vorzuschlagen", () => {
// 99 + 1 passt nicht mehr in zwei Stellen. Abgeschnitten käme „00“
// heraus, und das gibt es bereits.
expect(naechsteOrgnummer(["OE-98", "OE-99"])).toBe("OE-100");
});
it("schweigt bei zwei Systematiken nebeneinander", () => {
expect(naechsteOrgnummer(["OE-0001", "B7"])).toBeUndefined();
});
it("schweigt, wenn eine Nummer gar nicht auf Ziffern endet", () => {
expect(naechsteOrgnummer(["OE-0001", "Vertrieb"])).toBeUndefined();
});
it("schweigt im leeren Bestand", () => {
expect(naechsteOrgnummer([])).toBeUndefined();
});
});