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
+27 -2
View File
@@ -317,6 +317,7 @@ async function lauf(quelle: Verzeichnisquelle, benutzer: HandelnderBenutzer): Pr
// Der Vorgesetzte kommt im zweiten Durchgang: sein DN laesst sich
// erst aufloesen, wenn alle Kennungen bekannt sind.
vorgesetzterExterneId: null,
vorgesetzterId: null,
aktiv: true,
herkunft: Herkunft.ABGLEICH,
zuletztGesehenAm: beginn,
@@ -365,7 +366,19 @@ async function lauf(quelle: Verzeichnisquelle, benutzer: HandelnderBenutzer): Pr
// Verzeichnis diese Person, und der naechste Lauf findet sie ueber die
// Kennung.
...(uebernommenJetzt ? { quelleId, externeId: eintrag.externeId } : {}),
...(vorgesetztenLeeren ? { vorgesetzterExterneId: null } : {}),
/*
* Steht im Verzeichnis kein Vorgesetzter, wird der **rohe** Wert
* geleert. Die gepflegte Zuordnung nur dann, wenn sie vom
* Verzeichnis kam — erkennbar daran, dass dort eine Kennung stand.
* Eine von Hand gesetzte Zuordnung bleibt stehen: das Verzeichnis
* fuehrt sie nicht, also hat es sie auch nicht zu loeschen (#020).
*/
...(vorgesetztenLeeren
? {
vorgesetzterExterneId: null,
...(vorhanden.vorgesetzterExterneId !== null ? { vorgesetzterId: null } : {}),
}
: {}),
// Wer wieder im Ergebnis steht, ist wieder aktiv — eine
// wiedereingestellte Person behaelt so ihre Laeufe.
aktiv: true,
@@ -386,12 +399,24 @@ async function lauf(quelle: Verzeichnisquelle, benutzer: HandelnderBenutzer): Pr
gelesen = bilanz.gesamtGelesen;
// --- Zweiter Durchgang: Vorgesetzte --------------------------------------
//
// Geschrieben werden beide Felder: `vorgesetzterExterneId` als roher Wert des
// Verzeichnisses, `vorgesetzterId` als die gepflegte Zuordnung, aus der
// jeder Leseweg schoepft (#020). Vorher gab es nur das erste, und eine von
// Hand angelegte Person konnte keinen Vorgesetzten haben.
for (const offen of offene) {
const kennung = kennungNachDn.get(dnSchluessel(offen.vorgesetzterDn)) ?? null;
if (kennung === offen.vorherigeKennung) continue;
const vorgesetzter =
kennung === null
? null
: await prisma.mitarbeiter.findUnique({
where: { quelleId_externeId: { quelleId, externeId: kennung } },
select: { id: true },
});
await prisma.mitarbeiter.updateMany({
where: { quelleId, externeId: offen.externeId },
data: { vorgesetzterExterneId: kennung },
data: { vorgesetzterExterneId: kennung, vorgesetzterId: vorgesetzter?.id ?? null },
});
// Eine gerade angelegte Person ist angelegt, nicht zusaetzlich
// aktualisiert.
+88
View File
@@ -212,6 +212,94 @@ export async function mitarbeiterBearbeiten(
});
}
/**
* Setzt oder entfernt den Vorgesetzten.
*
* Eigener Weg und nicht Teil von `mitarbeiterBearbeiten`, weil es kein
* abgeglichenes Feld im Sinn der Sperre ist: eine Person aus dem Verzeichnis
* darf hier geaendert werden. Der naechste Abgleich setzt die Zuordnung
* allerdings zurueck, wenn das Verzeichnis einen `manager` liefert — dort ist
* sie dann gepflegt. Liefert es keinen, bleibt die Zuordnung von Hand stehen
* (#020).
*
* `vorgesetzterExterneId` bleibt unangetastet: das ist der rohe Wert des
* Verzeichnisses, nicht die Zuordnung.
*/
export async function vorgesetztenSetzen(
mitarbeiterId: string,
vorgesetzterId: string | null,
benutzer: HandelnderBenutzer,
): Promise<Mitarbeiter> {
const vorher = await prisma.mitarbeiter.findUnique({ where: { id: mitarbeiterId } });
if (!vorher) throw new MitarbeiterFehler("Diese Person gibt es nicht (mehr).");
await kundeZugriffPruefen(vorher.kundeId, benutzer);
const gewuenscht = vorgesetzterId === null || vorgesetzterId === "" ? null : vorgesetzterId;
if (gewuenscht === vorher.vorgesetzterId) return vorher;
let name = "niemand";
if (gewuenscht !== null) {
if (gewuenscht === mitarbeiterId) {
throw new MitarbeiterFehler("Niemand ist sein eigener Vorgesetzter.");
}
const kuenftig = await prisma.mitarbeiter.findUnique({ where: { id: gewuenscht } });
if (!kuenftig) throw new MitarbeiterFehler("Diese Person gibt es nicht (mehr).");
// Dieselbe Pruefung haelt der zusammengesetzte Fremdschluessel fest; hier
// steht sie fuer die Meldung.
if (kuenftig.kundeId !== vorher.kundeId) {
throw new MitarbeiterFehler("Der Vorgesetzte muss zum selben Kunden gehören.");
}
await schleifePruefen(mitarbeiterId, gewuenscht);
name = kuenftig.anzeigename;
}
return prisma.$transaction(async (tx) => {
const nachher = await tx.mitarbeiter.update({
where: { id: mitarbeiterId },
data: { vorgesetzterId: gewuenscht },
});
await tx.protokoll.create({
data: {
entitaet: "Mitarbeiter",
entitaetId: mitarbeiterId,
aktion: gewuenscht === null ? "Vorgesetzter entfernt" : `Vorgesetzter auf ${name} gesetzt`,
benutzerId: benutzer.id,
vorher: auszug(vorher, ["vorgesetzterId"]),
nachher: auszug(nachher, ["vorgesetzterId"]),
},
});
return nachher;
});
}
/**
* Weist eine Schleife ab: A ist Vorgesetzter von B ist Vorgesetzter von A.
*
* Die Datenbank faengt nur die kuerzeste ab (`vorgesetzterId <> id`); laengere
* brauchen einen Gang nach oben. Die Obergrenze ist kein Schoenheitsfehler,
* sondern die Notbremse: waere die Kette schon beschaedigt, liefe die Suche
* sonst ewig.
*/
async function schleifePruefen(mitarbeiterId: string, kuenftigerVorgesetzter: string): Promise<void> {
const gesehen = new Set<string>([mitarbeiterId]);
let aktuell: string | null = kuenftigerVorgesetzter;
for (let schritt = 0; schritt < 100 && aktuell !== null; schritt += 1) {
if (gesehen.has(aktuell)) {
throw new MitarbeiterFehler(
"Das ergäbe eine Schleife: diese Person ist dem künftigen Vorgesetzten " +
"bereits selbst vorgesetzt, direkt oder über weitere Personen.",
);
}
gesehen.add(aktuell);
const naechster: { readonly vorgesetzterId: string | null } | null = await prisma.mitarbeiter.findUnique({
where: { id: aktuell },
select: { vorgesetzterId: true },
});
aktuell = naechster?.vorgesetzterId ?? null;
}
}
/**
* Deaktivieren und wieder aktivieren.
*
+9 -9
View File
@@ -106,7 +106,7 @@ export async function platzhalterWerteDesLaufs(kontext: LaufKontext): Promise<Pl
titel: true,
dn: true,
quelleId: true,
vorgesetzterExterneId: true,
vorgesetzterId: true,
},
},
abteilung: { select: { name: true, zielOu: true, standardGruppen: true } },
@@ -207,18 +207,18 @@ export async function platzhalterWerteDesLaufs(kontext: LaufKontext): Promise<Pl
}
/**
* Der Vorgesetzte, ueber `vorgesetzterExterneId` **derselben** Verzeichnisquelle.
* Der Vorgesetzte, ueber die gepflegte Zuordnung `vorgesetzterId`.
*
* Die Kennung ist nur innerhalb einer Quelle eindeutig; ohne diese
* Einschraenkung zoege ein zweites Verzeichnis fremde Treffer herbei.
* Vorher lief das ueber `vorgesetzterExterneId` und die `externeId` derselben
* Quelle. Dann blieb `{{vorgesetzter_sam}}` bei jeder von Hand angelegten Person
* leer — genau im Onboarding, wo der Befehl ihn braucht (#020).
*/
async function vorgesetztenLesen(person: {
readonly quelleId: string | null;
readonly vorgesetzterExterneId: string | null;
readonly vorgesetzterId: string | null;
}): Promise<{ anzeigename: string; sam: string | null } | null> {
if (!person.vorgesetzterExterneId || !person.quelleId) return null;
return prisma.mitarbeiter.findFirst({
where: { quelleId: person.quelleId, externeId: person.vorgesetzterExterneId },
if (!person.vorgesetzterId) return null;
return prisma.mitarbeiter.findUnique({
where: { id: person.vorgesetzterId },
select: { anzeigename: true, sam: true },
});
}