feat(mitarbeiter): Vorgesetzter auch fuer Personen ohne Verzeichnis
Die Zuordnung lief allein ueber vorgesetzterExterneId, und die ist nur
innerhalb einer Verzeichnisquelle eindeutig: eine von Hand angelegte Person
konnte keinen Vorgesetzten haben, und {{vorgesetzter_sam}} blieb im
Onboarding-Befehl leer -- genau dort, wo er gebraucht wird.
Entschieden fuer eine eigene Selbstbeziehung vorgesetzterId. Die befuerchteten
"zwei Wege zur selben Auskunft" entstehen nicht, weil der Abgleich sie
mitschreibt: vorgesetzterExterneId ist jetzt wie abteilung nur der rohe Wert
des Verzeichnisses, gelesen wird allein vorgesetzterId. Die Alternative haette
manuellen Personen eine erfundene externeId geben muessen, und externeId heisst
"objectGUID aus dem Verzeichnis".
Liefert das Verzeichnis keinen manager, bleibt eine von Hand gesetzte Zuordnung
stehen -- geleert wird nur, was das Verzeichnis selbst gesetzt hatte.
Die Migration fuehrt bestehende Daten nach. Ohne das waere die Auskunft nach
dem Einspielen bei jeder Verzeichnis-Person still leer gewesen, bis der
naechste Abgleich laeuft. An einem nachgebauten Vor-Zustand gefahren.
Drei Grenzen, drei Stellen: der zusammengesetzte Fremdschluessel haelt die
Mandantengrenze, ein CHECK die kuerzeste Schleife, und der Dienst laeuft die
Kette hinauf fuer laengere.
Im gebauten Stand durchgeklickt: speichern, neu laden, Wert steht, entfernen,
390 px ohne horizontales Scrollen. Gegenprobe am Abgleich gefahren.
Closes #31
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user