Files
Laufzettel/scripts/beispiel-stammdaten.ts
T
Florian WildeandClaude Opus 5 86d55917f9
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(stammdaten): Mitarbeiter, Abteilungen und Geraete
Die Stammdaten, an denen die Historie haengt. Personen werden deaktiviert,
Geraete und Abteilungen archiviert — geloescht wird nichts, woran ein Lauf
haengen kann.

- Mitarbeiterliste mit Suche ueber Anzeigename, Mail, UPN und sAMAccountName,
  Filter nach Abteilung, standardmaessig nur aktive; die Suche ist ein eigenes
  Bauteil, weil #013 und #014 sie wieder brauchen
- Personenseite mit Stammdaten, Geraeten, Vorgesetztem; was aus dem Verzeichnis
  kommt, ist gesperrt — der naechste Abgleich wuerde es ohnehin zurueckdrehen
- Abteilungen unter Admin: Ziel-OU, Standardgruppen, Mail-Domain, dazu die
  Namen, die bei Personen vorkommen, aber nicht gepflegt sind
- Geraete mit Suche ueber Hostname, Bezeichnung, Seriennummer und Tag-Code;
  CSV-Import mit einem Ergebnis je Zeile
- Neues Recht mitarbeiterBearbeiten; die Seitenleiste fuehrt Personen und
  Geraete unter einem Eintrag, im Modus Dienstleistung anders beschriftet

Der unabhaengige Review fand zwei Blocker. Der schwerere meldete Erfolg: ein
nicht geschlossenes Anfuehrungszeichen verschluckte im CSV-Import alle
Folgezeilen bis zum naechsten `"`, und der Import sagte „3 von 3 Zeilen
angelegt". Ein Wert reicht jetzt nicht mehr ueber die Zeile hinaus — fuer
Hostnamen und Seriennummern ist ein Umbruch praktisch immer ein vergessenes
Anfuehrungszeichen, und so endet der Schaden an seiner Zeile. Der zweite:
ungepflegte Abteilungen wurden ueber alle Kunden hinweg gruppiert.

Dazu sieben weitere Befunde, alle behoben — darunter eine Besitzerauswahl, die
Personen fremder Kunden anbot, und ein Techniker, der ein Geraet endgueltig
loeschen konnte.

610 Tests gruen, 18 E2E-Ablaeufe gegen den gebauten Stand.

Schliesst #011

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 12:51:54 +02:00

158 lines
5.7 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 die
* echten Beispieldaten kommen erst mit #017. Bis dahin legt dieses Skript 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. Mit #017 wird das Skript geloescht.
*
* 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();