717 lines
28 KiB
Plaintext
717 lines
28 KiB
Plaintext
// Laufzettel – Datenmodell
|
||
//
|
||
// Benennung deutsch, Umlaute ASCII-gefaltet. Die Abbildung auf die englischen
|
||
// Modellnamen der Auftragsvorgabe steht in CLAUDE.md.
|
||
//
|
||
// Zwei Entwurfsentscheidungen tragen das Ganze, siehe docs/architektur.md:
|
||
//
|
||
// 1. Jede kundenbezogene Zeile traegt IMMER eine kundeId – auch im Modus
|
||
// INTERN, wo es die eine Organisation mit istEigeneOrganisation = true ist.
|
||
// Dadurch ist der Wechsel in den Modus DIENSTLEISTUNG eine Datenaenderung
|
||
// und keine Migration.
|
||
//
|
||
// 2. Nichts wird geloescht, woran ein Lauf haengt. Geraete, Abteilungen und
|
||
// Vorlagen werden archiviert, Mitarbeiter nur auf aktiv = false gesetzt.
|
||
// Deshalb stehen die Fremdschluessel auf Restrict; Cascade gibt es nur
|
||
// innerhalb einer Vorlagen-Version, die als Ganzes verworfen werden darf.
|
||
|
||
generator client {
|
||
provider = "prisma-client"
|
||
output = "../src/generated/prisma"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
}
|
||
|
||
// ---- Aufzaehlungen ---------------------------------------------------------
|
||
|
||
enum Rolle {
|
||
ADMIN
|
||
REDAKTEUR
|
||
TECHNIKER
|
||
LESER
|
||
}
|
||
|
||
enum Betriebsmodus {
|
||
INTERN
|
||
DIENSTLEISTUNG
|
||
}
|
||
|
||
enum LaufStatus {
|
||
OFFEN
|
||
IN_ARBEIT
|
||
ABGESCHLOSSEN
|
||
ARCHIVIERT
|
||
}
|
||
|
||
enum SchrittZustand {
|
||
OFFEN
|
||
ERLEDIGT
|
||
NICHT_ZUTREFFEND
|
||
FEHLGESCHLAGEN
|
||
}
|
||
|
||
// AKTION kommt in Phase 4 dazu. Weil der Typ ein Enum ist und die Einzelheiten
|
||
// in VorlagenSchritt.konfig liegen, braucht das keine Migration der bestehenden
|
||
// Schritte.
|
||
enum SchrittTyp {
|
||
HAKEN
|
||
TEXT
|
||
ZAHL
|
||
AUSWAHL
|
||
DATUM
|
||
BEFEHL
|
||
}
|
||
|
||
enum VariablenTyp {
|
||
TEXT
|
||
ZAHL
|
||
AUSWAHL
|
||
DATUM
|
||
}
|
||
|
||
enum GeraeteTyp {
|
||
PC
|
||
NOTEBOOK
|
||
SERVER
|
||
FIREWALL
|
||
SWITCH
|
||
DRUCKER
|
||
RAUM
|
||
SONSTIGES
|
||
}
|
||
|
||
enum VersionStatus {
|
||
ENTWURF
|
||
VEROEFFENTLICHT
|
||
}
|
||
|
||
enum Herkunft {
|
||
ABGLEICH
|
||
MANUELL
|
||
}
|
||
|
||
// ---- Einstellungen und Mandanten ------------------------------------------
|
||
|
||
/// Einzelne Zeile mit fester id "global". Der Betriebsmodus liegt bewusst in
|
||
/// der Datenbank und nicht in der Umgebung, damit der Admin ohne Neustart
|
||
/// umschalten kann.
|
||
model Einstellung {
|
||
id String @id @default("global")
|
||
modus Betriebsmodus @default(INTERN)
|
||
/// Globale Namensregeln fuer upn, sam, mail und hostname. Im Modus
|
||
/// DIENSTLEISTUNG koennen Kunden eigene Regeln haben, die diese ueberlagern.
|
||
namensregeln Json @default("{}")
|
||
/// Ziel-OU fuer deaktivierte Konten, Vorbelegung im Offboarding.
|
||
gesperrteOu String?
|
||
/// Sobald jemand den Schnellstart in der Oberflaeche gespeichert hat, laesst
|
||
/// der Seed `GeraetetypVorlage` in Ruhe (#022). Ohne diese Marke kaeme eine
|
||
/// entfernte Zuordnung beim naechsten Seed-Lauf zurueck: eine fehlende Zeile
|
||
/// sagt nicht, ob sie noch nie da war oder bewusst entfernt wurde.
|
||
schnellstartGepflegt Boolean @default(false)
|
||
erstelltAm DateTime @default(now())
|
||
geaendertAm DateTime @default(now()) @updatedAt
|
||
}
|
||
|
||
model Kunde {
|
||
id String @id @default(cuid())
|
||
name String
|
||
/// 2 bis 6 Zeichen, erscheint auf Etiketten und in der Hostname-Regel.
|
||
kuerzel String @unique
|
||
/// Genau eine Zeile traegt true. Erzwungen durch einen partiellen eindeutigen
|
||
/// Index in der Migration, den Prisma selbst nicht ausdruecken kann.
|
||
istEigeneOrganisation Boolean @default(false)
|
||
/// Namensregeln dieses Kunden; fehlende Schluessel fallen auf die globalen
|
||
/// aus Einstellung zurueck.
|
||
namensregeln Json @default("{}")
|
||
notizen String?
|
||
archiviert Boolean @default(false)
|
||
erstelltAm DateTime @default(now())
|
||
geaendertAm DateTime @default(now()) @updatedAt
|
||
|
||
abteilungen Abteilung[]
|
||
benutzerbindungen BenutzerKunde[]
|
||
geraete Geraet[]
|
||
laeufe Lauf[]
|
||
mitarbeiter Mitarbeiter[]
|
||
protokolle Protokoll[]
|
||
verzeichnisquellen Verzeichnisquelle[]
|
||
vorlagen Vorlage[]
|
||
|
||
@@index([archiviert])
|
||
}
|
||
|
||
// ---- Benutzer der Anwendung ------------------------------------------------
|
||
|
||
/// Die Menschen, die Laufzettel bedienen – nicht zu verwechseln mit Mitarbeiter,
|
||
/// den Personen der Firma aus dem Verzeichnis.
|
||
model Benutzer {
|
||
id String @id @default(cuid())
|
||
email String @unique
|
||
name String
|
||
rolle Rolle @default(LESER)
|
||
/// Subject-Claim des OIDC-Anbieters. Leer bei rein lokalen Konten.
|
||
oidcSub String? @unique
|
||
/// Nur gesetzt, wenn der lokale Login benutzt wird. argon2, nie Klartext.
|
||
passwortHash String?
|
||
aktiv Boolean @default(true)
|
||
/// Sitzungen, die vor diesem Zeitpunkt ausgestellt wurden, gelten nicht mehr.
|
||
/// So laesst sich ein Konto sperren, ohne auf den Ablauf des Cookies zu warten.
|
||
sitzungenUngueltigAb DateTime?
|
||
letzteAnmeldungAm DateTime?
|
||
erstelltAm DateTime @default(now())
|
||
geaendertAm DateTime @default(now()) @updatedAt
|
||
|
||
abgeschlosseneLaeufe Lauf[] @relation("LaufAbgeschlossenVon")
|
||
archivierteLaeufe Lauf[] @relation("LaufArchiviertVon")
|
||
erstellteLaeufe Lauf[] @relation("LaufErstelltVon")
|
||
zugewieseneLaeufe Lauf[] @relation("LaufZugewiesenAn")
|
||
kommentare Kommentar[]
|
||
kundenbindungen BenutzerKunde[]
|
||
protokolle Protokoll[]
|
||
schrittergebnisse Schrittergebnis[]
|
||
veroeffentlichteVersionen VorlagenVersion[] @relation("VersionVeroeffentlichtVon")
|
||
|
||
@@index([aktiv])
|
||
@@index([rolle])
|
||
}
|
||
|
||
/// Bindet einen Benutzer an Kunden. Nur im Modus DIENSTLEISTUNG und nur, wenn
|
||
/// OIDC_CUSTOMER_GROUP_PREFIX gesetzt ist: TECHNIKER und LESER sehen dann allein
|
||
/// ihre Kunden, ADMIN und REDAKTEUR alles.
|
||
///
|
||
/// Die Bindung liegt in der Datenbank und nicht im Token, damit sie – wie Rolle
|
||
/// und Aktiv-Status – pro Anfrage frisch gelesen wird und auch fuer lokale Konten
|
||
/// gilt. Beim OIDC-Login wird sie neu geschrieben.
|
||
model BenutzerKunde {
|
||
benutzerId String
|
||
kundeId String
|
||
erstelltAm DateTime @default(now())
|
||
|
||
benutzer Benutzer @relation(fields: [benutzerId], references: [id], onDelete: Cascade)
|
||
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Cascade)
|
||
|
||
@@id([benutzerId, kundeId])
|
||
@@index([kundeId])
|
||
}
|
||
|
||
// ---- Verzeichnis und Personen ---------------------------------------------
|
||
|
||
/// LDAP-Quelle fuer den Abgleich des Mitarbeiterverzeichnisses. In der Regel
|
||
/// genau eine (das Firmen-AD); mehrere sind erlaubt, etwa eine zweite Domaene.
|
||
/// Nur fuer die eigene Organisation – im Modus DIENSTLEISTUNG gibt es keinen
|
||
/// Abgleich zu Kunden-ADs.
|
||
model Verzeichnisquelle {
|
||
id String @id @default(cuid())
|
||
kundeId String
|
||
name String
|
||
aktiv Boolean @default(false)
|
||
url String
|
||
bindDn String
|
||
/// AES-256-GCM aus APP_SECRET. Darf keine API-Antwort erreichen; dafuer sorgt
|
||
/// der Serializer in der Service-Schicht, abgedeckt durch einen eigenen Test.
|
||
bindPasswortVerschluesselt String
|
||
basisDn String
|
||
filter String
|
||
/// Zuordnung der LDAP-Attribute auf unsere Felder, mit AD-Vorgaben.
|
||
attributZuordnung Json
|
||
/// Eigenes CA-Zertifikat, falls ldaps gegen eine interne CA laeuft.
|
||
caZertifikatPem String?
|
||
abgleichIntervallMinuten Int @default(60)
|
||
letzterAbgleichAm DateTime?
|
||
letzterAbgleichStatus String?
|
||
/// Nimmt die Meldung der LDAP-Bibliothek auf. #016 muss sie filtern, bevor sie
|
||
/// hier landet – rohe Bind-Fehler koennen das Passwort enthalten.
|
||
letzterAbgleichFehler String?
|
||
erstelltAm DateTime @default(now())
|
||
geaendertAm DateTime @default(now()) @updatedAt
|
||
|
||
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
||
mitarbeiter Mitarbeiter[]
|
||
|
||
@@index([kundeId])
|
||
@@index([aktiv])
|
||
}
|
||
|
||
/// Die Personen der Firma bzw. des Kunden. Werden nie geloescht, nur auf
|
||
/// aktiv = false gesetzt, damit archivierte Laeufe weiter auf sie zeigen.
|
||
model Mitarbeiter {
|
||
id String @id @default(cuid())
|
||
kundeId String
|
||
/// Nur bei Herkunft ABGLEICH gesetzt.
|
||
quelleId String?
|
||
/// objectGUID des Verzeichnisses, je Quelle eindeutig.
|
||
externeId String?
|
||
vorname String
|
||
nachname String
|
||
anzeigename String
|
||
mail String?
|
||
upn String?
|
||
sam String?
|
||
dn String?
|
||
/// Der rohe department-Wert aus dem Verzeichnis. Die gepflegten Abteilungen
|
||
/// stehen in Abteilung und werden ueber den Namen zugeordnet; ein Wert ohne
|
||
/// 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)
|
||
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.
|
||
@@unique([quelleId, externeId])
|
||
/// Schluesselkandidat fuer den zusammengesetzten Fremdschluessel.
|
||
@@unique([id, kundeId])
|
||
@@index([kundeId, aktiv])
|
||
@@index([abteilung])
|
||
@@index([upn])
|
||
@@index([sam])
|
||
@@index([vorgesetzterId])
|
||
}
|
||
|
||
/// Vom Admin gepflegt, Vorschlaege entstehen aus den department-Werten des
|
||
/// Abgleichs. Liefert beim Onboarding die Ziel-OU und die Standardgruppen.
|
||
model Abteilung {
|
||
id String @id @default(cuid())
|
||
kundeId String
|
||
name String
|
||
zielOu String?
|
||
standardGruppen String[] @default([])
|
||
mailDomain String?
|
||
notizen String?
|
||
archiviert Boolean @default(false)
|
||
erstelltAm DateTime @default(now())
|
||
geaendertAm DateTime @default(now()) @updatedAt
|
||
|
||
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
||
laeufe Lauf[]
|
||
|
||
@@unique([kundeId, name])
|
||
/// Schluesselkandidat fuer den zusammengesetzten Fremdschluessel aus der
|
||
/// Migration, der eine Abteilung eines fremden Kunden ausschliesst.
|
||
@@unique([id, kundeId])
|
||
@@index([kundeId, archiviert])
|
||
}
|
||
|
||
// ---- Geraete und Tags ------------------------------------------------------
|
||
|
||
/// Nicht nur PCs: ein Serverschrank, ein Raum oder ein Auftrag bekommen
|
||
/// genauso einen Aufkleber.
|
||
///
|
||
/// Ein Geraet ohne registriertAm ist ein vorgedruckter, noch nicht zugeordneter
|
||
/// Tag. Erst beim ersten Scannen wird es zu einem echten Objekt – deshalb greift
|
||
/// die Pflicht zu hostname oder bezeichnung erst ab dann (Check-Constraint in
|
||
/// der Migration).
|
||
model Geraet {
|
||
id String @id @default(cuid())
|
||
kundeId String
|
||
/// Besitzer bzw. Hauptnutzer. Server, Raeume und Poolgeraete haben keinen.
|
||
mitarbeiterId String?
|
||
/// Wird nie geaendert und nie wiederverwendet, auch nicht nach Archivierung.
|
||
/// Unabhaengig vom Hostname, damit ein Geraet umbenannt werden kann, ohne dass
|
||
/// der Aufkleber ungueltig wird.
|
||
tagCode String @unique
|
||
hostname String?
|
||
/// Anzeigename fuer Objekte ohne Hostname, etwa "Serverschrank Keller".
|
||
bezeichnung String?
|
||
seriennummer String?
|
||
inventarnummer String?
|
||
typ GeraeteTyp @default(SONSTIGES)
|
||
modell String?
|
||
notizen String?
|
||
archiviert Boolean @default(false)
|
||
registriertAm DateTime?
|
||
erstelltAm DateTime @default(now())
|
||
geaendertAm DateTime @default(now()) @updatedAt
|
||
|
||
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
||
mitarbeiter Mitarbeiter? @relation(fields: [mitarbeiterId], references: [id], onDelete: Restrict)
|
||
laeufe Lauf[]
|
||
|
||
/// Schluesselkandidat fuer den zusammengesetzten Fremdschluessel.
|
||
@@unique([id, kundeId])
|
||
@@index([kundeId, archiviert])
|
||
@@index([mitarbeiterId])
|
||
@@index([typ])
|
||
@@index([registriertAm])
|
||
}
|
||
|
||
/// Jeder je vergebene tagCode, unabhaengig davon, ob das Geraet noch existiert.
|
||
///
|
||
/// Die Eindeutigkeit allein aus Geraet.tagCode zu ziehen reicht nicht: verschwindet
|
||
/// die Zeile, waere der Code wieder frei und ein alter Aufkleber im Regal zeigte
|
||
/// auf ein fremdes Objekt. Aus diesem Buch wird nie geloescht.
|
||
model TagCodeVergabe {
|
||
tagCode String @id
|
||
vergebenAm DateTime @default(now())
|
||
}
|
||
|
||
/// Vorlagen, die auf der Geraeteseite fuer diesen Geraetetyp als Schnellstart
|
||
/// vorgeschlagen werden. Bewusst ohne kundeId: welcher Schnellstart zu einem
|
||
/// Geraetetyp passt, haengt am Typ und nicht am Kunden. Kundenspezifische
|
||
/// Vorlagen filtert die Geraeteseite ueber Vorlage.kundeId heraus.
|
||
model GeraetetypVorlage {
|
||
id String @id @default(cuid())
|
||
geraeteTyp GeraeteTyp
|
||
vorlageId String
|
||
position Int @default(0)
|
||
|
||
vorlage Vorlage @relation(fields: [vorlageId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([geraeteTyp, vorlageId])
|
||
@@index([geraeteTyp, position])
|
||
}
|
||
|
||
// ---- Vorlagen --------------------------------------------------------------
|
||
|
||
model Vorlage {
|
||
id String @id @default(cuid())
|
||
name String
|
||
beschreibung String?
|
||
kategorie String?
|
||
schlagworte String[] @default([])
|
||
/// Nur im Modus DIENSTLEISTUNG: kundenspezifische Vorlage. Leer heisst
|
||
/// "gilt fuer alle".
|
||
kundeId String?
|
||
archiviert Boolean @default(false)
|
||
/// Kennung einer **mitgelieferten** Vorlage. Der Seed setzt sie und erkennt
|
||
/// seine sieben Vorlagen daran statt am Namen, der sich aendern darf (#021).
|
||
/// Eindeutig, wo gesetzt — erzwungen durch den partiellen eindeutigen Index
|
||
/// `Vorlage_seedSchluessel_einmalig`, den Prisma nicht ausdruecken kann.
|
||
/// Der Import setzt sie absichtlich **nicht**: eine eingespielte Kopie soll
|
||
/// nicht den Platz des Originals einnehmen.
|
||
seedSchluessel String?
|
||
erstelltAm DateTime @default(now())
|
||
geaendertAm DateTime @default(now()) @updatedAt
|
||
|
||
kunde Kunde? @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
||
versionen VorlagenVersion[]
|
||
typVorgaben GeraetetypVorlage[]
|
||
|
||
@@index([archiviert])
|
||
@@index([kategorie])
|
||
@@index([kundeId])
|
||
}
|
||
|
||
/// Eine veroeffentlichte Version wird nie wieder veraendert. "Bearbeiten"
|
||
/// erzeugt eine tiefe Kopie als neuen Entwurf; je Vorlage gibt es hoechstens
|
||
/// einen Entwurf. Laeufe verweisen immer auf eine veroeffentlichte Version.
|
||
model VorlagenVersion {
|
||
id String @id @default(cuid())
|
||
vorlageId String
|
||
version Int
|
||
status VersionStatus @default(ENTWURF)
|
||
aenderungsnotiz String?
|
||
veroeffentlichtAm DateTime?
|
||
veroeffentlichtVonId String?
|
||
erstelltAm DateTime @default(now())
|
||
/// Wird bei jeder Aenderung am Entwurf neu gesetzt und dient als Stand, den
|
||
/// der Editor beim Laden mitbekommt und beim Speichern zurueckschickt. Weicht
|
||
/// er ab, hat jemand anderes zwischenzeitlich gespeichert — ohne diese Pruefung
|
||
/// ueberschriebe der zweite Redakteur die Arbeit des ersten spurlos, und weil
|
||
/// Entwuerfe bewusst nicht protokolliert werden, waere sie auch nicht
|
||
/// wiederherstellbar.
|
||
geaendertAm DateTime @default(now()) @updatedAt
|
||
|
||
vorlage Vorlage @relation(fields: [vorlageId], references: [id], onDelete: Cascade)
|
||
veroeffentlichtVon Benutzer? @relation("VersionVeroeffentlichtVon", fields: [veroeffentlichtVonId], references: [id], onDelete: Restrict)
|
||
abschnitte VorlagenAbschnitt[]
|
||
variablen VorlagenVariable[]
|
||
laeufe Lauf[]
|
||
|
||
@@unique([vorlageId, version])
|
||
@@index([status])
|
||
}
|
||
|
||
/// Wird beim Start eines Laufs abgefragt und fuellt die Platzhalter.
|
||
model VorlagenVariable {
|
||
id String @id @default(cuid())
|
||
versionId String
|
||
schluessel String
|
||
beschriftung String
|
||
typ VariablenTyp @default(TEXT)
|
||
optionen String[] @default([])
|
||
pflicht Boolean @default(false)
|
||
position Int @default(0)
|
||
|
||
version VorlagenVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([versionId, schluessel])
|
||
@@index([versionId, position])
|
||
}
|
||
|
||
model VorlagenAbschnitt {
|
||
id String @id @default(cuid())
|
||
versionId String
|
||
titel String
|
||
position Int @default(0)
|
||
/// Verweis auf einen wiederverwendbaren Baustein (#030).
|
||
///
|
||
/// In einem **Entwurf** heisst das: beim Veroeffentlichen hier die Schritte des
|
||
/// Bausteins einsetzen. In einer **veroeffentlichten** Version heisst dasselbe
|
||
/// Feld: diese Schritte stammen von dort — es ist dann nur noch Herkunft.
|
||
/// Welche der beiden Lesarten gilt, sagt der Status der Version; ein zweites
|
||
/// Feld dafuer waere eine zweite Wahrheit.
|
||
bausteinVersionId String?
|
||
|
||
version VorlagenVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
|
||
bausteinVersion BausteinVersion? @relation(fields: [bausteinVersionId], references: [id], onDelete: Restrict)
|
||
schritte VorlagenSchritt[]
|
||
|
||
@@index([versionId, position])
|
||
@@index([bausteinVersionId])
|
||
}
|
||
|
||
// ---- Bausteine -------------------------------------------------------------
|
||
|
||
/// Ein Abschnitt, den mehrere Vorlagen teilen (#030).
|
||
///
|
||
/// Versioniert nach denselben Regeln wie eine Vorlage: hoechstens ein Entwurf,
|
||
/// eine veroeffentlichte Version wird nie veraendert. Beim Veroeffentlichen
|
||
/// einer Vorlage wird der Baustein **tief kopiert** — ein Lauf sieht damit
|
||
/// weiterhin konkrete Schritte, und Schrittergebnis, Druckansicht und Archiv
|
||
/// muessen nichts lernen.
|
||
model Baustein {
|
||
id String @id @default(cuid())
|
||
/// Eindeutig: der Name ist, wonach ein Redakteur ihn sucht.
|
||
name String @unique
|
||
beschreibung String?
|
||
archiviert Boolean @default(false)
|
||
erstelltAm DateTime @default(now())
|
||
geaendertAm DateTime @default(now()) @updatedAt
|
||
|
||
versionen BausteinVersion[]
|
||
|
||
@@index([archiviert])
|
||
}
|
||
|
||
model BausteinVersion {
|
||
id String @id @default(cuid())
|
||
bausteinId String
|
||
version Int
|
||
status VersionStatus @default(ENTWURF)
|
||
/// Titel des Abschnitts, der daraus entsteht. Steht an der Version und nicht
|
||
/// am Baustein, damit er sich aendern laesst, ohne alte Vorlagen umzuschreiben.
|
||
titel String
|
||
aenderungsnotiz String?
|
||
veroeffentlichtAm DateTime?
|
||
erstelltAm DateTime @default(now())
|
||
geaendertAm DateTime @default(now()) @updatedAt
|
||
|
||
baustein Baustein @relation(fields: [bausteinId], references: [id], onDelete: Cascade)
|
||
schritte BausteinSchritt[]
|
||
verwendungen VorlagenAbschnitt[]
|
||
|
||
@@unique([bausteinId, version])
|
||
@@index([bausteinId, status])
|
||
}
|
||
|
||
model BausteinSchritt {
|
||
id String @id @default(cuid())
|
||
versionId String
|
||
position Int @default(0)
|
||
titel String
|
||
hilfetext String?
|
||
typ SchrittTyp @default(HAKEN)
|
||
pflicht Boolean @default(false)
|
||
konfig Json @default("{}")
|
||
dokuUrl String?
|
||
|
||
version BausteinVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([versionId, position])
|
||
}
|
||
|
||
model VorlagenSchritt {
|
||
id String @id @default(cuid())
|
||
abschnittId String
|
||
position Int @default(0)
|
||
titel String
|
||
/// Markdown, in der Abarbeitungsansicht ausklappbar. Kein rohes HTML.
|
||
hilfetext String?
|
||
typ SchrittTyp @default(HAKEN)
|
||
pflicht Boolean @default(false)
|
||
/// Typabhaengige Einzelheiten: Auswahloptionen, Min/Max/Einheit, bei BEFEHL
|
||
/// shell, multiline und der Befehlstext. Ab Phase 4 ausserdem Connector und
|
||
/// Parameter fuer Schritte vom Typ AKTION.
|
||
konfig Json @default("{}")
|
||
dokuUrl String?
|
||
|
||
abschnitt VorlagenAbschnitt @relation(fields: [abschnittId], references: [id], onDelete: Cascade)
|
||
ergebnisse Schrittergebnis[]
|
||
|
||
@@index([abschnittId, position])
|
||
}
|
||
|
||
// ---- Laeufe ----------------------------------------------------------------
|
||
|
||
model Lauf {
|
||
id String @id @default(cuid())
|
||
vorlagenVersionId String
|
||
titel String
|
||
kundeId String
|
||
/// Mitarbeiter aus dem Verzeichnis. Ein Lauf ohne ist gueltig – Server,
|
||
/// Firewall, Patchday.
|
||
mitarbeiterId String?
|
||
/// Freitext, wenn die Person noch nicht im Verzeichnis existiert, etwa beim
|
||
/// Onboarding. Kann spaeter ueber den UPN mit einem Mitarbeiter verknuepft
|
||
/// werden; beim Verknuepfen wird dieses Feld geleert, weil eine Bedingung in
|
||
/// der Datenbank beides zugleich ausschliesst. Der urspruengliche Name bleibt
|
||
/// im Protokoll erhalten.
|
||
mitarbeiterName String?
|
||
geraetId String?
|
||
/// Die beim Start gewaehlte Abteilung. Speist {{ou}} und {{gruppen}}. Steht als
|
||
/// Bezug und nicht nur als Wert in variablen, damit die Druckansicht eines
|
||
/// archivierten Laufs reproduzierbar bleibt, auch wenn die Abteilung spaeter
|
||
/// umbenannt wird.
|
||
abteilungId String?
|
||
ticket String?
|
||
/// Werte der VorlagenVariablen, beim Start erfasst.
|
||
variablen Json @default("{}")
|
||
status LaufStatus @default(OFFEN)
|
||
zugewiesenAnId String?
|
||
faelligAm DateTime?
|
||
/// Leer bei einem Lauf, den die Anwendung selbst angelegt hat – wiederkehrende
|
||
/// Pruefungen und der Start per Webhook kommen in Phase 4. Kein Codepfad darf
|
||
/// annehmen, dass hier immer ein Mensch steht.
|
||
erstelltVonId String?
|
||
/// Bleibt leer, solange der Lauf nur zugewiesen ist. Wird beim ersten Oeffnen
|
||
/// zur Bearbeitung gesetzt, zusammen mit dem Wechsel auf IN_ARBEIT.
|
||
gestartetAm DateTime?
|
||
abgeschlossenAm DateTime?
|
||
abgeschlossenVonId String?
|
||
archiviertAm DateTime?
|
||
archiviertVonId String?
|
||
erstelltAm DateTime @default(now())
|
||
geaendertAm DateTime @default(now()) @updatedAt
|
||
|
||
vorlagenVersion VorlagenVersion @relation(fields: [vorlagenVersionId], references: [id], onDelete: Restrict)
|
||
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
||
mitarbeiter Mitarbeiter? @relation(fields: [mitarbeiterId], references: [id], onDelete: Restrict)
|
||
geraet Geraet? @relation(fields: [geraetId], references: [id], onDelete: Restrict)
|
||
abteilung Abteilung? @relation(fields: [abteilungId], references: [id], onDelete: Restrict)
|
||
erstelltVon Benutzer? @relation("LaufErstelltVon", fields: [erstelltVonId], references: [id], onDelete: Restrict)
|
||
zugewiesenAn Benutzer? @relation("LaufZugewiesenAn", fields: [zugewiesenAnId], references: [id], onDelete: Restrict)
|
||
abgeschlossenVon Benutzer? @relation("LaufAbgeschlossenVon", fields: [abgeschlossenVonId], references: [id], onDelete: Restrict)
|
||
archiviertVon Benutzer? @relation("LaufArchiviertVon", fields: [archiviertVonId], references: [id], onDelete: Restrict)
|
||
|
||
ergebnisse Schrittergebnis[]
|
||
kommentare Kommentar[]
|
||
protokolle Protokoll[]
|
||
|
||
@@index([kundeId, status])
|
||
@@index([geraetId, status])
|
||
@@index([mitarbeiterId])
|
||
@@index([zugewiesenAnId, status])
|
||
@@index([status, faelligAm])
|
||
@@index([vorlagenVersionId])
|
||
@@index([abteilungId])
|
||
}
|
||
|
||
model Schrittergebnis {
|
||
id String @id @default(cuid())
|
||
laufId String
|
||
vorlagenSchrittId String
|
||
zustand SchrittZustand @default(OFFEN)
|
||
/// Der erfasste Wert, je nach Schritt-Typ. Nimmt ab Phase 4 auch das Ergebnis
|
||
/// einer Aktion auf: Log, IDs, Zeitstempel.
|
||
wert Json?
|
||
/// Pflicht bei NICHT_ZUTREFFEND und FEHLGESCHLAGEN, erzwungen im Service.
|
||
notiz String?
|
||
aktualisiertVonId String?
|
||
aktualisiertAm DateTime @default(now()) @updatedAt
|
||
erstelltAm DateTime @default(now())
|
||
|
||
lauf Lauf @relation(fields: [laufId], references: [id], onDelete: Restrict)
|
||
vorlagenSchritt VorlagenSchritt @relation(fields: [vorlagenSchrittId], references: [id], onDelete: Restrict)
|
||
/// Leer, wenn der Schritt von der Anwendung selbst gesetzt wurde. Kein
|
||
/// Codepfad darf annehmen, dass immer ein Mensch dahintersteht.
|
||
aktualisiertVon Benutzer? @relation(fields: [aktualisiertVonId], references: [id], onDelete: Restrict)
|
||
kommentare Kommentar[]
|
||
|
||
@@unique([laufId, vorlagenSchrittId])
|
||
@@index([laufId, zustand])
|
||
}
|
||
|
||
model Kommentar {
|
||
id String @id @default(cuid())
|
||
laufId String
|
||
schrittergebnisId String?
|
||
benutzerId String
|
||
rumpf String
|
||
erstelltAm DateTime @default(now())
|
||
|
||
lauf Lauf @relation(fields: [laufId], references: [id], onDelete: Restrict)
|
||
schrittergebnis Schrittergebnis? @relation(fields: [schrittergebnisId], references: [id], onDelete: Restrict)
|
||
benutzer Benutzer @relation(fields: [benutzerId], references: [id], onDelete: Restrict)
|
||
|
||
@@index([laufId, erstelltAm])
|
||
@@index([schrittergebnisId])
|
||
}
|
||
|
||
// ---- Protokoll und Sperren -------------------------------------------------
|
||
|
||
/// Jede Aenderung an einem Schrittergebnis, jeder Statuswechsel eines Laufs und
|
||
/// jedes Veroeffentlichen einer Vorlage landet hier. Wird nie geloescht.
|
||
model Protokoll {
|
||
id String @id @default(cuid())
|
||
laufId String?
|
||
/// Der Kunde, um den es geht — **leer heisst global** (Einstellungen,
|
||
/// Benutzer, kundenfreie Vorlagen, Schnellstart). CLAUDE.md verlangt die
|
||
/// Spalte fuer jede kundenbezogene Zeile; ohne sie liess sich das Protokoll
|
||
/// nicht nach Mandant filtern, ohne je Eintrag die Entitaet nachzuschlagen
|
||
/// (#023).
|
||
kundeId String?
|
||
entitaet String
|
||
entitaetId String
|
||
aktion String
|
||
benutzerId String?
|
||
vorher Json?
|
||
nachher Json?
|
||
erstelltAm DateTime @default(now())
|
||
|
||
lauf Lauf? @relation(fields: [laufId], references: [id], onDelete: Restrict)
|
||
kunde Kunde? @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
||
benutzer Benutzer? @relation(fields: [benutzerId], references: [id], onDelete: Restrict)
|
||
|
||
@@index([laufId, erstelltAm])
|
||
@@index([entitaet, entitaetId])
|
||
@@index([erstelltAm])
|
||
@@index([kundeId, erstelltAm])
|
||
}
|
||
|
||
/// Sperre fuer Aufgaben, die nur einmal gleichzeitig laufen duerfen – zuerst der
|
||
/// LDAP-Abgleich. Liegt in der Datenbank, damit auch mehrere Instanzen der
|
||
/// Anwendung sich einig sind.
|
||
model AbgleichSperre {
|
||
schluessel String @id
|
||
gehaltenVon String
|
||
gehaltenSeit DateTime @default(now())
|
||
laeuftAbAm DateTime
|
||
|
||
@@index([laeuftAbAm])
|
||
}
|