Files
Laufzettel/scripts/beispiel-stammdaten.ts
T
Florian WildeandClaude Opus 5 5c81a62cfd
Prüfung / pruefung (push) Canceled after 0s
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
feat(seed): sieben Vorlagen, Beispielbestand und Demo-Kunden
Die sieben Ablaeufe des Hauses liegen nach dem Seed als veroeffentlichte
Version 1 bereit: 36 Abschnitte, 155 Schritte, 28 Variablen, jeder
Schritt mit Hilfetext. Sie stehen als JSON im Austauschformat unter
prisma/seed-daten/vorlagen/ und werden beim Einrichten gegen dasselbe
Schema geprueft wie ein Import.

Der Seed richtet ein, er raeumt nicht auf: jede Zeile hat einen
sprechenden Schluessel, und was es schon gibt, bleibt unberuehrt. Erst
Entwurf, dann veroeffentlichen - die Trigger aus #010 lassen keinen
anderen Weg zu. Im Modus DIENSTLEISTUNG haengt der Beispielbestand an
der Musterfirma, LZ-7K3F9Q fuehrt also auf MUS-PC-001.

Dazu zwei Zusagen eingeloest, die niemand deckte: {{ou_gesperrt}} kommt
jetzt wirklich aus den Einstellungen, und Titel und Hilfetext eines
Laufs bekommen dasselbe generierte Passwort wie der Befehl daneben -
vorher stand im Befehl A und im Hilfetext B.

Closes #17

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-26 05:26:04 +02:00

167 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env tsx
/**
* Beispiel-Stammdaten fuer die Screenshots (#011).
*
* Personen, Abteilungen und Geraete lassen sich nur mit Inhalt zeigen, und der Seed legt
* keine Person aus dem Verzeichnis an. Dieses Skript legt einen
* kleinen, glaubwuerdigen Bestand an — darunter eine Person mit
* `herkunft = ABGLEICH`, damit die gesperrten Stammdaten sichtbar werden.
*
* Es geht durch die echten Dienste, wo es welche gibt: waeren die Regeln anders
* als hier angenommen, schlaegt es fehl, statt eine Lage herzustellen, die es in
* der Anwendung nie gibt. Der abgeglichene Fall entsteht direkt ueber Prisma —
* den Weg dorthin baut erst der Verzeichnisabgleich (#016).
*
* Idempotent.
*
* **Bleibt auch nach #017.** Der Kopf sagte frueher „Mit #017 wird das Skript
* geloescht“ — das war eine Verwechslung zweier Dinge. Der Seed richtet eine
* **Installation** ein: Vorlagen, Abteilungen, Personen, Geraete. Was
* Screenshots und Ende-zu-Ende-Abläufe brauchen, ist etwas anderes: ein offener
* Entwurf, ein halb abgearbeiteter Lauf, eine Person aus dem Verzeichnis. Das in
* den Seed zu legen hiesse, jeder frischen Installation Demo-Laeufe mitzugeben,
* die dort niemand bestellt hat. Die Zusammenfuehrung dieser Fixtures gehoert zu
* #018.
*
* pnpm beispiel:stammdaten
*/
import "dotenv/config";
import { Herkunft } from "../src/generated/prisma/enums.js";
import { prisma } from "../src/lib/db.js";
import { abteilungAnlegen } from "../src/server/services/abteilungen.js";
import { geraetAnlegen } from "../src/server/services/geraete.js";
import { mitarbeiterAnlegen } from "../src/server/services/mitarbeiter.js";
const admin = await prisma.benutzer.findFirst({
where: { rolle: "ADMIN" },
orderBy: { erstelltAm: "asc" },
select: { id: true, rolle: true },
});
if (!admin) {
throw new Error("Es gibt keinen Administrator. Erst pnpm db:seed laufen lassen.");
}
const handelnder = { id: admin.id, rolle: admin.rolle };
const eigene = await prisma.kunde.findFirstOrThrow({ where: { istEigeneOrganisation: true } });
const ABTEILUNGEN = [
{
name: "Buchhaltung",
zielOu: "OU=Buchhaltung,OU=Benutzer,DC=firma,DC=de",
standardGruppen: ["G-Buchhaltung", "G-Alle"],
mailDomain: "firma.de",
},
{
name: "Vertrieb",
zielOu: "OU=Vertrieb,OU=Benutzer,DC=firma,DC=de",
standardGruppen: ["G-Vertrieb", "G-Alle"],
mailDomain: "firma.de",
},
{
name: "IT",
zielOu: "OU=IT,OU=Benutzer,DC=firma,DC=de",
standardGruppen: ["G-IT", "G-Alle", "G-Adminrechte"],
mailDomain: "firma.de",
},
];
const PERSONEN = [
{ vorname: "Maria", nachname: "Huber", sam: "mhuber", abteilung: "Buchhaltung", titel: "Sachbearbeiterin" },
{ vorname: "Jörg", nachname: "Müller-Lüdenscheid", sam: "jmuellerluedensch", abteilung: "Vertrieb" },
{ vorname: "Ali", nachname: "Demir", sam: "ademir", abteilung: "IT", titel: "Systemadministrator" },
{ vorname: "Sonja", nachname: "Brand", sam: "sbrand", abteilung: "Vertrieb" },
];
let neueAbteilungen = 0;
for (const eingabe of ABTEILUNGEN) {
const vorhanden = await prisma.abteilung.findUnique({
where: { kundeId_name: { kundeId: eigene.id, name: eingabe.name } },
});
if (vorhanden) continue;
await abteilungAnlegen(eingabe, handelnder);
neueAbteilungen += 1;
}
let neuePersonen = 0;
for (const person of PERSONEN) {
const vorhanden = await prisma.mitarbeiter.findFirst({ where: { sam: person.sam } });
if (vorhanden) continue;
const mail = `${person.sam}@firma.de`;
await mitarbeiterAnlegen({ ...person, mail, upn: mail }, handelnder);
neuePersonen += 1;
}
/**
* Eine Person aus dem Verzeichnis.
*
* Direkt ueber Prisma, weil der Dienst nur `herkunft = MANUELL` anlegt — den
* anderen Weg baut der Verzeichnisabgleich (#016). Fuer den Screenshot der
* gesperrten Stammdaten braucht es sie trotzdem schon jetzt.
*/
const abgeglichen = await prisma.mitarbeiter.findFirst({ where: { sam: "kwinter" } });
if (!abgeglichen) {
await prisma.mitarbeiter.create({
data: {
kundeId: eigene.id,
vorname: "Katrin",
nachname: "Winter",
anzeigename: "Katrin Winter",
mail: "kwinter@firma.de",
upn: "kwinter@firma.de",
sam: "kwinter",
abteilung: "Einkauf",
titel: "Einkäuferin",
dn: "CN=Katrin Winter,OU=Einkauf,OU=Benutzer,DC=firma,DC=de",
herkunft: Herkunft.ABGLEICH,
zuletztGesehenAm: new Date(),
},
});
neuePersonen += 1;
}
/**
* Ein paar Geraete, darunter eines ohne Hostname.
*
* `PC-001` und `NB-001` heissen so, weil der Auftrag sie in den Beispieldaten
* nennt; der Serverschrank zeigt den Fall „Bezeichnung statt Hostname“, den die
* Pflichtregel ausdruecklich erlaubt.
*/
const GERAETE = [
{ hostname: "PC-001", typ: "PC" as const, modell: "Dell OptiPlex 7010", besitzer: "mhuber" },
{ hostname: "NB-001", typ: "NOTEBOOK" as const, modell: "Lenovo T14", besitzer: "jmuellerluedensch" },
{ hostname: "SRV-DC01", typ: "SERVER" as const, modell: "Dell PowerEdge R450" },
{ bezeichnung: "Serverschrank Keller", typ: "SONSTIGES" as const },
{ hostname: "PRN-EG-01", typ: "DRUCKER" as const, modell: "Kyocera ECOSYS" },
];
let neueGeraete = 0;
for (const geraet of GERAETE) {
// Seriennummern haben keine Leerzeichen – auch die erfundenen nicht.
const kennzeichen = (geraet.hostname ?? geraet.bezeichnung ?? "").replace(/\s+/gu, "-").toUpperCase();
const vorhanden = await prisma.geraet.findFirst({
where: geraet.hostname ? { hostname: geraet.hostname } : { bezeichnung: geraet.bezeichnung },
});
if (vorhanden) continue;
const besitzer = geraet.besitzer
? await prisma.mitarbeiter.findFirst({ where: { sam: geraet.besitzer }, select: { id: true } })
: null;
await geraetAnlegen(
{
...(geraet.hostname ? { hostname: geraet.hostname } : {}),
...(geraet.bezeichnung ? { bezeichnung: geraet.bezeichnung } : {}),
...(geraet.modell ? { modell: geraet.modell } : {}),
...(besitzer ? { mitarbeiterId: besitzer.id } : {}),
typ: geraet.typ,
seriennummer: `SN-${kennzeichen}`,
},
handelnder,
);
neueGeraete += 1;
}
console.log(
`Beispiel-Stammdaten: ${neueAbteilungen} Abteilungen, ${neuePersonen} Personen und ` +
`${neueGeraete} Geräte neu angelegt.`,
);
await prisma.$disconnect();