feat(mitarbeiter): Vorgesetzter auch fuer Personen ohne Verzeichnis (#43)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

This commit was merged in pull request #43.
This commit is contained in:
2026-10-04 15:10:34 +00:00
parent 0fa41b9e08
commit 0060a55387
19 changed files with 683 additions and 49 deletions
+30
View File
@@ -408,6 +408,36 @@ describe.runIf(datenbankVerfuegbar && ldapDa)("Abgleich gegen das Verzeichnis",
expect(await testPrisma.mitarbeiter.count()).toBe(FIXTURE_PERSONEN + 1);
});
it("loescht eine von Hand gesetzte Vorgesetzten-Zuordnung nicht", async () => {
// `kberger` traegt im Verzeichnis keinen `manager` — mhuber und jweiss tun
// es. Wer die Zuordnung fuer kberger von Hand setzt, pflegt etwas, das das
// Verzeichnis nicht fuehrt; also hat es sie auch nicht zu loeschen (#020).
const { admin } = await grundbestand();
const quelle = await quelleFuerContainer(admin);
await abgleichAusfuehren(quelle.id, admin);
const ohneChef = await testPrisma.mitarbeiter.findFirstOrThrow({ where: { sam: "kberger" } });
const vonHand = await testPrisma.mitarbeiter.findFirstOrThrow({ where: { sam: "jweiss" } });
expect(ohneChef.vorgesetzterExterneId).toBeNull();
expect(ohneChef.vorgesetzterId).toBeNull();
await testPrisma.mitarbeiter.update({
where: { id: ohneChef.id },
data: { vorgesetzterId: vonHand.id },
});
const zweiter = await abgleichAusfuehren(quelle.id, admin);
expect(zweiter.status).toBe("ERFOLG");
const nachher = await testPrisma.mitarbeiter.findFirstOrThrow({ where: { id: ohneChef.id } });
expect(nachher.vorgesetzterId).toBe(vonHand.id);
// Gegenstueck: wo das Verzeichnis einen `manager` fuehrt, bestimmt es — und
// zwar in **beiden** Feldern, dem rohen Wert und der Zuordnung.
const mitChef = await testPrisma.mitarbeiter.findFirstOrThrow({ where: { sam: "mhuber" } });
expect(mitChef.vorgesetzterId).toBe(ohneChef.id);
expect(mitChef.vorgesetzterExterneId).toBe(ohneChef.externeId);
});
it("setzt ein per userAccountControl deaktiviertes Konto auf aktiv = false", async () => {
const { admin } = await grundbestand();
const quelle = await quelleFuerContainer(admin);
+2
View File
@@ -31,6 +31,7 @@ describe.runIf(datenbankVerfuegbar)("Handgeschriebene Datenbankobjekte", () => {
"Lauf_abteilung_gleicher_kunde",
"Lauf_geraet_gleicher_kunde",
"Lauf_mitarbeiter_gleicher_kunde",
"Mitarbeiter_vorgesetzter_gleicher_kunde",
]);
});
@@ -83,6 +84,7 @@ describe.runIf(datenbankVerfuegbar)("Handgeschriebene Datenbankobjekte", () => {
"Lauf_archiviert_mit_zeitpunkt",
"Lauf_begonnen_mit_zeitpunkt",
"Lauf_mitarbeiter_eindeutig",
"Mitarbeiter_vorgesetzter_nicht_selbst",
"Schrittergebnis_begruendung_noetig",
"TagCodeVergabe_form",
"Verzeichnisquelle_bindpasswort_verschluesselt",
+15
View File
@@ -333,6 +333,21 @@ describe.runIf(datenbankVerfuegbar)("Datenmodell: Regeln in der Datenbank", () =
).rejects.toThrow(/Vorlage_seedSchluessel_einmalig/);
});
it("laesst niemanden sein eigener Vorgesetzter sein", async () => {
// Die kuerzeste Schleife, und die einzige, die sich ohne Rekursion
// abfangen laesst. Laengere haelt der Dienst auf (#020).
const kunde = await kundeAnlegen("VORG");
const person = await testPrisma.mitarbeiter.create({
data: { kundeId: kunde.id, vorname: "Maria", nachname: "Huber", anzeigename: "Maria Huber" },
});
await expect(
testPrisma.mitarbeiter.update({
where: { id: person.id },
data: { vorgesetzterId: person.id },
}),
).rejects.toThrow(/Mitarbeiter_vorgesetzter_nicht_selbst/);
});
it("laesst beliebig viele Vorlagen ohne Seed-Schluessel zu", async () => {
// Der Index ist partiell, sonst waere die zweite eigene Vorlage eine
// Dublette von null (#021).
+115
View File
@@ -6,12 +6,15 @@ import {
mitarbeiterAktivSetzen,
mitarbeiterAnlegen,
mitarbeiterBearbeiten,
vorgesetztenSetzen,
} from "@/server/services/mitarbeiter";
import {
abteilungsNamen,
geraeteDesMitarbeiters,
mitarbeiterHolen,
mitarbeiterListe,
moeglicheVorgesetzte,
vorgesetzterVon,
} from "@/server/queries/mitarbeiter";
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
@@ -40,6 +43,118 @@ describe.runIf(datenbankVerfuegbar)("Mitarbeiter", () => {
await datenbankLeeren();
});
describe("Vorgesetzter", () => {
it("laesst sich fuer eine von Hand angelegte Person setzen", async () => {
// Der Fall, der vorher gar nicht ging: ohne Verzeichnisquelle gab es kein
// Feld fuer die Zuordnung (#020).
const { admin } = await grundbestand();
const maria = await mitarbeiterAnlegen({ vorname: "Maria", nachname: "Huber" }, admin);
const petra = await mitarbeiterAnlegen({ vorname: "Petra", nachname: "Klein" }, admin);
expect(maria.herkunft).toBe(Herkunft.MANUELL);
await vorgesetztenSetzen(maria.id, petra.id, admin);
expect(await vorgesetzterVon(maria.id, admin)).toEqual({
id: petra.id,
anzeigename: "Petra Klein",
});
});
it("entfernt die Zuordnung bei leerer Angabe", async () => {
const { admin } = await grundbestand();
const maria = await mitarbeiterAnlegen({ vorname: "Maria", nachname: "Huber" }, admin);
const petra = await mitarbeiterAnlegen({ vorname: "Petra", nachname: "Klein" }, admin);
await vorgesetztenSetzen(maria.id, petra.id, admin);
await vorgesetztenSetzen(maria.id, "", admin);
expect(await vorgesetzterVon(maria.id, admin)).toBeUndefined();
});
it("laesst niemanden sein eigener Vorgesetzter sein", async () => {
const { admin } = await grundbestand();
const maria = await mitarbeiterAnlegen({ vorname: "Maria", nachname: "Huber" }, admin);
await expect(vorgesetztenSetzen(maria.id, maria.id, admin)).rejects.toThrow(/eigener Vorgesetzter/);
});
it("weist eine Schleife ueber zwei Personen ab", async () => {
const { admin } = await grundbestand();
const a = await mitarbeiterAnlegen({ vorname: "A", nachname: "Eins" }, admin);
const b = await mitarbeiterAnlegen({ vorname: "B", nachname: "Zwei" }, admin);
await vorgesetztenSetzen(b.id, a.id, admin);
await expect(vorgesetztenSetzen(a.id, b.id, admin)).rejects.toThrow(/Schleife/);
});
it("weist eine Schleife ueber drei Personen ab", async () => {
// Die Datenbank faengt nur die kuerzeste ab; diese braucht den Gang nach
// oben im Dienst.
const { admin } = await grundbestand();
const a = await mitarbeiterAnlegen({ vorname: "A", nachname: "Eins" }, admin);
const b = await mitarbeiterAnlegen({ vorname: "B", nachname: "Zwei" }, admin);
const c = await mitarbeiterAnlegen({ vorname: "C", nachname: "Drei" }, admin);
await vorgesetztenSetzen(b.id, a.id, admin);
await vorgesetztenSetzen(c.id, b.id, admin);
await expect(vorgesetztenSetzen(a.id, c.id, admin)).rejects.toThrow(/Schleife/);
});
it("gilt nur innerhalb desselben Kunden", async () => {
const { eigene, admin } = await grundbestand();
const fremd = await testPrisma.kunde.create({ data: { name: "Fremd GmbH", kuerzel: "FREMD" } });
const hier = await testPrisma.mitarbeiter.create({
data: { kundeId: eigene.id, vorname: "Maria", nachname: "Huber", anzeigename: "Maria Huber" },
});
const dort = await testPrisma.mitarbeiter.create({
data: { kundeId: fremd.id, vorname: "Petra", nachname: "Klein", anzeigename: "Petra Klein" },
});
await expect(vorgesetztenSetzen(hier.id, dort.id, admin)).rejects.toThrow(/selben Kunden/);
// Und auch an der Service-Schicht vorbei nicht: der zusammengesetzte
// Fremdschluessel haelt dieselbe Grenze.
await expect(
testPrisma.mitarbeiter.update({
where: { id: hier.id },
data: { vorgesetzterId: dort.id },
}),
).rejects.toThrow(/Mitarbeiter_vorgesetzter_gleicher_kunde/);
});
it("bietet zur Auswahl nur aktive Kollegen desselben Kunden, ohne die Person selbst", async () => {
const { eigene, admin } = await grundbestand();
const fremd = await testPrisma.kunde.create({ data: { name: "Fremd GmbH", kuerzel: "FREMD" } });
const maria = await mitarbeiterAnlegen({ vorname: "Maria", nachname: "Huber" }, admin);
const petra = await mitarbeiterAnlegen({ vorname: "Petra", nachname: "Klein" }, admin);
const weg = await mitarbeiterAnlegen({ vorname: "Alt", nachname: "Kollege" }, admin);
await mitarbeiterAktivSetzen(weg.id, false, admin);
await testPrisma.mitarbeiter.create({
data: { kundeId: fremd.id, vorname: "Fremd", nachname: "Person", anzeigename: "Fremd Person" },
});
expect(eigene.id).toBe(maria.kundeId);
const auswahl = await moeglicheVorgesetzte(maria.id, admin);
expect(auswahl.map((eintrag) => eintrag.id)).toEqual([petra.id]);
});
it("schreibt die Aenderung ins Protokoll, mit Namen", async () => {
const { admin } = await grundbestand();
const maria = await mitarbeiterAnlegen({ vorname: "Maria", nachname: "Huber" }, admin);
const petra = await mitarbeiterAnlegen({ vorname: "Petra", nachname: "Klein" }, admin);
await vorgesetztenSetzen(maria.id, petra.id, admin);
await vorgesetztenSetzen(maria.id, null, admin);
const eintraege = await testPrisma.protokoll.findMany({
where: { entitaet: "Mitarbeiter", entitaetId: maria.id },
orderBy: { erstelltAm: "asc" },
select: { aktion: true },
});
// Das Anlegen protokolliert selbst - hier zaehlt, was danach kam.
expect(eintraege.map((e) => e.aktion).slice(1)).toEqual([
"Vorgesetzter auf Petra Klein gesetzt",
"Vorgesetzter entfernt",
]);
});
});
describe("Anlegen", () => {
it("bildet den Anzeigenamen aus Vor- und Nachname, wenn keiner angegeben ist", async () => {
const { admin } = await grundbestand();
+49
View File
@@ -70,7 +70,10 @@ async function aufbau(modus: Betriebsmodus = Betriebsmodus.INTERN) {
abteilung: "Buchhaltung",
titel: "Sachbearbeiterin",
dn: DN,
// Beides, so wie der Abgleich es schreibt: der rohe Wert des
// Verzeichnisses und die gepflegte Zuordnung, aus der gelesen wird (#020).
vorgesetzterExterneId: "guid-chefin",
vorgesetzterId: chefin.id,
herkunft: Herkunft.ABGLEICH,
},
});
@@ -170,6 +173,52 @@ describe.runIf(datenbankVerfuegbar)("Werte eines Laufs", () => {
});
});
it("löst den Vorgesetzten auch für eine von Hand angelegte Person auf", async () => {
// Der Fall aus #020: ohne Verzeichnisquelle gab es vorher kein Feld fuer die
// Zuordnung, und {{vorgesetzter_sam}} blieb im Onboarding-Befehl leer -
// genau dort, wo er gebraucht wird.
const { eigene, versionId } = await aufbau();
const chefin = await testPrisma.mitarbeiter.create({
data: {
kundeId: eigene.id,
vorname: "Petra",
nachname: "Klein",
anzeigename: "Petra Klein",
sam: "pklein",
herkunft: Herkunft.MANUELL,
},
});
const vonHand = await testPrisma.mitarbeiter.create({
data: {
kundeId: eigene.id,
vorname: "Neu",
nachname: "Kollege",
anzeigename: "Neu Kollege",
sam: "nkollege",
herkunft: Herkunft.MANUELL,
vorgesetzterId: chefin.id,
},
});
// Keine Quelle, keine externe Kennung - der alte Leseweg haette hier
// ausgestiegen.
expect(vonHand.quelleId).toBeNull();
expect(vonHand.vorgesetzterExterneId).toBeNull();
const laufId = await laufAnlegen({
vorlagenVersionId: versionId,
titel: "Onboarding Neu Kollege",
kundeId: eigene.id,
mitarbeiterId: vonHand.id,
});
const werte = await platzhalterWerteDesLaufs({
laufId,
technikerName: "Ali Demir",
jetzt: new Date("2026-10-04T10:00:00Z"),
});
expect(werte).toMatchObject({ vorgesetzter: "Petra Klein", vorgesetzter_sam: "pklein" });
});
it("nimmt für die OU den Eltern-Container des dn, nicht die Ziel-OU", async () => {
// Der dn sagt, wo das Konto **wirklich** liegt; die Ziel-OU der Abteilung
// ist die Absicht fuer ein Konto, das es noch nicht gibt.
+29
View File
@@ -450,6 +450,35 @@ describe.runIf(datenbankVerfuegbar)("Seed gegen die Datenbank", () => {
expect(await testPrisma.tagCodeVergabe.count()).toBe(OFFENE_TAGS.length + 2);
});
it("macht Petra Klein zur Vorgesetzten von Maria Huber", async () => {
// Steht so im Auftrag. Ging vor #020 nicht: eine von Hand angelegte Person
// konnte keinen Vorgesetzten haben.
const eigene = await eigeneOrganisation();
await stammdatenSaeen(testPrisma, eigene.id);
const maria = await testPrisma.mitarbeiter.findFirstOrThrow({
where: { sam: "mhuber" },
include: { vorgesetzter: { select: { anzeigename: true, sam: true } } },
});
expect(maria.vorgesetzter).toEqual({ anzeigename: "Klein, Petra", sam: "pklein" });
});
it("gibt eine im Betrieb entfernte Vorgesetzten-Zuordnung nicht zurueck", async () => {
// Dieselbe Regel wie fuer alles andere im Seed: er legt an, er korrigiert
// nicht.
const eigene = await eigeneOrganisation();
await stammdatenSaeen(testPrisma, eigene.id);
const maria = await testPrisma.mitarbeiter.findFirstOrThrow({ where: { sam: "mhuber" } });
await testPrisma.mitarbeiter.update({
where: { id: maria.id },
data: { vorgesetzterId: null },
});
await stammdatenSaeen(testPrisma, eigene.id);
const nachher = await testPrisma.mitarbeiter.findFirstOrThrow({ where: { id: maria.id } });
expect(nachher.vorgesetzterId).toBeNull();
});
it("legt den Bestand beim zweiten Lauf nicht noch einmal an", async () => {
const eigene = await eigeneOrganisation();
await stammdatenSaeen(testPrisma, eigene.id);