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:
@@ -0,0 +1,62 @@
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- HANDGESCHRIEBEN. Prisma erzeugt hier zusaetzlich Loeschbefehle fuer die
|
||||
-- zusammengesetzten Fremdschluessel (Mandantengrenze), die Trigramm-Indizes
|
||||
-- (Archivsuche), die partiellen eindeutigen Indizes und die Trigger auf
|
||||
-- veroeffentlichten Versionen: es kennt sie nicht, weil sie sich im Schema nicht
|
||||
-- ausdruecken lassen. Diese Loeschbefehle sind bewusst gestrichen.
|
||||
--
|
||||
-- tests/datenbankregeln.test.ts prueft, dass alle diese Objekte vorhanden sind –
|
||||
-- ein versehentlich stehengelassener Loeschbefehl faellt dort sofort auf.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Vorgesetzter auch fuer von Hand angelegte Personen (#020).
|
||||
--
|
||||
-- Bisher lief die Auskunft allein ueber `vorgesetzterExterneId`, und die ist nur
|
||||
-- innerhalb einer Verzeichnisquelle eindeutig: eine Person ohne Quelle konnte
|
||||
-- keinen Vorgesetzten haben, {{vorgesetzter_sam}} blieb dort immer leer.
|
||||
ALTER TABLE "Mitarbeiter"
|
||||
ADD COLUMN "vorgesetzterId" TEXT;
|
||||
|
||||
CREATE INDEX "Mitarbeiter_vorgesetzterId_idx" ON "Mitarbeiter"("vorgesetzterId");
|
||||
|
||||
ALTER TABLE "Mitarbeiter"
|
||||
ADD CONSTRAINT "Mitarbeiter_vorgesetzterId_fkey"
|
||||
FOREIGN KEY ("vorgesetzterId") REFERENCES "Mitarbeiter" ("id")
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- Die Mandantengrenze, wie bei Lauf und Geraet: der Vorgesetzte gehoert zum
|
||||
-- selben Kunden. MATCH SIMPLE ist richtig — ist die Spalte NULL, greift der
|
||||
-- Fremdschluessel nicht, eine Person ohne Vorgesetzten bleibt also erlaubt.
|
||||
ALTER TABLE "Mitarbeiter"
|
||||
ADD CONSTRAINT "Mitarbeiter_vorgesetzter_gleicher_kunde"
|
||||
FOREIGN KEY ("vorgesetzterId", "kundeId") REFERENCES "Mitarbeiter" ("id", "kundeId")
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- Niemand ist sein eigener Vorgesetzter. Das ist die kuerzeste Schleife und die
|
||||
-- einzige, die sich ohne Rekursion abfangen laesst; laengere haelt der Dienst
|
||||
-- auf (siehe mitarbeiter.ts, vorgesetztenSetzen).
|
||||
ALTER TABLE "Mitarbeiter"
|
||||
ADD CONSTRAINT "Mitarbeiter_vorgesetzter_nicht_selbst"
|
||||
CHECK ("vorgesetzterId" IS NULL OR "vorgesetzterId" <> "id");
|
||||
|
||||
-- Nachfuehrung fuer bestehende Installationen.
|
||||
--
|
||||
-- Ohne sie waere `{{vorgesetzter_sam}}` nach dieser Migration bei **jeder**
|
||||
-- Person aus dem Verzeichnis leer, bis der naechste Abgleich laeuft -- und zwar
|
||||
-- still: die Auskunft lief bisher allein ueber `vorgesetzterExterneId`, der neue
|
||||
-- Leseweg nimmt nur `vorgesetzterId`. Dieselbe Falle wie beim Seed-Schluessel
|
||||
-- in #021: ein Umstieg, der die vorhandenen Daten nicht mitnimmt.
|
||||
--
|
||||
-- Die Zuordnung laeuft ueber die `externeId` **derselben** Quelle, genau wie der
|
||||
-- alte Leseweg: die Kennung ist nur dort eindeutig.
|
||||
UPDATE "Mitarbeiter" AS person
|
||||
SET "vorgesetzterId" = chef."id"
|
||||
FROM "Mitarbeiter" AS chef
|
||||
WHERE person."vorgesetzterExterneId" IS NOT NULL
|
||||
AND person."quelleId" IS NOT NULL
|
||||
AND chef."quelleId" = person."quelleId"
|
||||
AND chef."externeId" = person."vorgesetzterExterneId"
|
||||
-- Die Mandantengrenze gilt auch hier; stand je eine Zuordnung ueber Kunden
|
||||
-- hinweg in den Daten, bliebe sie leer statt den Fremdschluessel zu brechen.
|
||||
AND chef."kundeId" = person."kundeId"
|
||||
AND chef."id" <> person."id";
|
||||
+18
-4
@@ -249,17 +249,30 @@ model Mitarbeiter {
|
||||
/// passende Abteilung bleibt trotzdem erhalten.
|
||||
abteilung String?
|
||||
titel String?
|
||||
/// Der rohe `manager`-Wert aus dem Verzeichnis, aufgeloest auf die `externeId`
|
||||
/// des Vorgesetzten — je Quelle eindeutig. Wie `abteilung` ist das der Wert
|
||||
/// des Verzeichnisses, nicht die gepflegte Zuordnung; die steht in
|
||||
/// `vorgesetzterId`. Leer bei einer von Hand angelegten Person.
|
||||
vorgesetzterExterneId String?
|
||||
/// Die **gepflegte** Zuordnung, und der einzige Leseweg (#020). Der Abgleich
|
||||
/// schreibt sie aus `vorgesetzterExterneId` mit, von Hand gesetzt geht sie
|
||||
/// auch ohne Verzeichnis. Darf nicht auf die Person selbst zeigen (CHECK) und
|
||||
/// nicht auf jemanden eines anderen Kunden (zusammengesetzter Fremdschluessel
|
||||
/// `Mitarbeiter_vorgesetzter_gleicher_kunde`). Laengere Schleifen haelt der
|
||||
/// Dienst auf.
|
||||
vorgesetzterId String?
|
||||
aktiv Boolean @default(true)
|
||||
herkunft Herkunft @default(MANUELL)
|
||||
zuletztGesehenAm DateTime?
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @default(now()) @updatedAt
|
||||
|
||||
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
||||
quelle Verzeichnisquelle? @relation(fields: [quelleId], references: [id], onDelete: Restrict)
|
||||
geraete Geraet[]
|
||||
laeufe Lauf[]
|
||||
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
||||
quelle Verzeichnisquelle? @relation(fields: [quelleId], references: [id], onDelete: Restrict)
|
||||
vorgesetzter Mitarbeiter? @relation("Vorgesetzter", fields: [vorgesetzterId], references: [id], onDelete: Restrict)
|
||||
untergebene Mitarbeiter[] @relation("Vorgesetzter")
|
||||
geraete Geraet[]
|
||||
laeufe Lauf[]
|
||||
|
||||
/// Postgres behandelt NULL als verschieden, deshalb stoeren mehrere manuell
|
||||
/// angelegte Mitarbeiter ohne Quelle und externe Id hier nicht.
|
||||
@@ -270,6 +283,7 @@ model Mitarbeiter {
|
||||
@@index([abteilung])
|
||||
@@index([upn])
|
||||
@@index([sam])
|
||||
@@index([vorgesetzterId])
|
||||
}
|
||||
|
||||
/// Vom Admin gepflegt, Vorschlaege entstehen aus den department-Werten des
|
||||
|
||||
@@ -57,6 +57,8 @@ export type PersonenVorgabe = {
|
||||
readonly upn: string;
|
||||
readonly abteilung: string;
|
||||
readonly titel: string;
|
||||
/** UPN des Vorgesetzten – aufgeloest, nachdem alle Personen stehen. */
|
||||
readonly vorgesetzterUpn?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -64,10 +66,11 @@ export type PersonenVorgabe = {
|
||||
* Haus ohne Verzeichnis entstehen.
|
||||
*
|
||||
* Maria Huber steht so im Auftrag und taucht in den Abnahmekriterien wieder auf.
|
||||
* Ihre Vorgesetzte Petra Klein ist deshalb ebenfalls dabei; **gespeichert wird
|
||||
* die Zuordnung nicht**: `vorgesetzterExterneId` zeigt auf die `externeId`
|
||||
* derselben Verzeichnisquelle, und eine von Hand angelegte Person hat beides
|
||||
* nicht. Im Onboarding-Lauf wird der Vorgesetzte ohnehin beim Start gewaehlt.
|
||||
* Ihre Vorgesetzte Petra Klein ist deshalb ebenfalls dabei — **und die Zuordnung
|
||||
* wird gespeichert**, seit `vorgesetzterId` es erlaubt (#020). Vorher ging das
|
||||
* nicht: die Auskunft lief allein ueber `vorgesetzterExterneId`, und die zeigt
|
||||
* auf die `externeId` derselben Verzeichnisquelle, die eine von Hand angelegte
|
||||
* Person nicht hat.
|
||||
*/
|
||||
export const PERSONEN: readonly PersonenVorgabe[] = [
|
||||
{
|
||||
@@ -77,6 +80,7 @@ export const PERSONEN: readonly PersonenVorgabe[] = [
|
||||
upn: "m.huber@firma.example",
|
||||
abteilung: "Buchhaltung",
|
||||
titel: "Sachbearbeiterin Rechnungsprüfung",
|
||||
vorgesetzterUpn: "p.klein@firma.example",
|
||||
},
|
||||
{
|
||||
vorname: "Petra",
|
||||
@@ -230,6 +234,8 @@ export async function stammdatenSaeen(
|
||||
|
||||
let personen = 0;
|
||||
const nachUpn = new Map<string, string>();
|
||||
/** Nur diese bekommen eine Vorgesetzten-Zuordnung – siehe unten. */
|
||||
const neuAngelegt = new Set<string>();
|
||||
for (const vorgabe of PERSONEN) {
|
||||
const vorhanden = await db.mitarbeiter.findFirst({ where: { kundeId, upn: vorgabe.upn } });
|
||||
if (vorhanden) {
|
||||
@@ -252,9 +258,27 @@ export async function stammdatenSaeen(
|
||||
},
|
||||
});
|
||||
nachUpn.set(vorgabe.upn, angelegt.id);
|
||||
neuAngelegt.add(vorgabe.upn);
|
||||
personen += 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Die Vorgesetzten erst jetzt, wenn alle Personen stehen: eine Zuordnung
|
||||
* zeigt auf eine Zeile, die beim Anlegen der ersten noch nicht da war.
|
||||
*
|
||||
* Nur fuer Personen, die **dieser Lauf** angelegt hat. Eine leere Zuordnung
|
||||
* sagt nicht, ob sie noch nie gesetzt oder im Betrieb entfernt wurde — und ein
|
||||
* zweiter Seed-Lauf darf nichts zurueckdrehen. Dieselbe Regel wie fuer alles
|
||||
* andere hier.
|
||||
*/
|
||||
for (const vorgabe of PERSONEN) {
|
||||
if (!vorgabe.vorgesetzterUpn || !neuAngelegt.has(vorgabe.upn)) continue;
|
||||
const personId = nachUpn.get(vorgabe.upn);
|
||||
const vorgesetzterId = nachUpn.get(vorgabe.vorgesetzterUpn);
|
||||
if (!personId || !vorgesetzterId) continue;
|
||||
await db.mitarbeiter.update({ where: { id: personId }, data: { vorgesetzterId } });
|
||||
}
|
||||
|
||||
// Im Modus DIENSTLEISTUNG traegt der Hostname das Kundenkuerzel – dieselbe
|
||||
// Regel, die auch die Namensvorschlaege benutzen (`{kundenkuerzel}-PC-{nnn}`).
|
||||
const praefix = optionen.hostnamePraefix ? `${optionen.hostnamePraefix}-` : "";
|
||||
|
||||
Reference in New Issue
Block a user