Phase 2: Wartungs- und Pruefmanagement
Wartungsvorlagen mit Checklisten, Prueftermine aus einem naechtlichen Lauf, Prueffristen-Dashboard, Pruefwizard mit Foto und Unterschrift, unveraenderliche Protokolle als PDF, Maengel-Workflow, Rueckrufe und Lebensdauerueberwachung. Elf neue Tabellen, alle mit organisationId und Row Level Security. Die Unveraenderbarkeit abgeschlossener Protokolle setzt ein Datenbanktrigger durch, nicht der Anwendungscode: eine Korrektur erzeugt eine neue Pruefung mit Verweis auf die alte, beide bleiben lesbar. 79 neue Tests (47 Unit, 10 gegen eine echte Datenbank, 22 E2E). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7f866ac35c
commit
e503aafc18
+644
-67
@@ -145,27 +145,40 @@ model Organisation {
|
||||
/// Funkrufname-Präfix der Wehr, z. B. "Florian Musterhausen".
|
||||
funkPraefix String? @db.VarChar(80)
|
||||
|
||||
zeitzone String @default("Europe/Berlin") @db.VarChar(60)
|
||||
zeitzone String @default("Europe/Berlin") @db.VarChar(60)
|
||||
aktiv Boolean @default(true)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
geloeschtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
personen Person[]
|
||||
rollen Rolle[]
|
||||
personRollen PersonRolle[]
|
||||
rolleRechte RolleBerechtigung[]
|
||||
ausweisTokens AusweisToken[]
|
||||
auditEintraege AuditLog[]
|
||||
dateien Datei[]
|
||||
fahrzeuge Fahrzeug[]
|
||||
geraete Geraet[]
|
||||
standorte Standort[]
|
||||
geraeteraeume Geraeteraum[]
|
||||
geraetetypen Geraetetyp[]
|
||||
personen Person[]
|
||||
rollen Rolle[]
|
||||
personRollen PersonRolle[]
|
||||
rolleRechte RolleBerechtigung[]
|
||||
ausweisTokens AusweisToken[]
|
||||
auditEintraege AuditLog[]
|
||||
dateien Datei[]
|
||||
fahrzeuge Fahrzeug[]
|
||||
geraete Geraet[]
|
||||
standorte Standort[]
|
||||
geraeteraeume Geraeteraum[]
|
||||
geraetetypen Geraetetyp[]
|
||||
qualifikationstypen Qualifikationstyp[]
|
||||
qualifikationen Qualifikation[]
|
||||
qualifikationen Qualifikation[]
|
||||
|
||||
// Phase 2 — Wartung und Prüfung
|
||||
wartungsvorlagen Wartungsvorlage[]
|
||||
checklistenpunkte Checklistenpunkt[]
|
||||
wartungstermine Wartungstermin[]
|
||||
pruefungen Pruefung[]
|
||||
pruefergebnisse Pruefergebnis[]
|
||||
signaturen Signatur[]
|
||||
maengel Mangel[]
|
||||
rueckrufe Rueckruf[]
|
||||
rueckrufTypen RueckrufGeraetetyp[]
|
||||
rueckrufBetroffenheiten RueckrufBetroffenheit[]
|
||||
erinnerungen Erinnerung[]
|
||||
|
||||
@@index([aktiv])
|
||||
@@map("organisation")
|
||||
@@ -182,16 +195,16 @@ model Person {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
vorname String @db.VarChar(100)
|
||||
nachname String @db.VarChar(100)
|
||||
vorname String @db.VarChar(100)
|
||||
nachname String @db.VarChar(100)
|
||||
/// Fortlaufende Mitgliedsnummer innerhalb der Wehr.
|
||||
mitgliedsnummer String? @db.VarChar(30)
|
||||
geburtsdatum DateTime? @db.Date
|
||||
dienstgrad Dienstgrad @default(KEIN)
|
||||
mitgliedsnummer String? @db.VarChar(30)
|
||||
geburtsdatum DateTime? @db.Date
|
||||
dienstgrad Dienstgrad @default(KEIN)
|
||||
|
||||
email String? @db.VarChar(200)
|
||||
email String? @db.VarChar(200)
|
||||
telefon String? @db.VarChar(40)
|
||||
mobil String? @db.VarChar(40)
|
||||
mobil String? @db.VarChar(40)
|
||||
|
||||
strasse String? @db.VarChar(200)
|
||||
plz String? @db.VarChar(10)
|
||||
@@ -222,16 +235,24 @@ model Person {
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
geloeschtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
avatar Datei? @relation("PersonAvatar", fields: [avatarDateiId], references: [id], onDelete: SetNull)
|
||||
rollen PersonRolle[] @relation("PersonRollenInhaber")
|
||||
vergebeneRollen PersonRolle[] @relation("PersonRollenVergeber")
|
||||
ausweisTokens AusweisToken[]
|
||||
auditEintraege AuditLog[]
|
||||
hochgeladeneDateien Datei[] @relation("DateiHochgeladenVon")
|
||||
qualifikationen Qualifikation[] @relation("QualifikationInhaber")
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
avatar Datei? @relation("PersonAvatar", fields: [avatarDateiId], references: [id], onDelete: SetNull)
|
||||
rollen PersonRolle[] @relation("PersonRollenInhaber")
|
||||
vergebeneRollen PersonRolle[] @relation("PersonRollenVergeber")
|
||||
ausweisTokens AusweisToken[]
|
||||
auditEintraege AuditLog[]
|
||||
hochgeladeneDateien Datei[] @relation("DateiHochgeladenVon")
|
||||
qualifikationen Qualifikation[] @relation("QualifikationInhaber")
|
||||
bestaetigteQualifikationen Qualifikation[] @relation("QualifikationBestaetiger")
|
||||
|
||||
// Phase 2 — Wartung und Prüfung
|
||||
zugewieseneTermine Wartungstermin[] @relation("TerminZustaendig")
|
||||
durchgefuehrtePruefungen Pruefung[] @relation("PruefungPruefer")
|
||||
signaturen Signatur[] @relation("SignaturUnterzeichner")
|
||||
gemeldeteMaengel Mangel[] @relation("MangelMelder")
|
||||
zugewieseneMaengel Mangel[] @relation("MangelZustaendig")
|
||||
behobeneMaengel Mangel[] @relation("MangelBehebung")
|
||||
|
||||
/// E-Mail und OIDC-Subject sind je Wehr eindeutig, nicht global — dieselbe
|
||||
/// Person kann in zwei Wehren Mitglied sein.
|
||||
@@unique([organisationId, email])
|
||||
@@ -249,11 +270,11 @@ model Rolle {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
schluessel RolleSchluessel
|
||||
name String @db.VarChar(100)
|
||||
beschreibung String? @db.VarChar(500)
|
||||
schluessel RolleSchluessel
|
||||
name String @db.VarChar(100)
|
||||
beschreibung String? @db.VarChar(500)
|
||||
/// Systemrollen dürfen nicht gelöscht werden.
|
||||
systemRolle Boolean @default(false)
|
||||
systemRolle Boolean @default(false)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
@@ -275,14 +296,14 @@ model PersonRolle {
|
||||
personId String @db.Uuid
|
||||
rolleId String @db.Uuid
|
||||
|
||||
zugewiesenAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
zugewiesenVonId String? @db.Uuid
|
||||
zugewiesenAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
zugewiesenVonId String? @db.Uuid
|
||||
/// Befristete Zuweisung, z. B. Vertretung während Urlaub.
|
||||
gueltigBis DateTime? @db.Timestamptz(3)
|
||||
gueltigBis DateTime? @db.Timestamptz(3)
|
||||
/// true, wenn die Rolle aus einer authentik-Gruppe vorbelegt wurde. Sobald
|
||||
/// jemand sie in FLORIAN ändert, steht hier false und OIDC überschreibt nicht
|
||||
/// mehr.
|
||||
ausOidcGruppe Boolean @default(false)
|
||||
ausOidcGruppe Boolean @default(false)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
person Person @relation("PersonRollenInhaber", fields: [personId], references: [id], onDelete: Cascade)
|
||||
@@ -304,11 +325,11 @@ model PersonRolle {
|
||||
/// trägt organisationId. Die Tabelle ist deshalb für alle lesbar und für die
|
||||
/// Anwendungsrolle nicht beschreibbar.
|
||||
model Berechtigung {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
schluessel String @unique @db.VarChar(80)
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
schluessel String @unique @db.VarChar(80)
|
||||
/// Fachbereich für die Gruppierung in der Oberfläche.
|
||||
bereich String @db.VarChar(60)
|
||||
bezeichnung String @db.VarChar(160)
|
||||
bereich String @db.VarChar(60)
|
||||
bezeichnung String @db.VarChar(160)
|
||||
beschreibung String? @db.VarChar(500)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
@@ -350,17 +371,17 @@ model AusweisToken {
|
||||
|
||||
typ AusweisTokenTyp
|
||||
/// bcrypt-Hash der PIN bzw. der RFID-UID.
|
||||
tokenHash String @db.VarChar(120)
|
||||
tokenHash String @db.VarChar(120)
|
||||
/// Letzte vier Stellen der Karten-UID, damit man Karten unterscheiden kann.
|
||||
kennzeichen String? @db.VarChar(20)
|
||||
bezeichnung String? @db.VarChar(120)
|
||||
kennzeichen String? @db.VarChar(20)
|
||||
bezeichnung String? @db.VarChar(120)
|
||||
|
||||
aktiv Boolean @default(true)
|
||||
gueltigAb DateTime @default(now()) @db.Timestamptz(3)
|
||||
gueltigBis DateTime? @db.Timestamptz(3)
|
||||
letzteNutzung DateTime? @db.Timestamptz(3)
|
||||
fehlversuche Int @default(0)
|
||||
gesperrtBis DateTime? @db.Timestamptz(3)
|
||||
aktiv Boolean @default(true)
|
||||
gueltigAb DateTime @default(now()) @db.Timestamptz(3)
|
||||
gueltigBis DateTime? @db.Timestamptz(3)
|
||||
letzteNutzung DateTime? @db.Timestamptz(3)
|
||||
fehlversuche Int @default(0)
|
||||
gesperrtBis DateTime? @db.Timestamptz(3)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
@@ -387,19 +408,19 @@ model AuditLog {
|
||||
|
||||
zeitpunkt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
akteurTyp AkteurTyp @default(PERSON)
|
||||
akteurPersonId String? @db.Uuid
|
||||
akteurTyp AkteurTyp @default(PERSON)
|
||||
akteurPersonId String? @db.Uuid
|
||||
/// Klarname zum Zeitpunkt der Aktion — bleibt lesbar, auch wenn die Person
|
||||
/// später anonymisiert wird.
|
||||
akteurName String? @db.VarChar(200)
|
||||
akteurName String? @db.VarChar(200)
|
||||
anmeldeArt AnmeldeArt?
|
||||
|
||||
aktion AuditAktion
|
||||
aktion AuditAktion
|
||||
/// Modellname, z. B. "Geraet".
|
||||
entitaet String @db.VarChar(80)
|
||||
entitaetId String? @db.Uuid
|
||||
entitaet String @db.VarChar(80)
|
||||
entitaetId String? @db.Uuid
|
||||
/// Menschenlesbare Kurzfassung für die Oberfläche.
|
||||
beschreibung String? @db.VarChar(500)
|
||||
beschreibung String? @db.VarChar(500)
|
||||
|
||||
vorher Json? @db.JsonB
|
||||
nachher Json? @db.JsonB
|
||||
@@ -428,15 +449,15 @@ model Datei {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
bucket String @db.VarChar(80)
|
||||
bucket String @db.VarChar(80)
|
||||
/// Objektschlüssel in MinIO, z. B. "ff-musterhausen/geraet/<uuid>/foto.jpg".
|
||||
objektSchluessel String @db.VarChar(500)
|
||||
|
||||
dateiname String @db.VarChar(300)
|
||||
mimeTyp String @db.VarChar(160)
|
||||
dateiname String @db.VarChar(300)
|
||||
mimeTyp String @db.VarChar(160)
|
||||
groesseBytes BigInt
|
||||
/// SHA-256 des Inhalts. Erkennt Dubletten und belegt Unverändertheit.
|
||||
pruefsumme String? @db.VarChar(64)
|
||||
pruefsumme String? @db.VarChar(64)
|
||||
|
||||
/// Woran die Datei hängt (z. B. "Geraet") und an welchem Datensatz.
|
||||
entitaet String? @db.VarChar(80)
|
||||
@@ -444,15 +465,22 @@ model Datei {
|
||||
/// Freie Einordnung: "pruefprotokoll", "foto", "rechnung", "export".
|
||||
kategorie String? @db.VarChar(60)
|
||||
|
||||
hochgeladenVonId String? @db.Uuid
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
hochgeladenVonId String? @db.Uuid
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
/// Soft-Delete: das Objekt in MinIO räumt erst der Aufbewahrungs-Job ab.
|
||||
geloeschtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
hochgeladenVon Person? @relation("DateiHochgeladenVon", fields: [hochgeladenVonId], references: [id], onDelete: SetNull)
|
||||
alsAvatarGenutzt Person[] @relation("PersonAvatar")
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
hochgeladenVon Person? @relation("DateiHochgeladenVon", fields: [hochgeladenVonId], references: [id], onDelete: SetNull)
|
||||
alsAvatarGenutzt Person[] @relation("PersonAvatar")
|
||||
|
||||
// Phase 2 — woran eine Datei sonst noch hängen kann
|
||||
alsPruefprotokoll Pruefung[] @relation("PruefungProtokoll")
|
||||
alsFremdprotokoll Pruefung[] @relation("PruefungFremdprotokoll")
|
||||
alsErgebnisFoto Pruefergebnis[] @relation("PruefergebnisFoto")
|
||||
alsSignaturbild Signatur[] @relation("SignaturBild")
|
||||
alsMangelFoto Mangel[] @relation("MangelFoto")
|
||||
|
||||
@@unique([bucket, objektSchluessel])
|
||||
@@index([organisationId, entitaet, entitaetId])
|
||||
@@ -688,6 +716,10 @@ model Geraetetyp {
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
geraete Geraet[]
|
||||
|
||||
// Phase 2
|
||||
wartungsvorlagen Wartungsvorlage[]
|
||||
rueckrufTypen RueckrufGeraetetyp[]
|
||||
|
||||
@@unique([organisationId, name])
|
||||
@@index([organisationId, kategorie])
|
||||
@@index([organisationId, aktiv])
|
||||
@@ -746,6 +778,12 @@ model Geraet {
|
||||
fahrzeug Fahrzeug? @relation(fields: [fahrzeugId], references: [id], onDelete: SetNull)
|
||||
geraeteraum Geraeteraum? @relation(fields: [geraeteraumId], references: [id], onDelete: SetNull)
|
||||
|
||||
// Phase 2 — alles, was am Gerät geprüft, gemeldet und zurückgerufen wird
|
||||
wartungstermine Wartungstermin[]
|
||||
pruefungen Pruefung[]
|
||||
maengel Mangel[]
|
||||
rueckrufBetroffenheiten RueckrufBetroffenheit[]
|
||||
|
||||
@@unique([organisationId, inventarnummer])
|
||||
@@index([organisationId, status])
|
||||
@@index([organisationId, naechstePruefung])
|
||||
@@ -826,3 +864,542 @@ model Qualifikation {
|
||||
@@index([organisationId, gueltigBis])
|
||||
@@map("qualifikation")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Phase 2 — Wartungs- und Prüfmanagement
|
||||
//
|
||||
// Ordnungsprinzip:
|
||||
// Wartungsvorlage was geprüft wird und in welchem Takt — je Gerätetyp
|
||||
// Checklistenpunkt eine einzelne Frage der Prüfung
|
||||
// Wartungstermin ein konkreter Prüftermin an einem konkreten Gerät
|
||||
// Pruefung die durchgeführte Prüfung samt Ergebnis
|
||||
// Pruefergebnis die Antwort auf einen Checklistenpunkt
|
||||
// Signatur die Unterschrift unter das Protokoll
|
||||
// Mangel was dabei auffiel und noch behoben werden muss
|
||||
// Rueckruf eine Herstellerwarnung und die betroffenen Geräte
|
||||
//
|
||||
// Zwei Festlegungen, die den Aufbau erklären:
|
||||
//
|
||||
// 1. Ein abgeschlossenes Prüfprotokoll ist unveränderbar. Ein Trigger in der
|
||||
// Datenbank lässt an einer Prüfung mit Status ABGESCHLOSSEN keine
|
||||
// Änderung mehr zu — außer der Kennzeichnung als ERSETZT. Eine Korrektur
|
||||
// ist eine neue Prüfung, die über ersetztPruefungId auf die alte zeigt.
|
||||
//
|
||||
// 2. "Überfällig" ist kein gespeicherter Zustand, sondern ergibt sich aus
|
||||
// faelligAm und dem heutigen Tag. Gespeicherte Zustände laufen der Uhr
|
||||
// hinterher; berechnete nicht.
|
||||
// =============================================================================
|
||||
|
||||
/// Zustand eines Prüftermins. Überfälligkeit steht bewusst nicht hier drin.
|
||||
enum WartungsterminStatus {
|
||||
OFFEN
|
||||
ERLEDIGT
|
||||
/// Der Termin ist gegenstandslos geworden — Gerät ausgesondert, Vorlage
|
||||
/// abgeschafft, Prüfpflicht entfallen.
|
||||
ENTFALLEN
|
||||
}
|
||||
|
||||
/// Lebenszyklus einer Prüfung.
|
||||
enum PruefungStatus {
|
||||
/// Der Wizard läuft noch. Nur in diesem Zustand ist Ändern erlaubt.
|
||||
ENTWURF
|
||||
/// Unterschrieben und festgeschrieben.
|
||||
ABGESCHLOSSEN
|
||||
/// Durch eine Korrekturprüfung abgelöst. Bleibt erhalten und lesbar.
|
||||
ERSETZT
|
||||
}
|
||||
|
||||
/// Ergebnis einer Prüfung nach DGUV-Logik.
|
||||
enum PruefErgebnis {
|
||||
BESTANDEN
|
||||
BESTANDEN_MIT_MAENGELN
|
||||
NICHT_BESTANDEN
|
||||
}
|
||||
|
||||
/// Wer geprüft hat — die eigene Wehr oder eine Fachfirma.
|
||||
enum PruefungArt {
|
||||
INTERN
|
||||
EXTERN
|
||||
}
|
||||
|
||||
/// Art eines Checklistenpunkts. Bestimmt, welches Eingabefeld der Wizard zeigt.
|
||||
enum ChecklistenpunktTyp {
|
||||
JA_NEIN
|
||||
MESSWERT
|
||||
TEXT
|
||||
FOTO
|
||||
}
|
||||
|
||||
/// In welcher Eigenschaft jemand unterschreibt.
|
||||
enum SignaturRolle {
|
||||
PRUEFER
|
||||
GERAETEWART
|
||||
/// Bei externer Wartung: der Vertreter der Fachfirma.
|
||||
FREMDFIRMA
|
||||
}
|
||||
|
||||
/// Dringlichkeit eines Mangels.
|
||||
enum MangelPrioritaet {
|
||||
NIEDRIG
|
||||
MITTEL
|
||||
HOCH
|
||||
/// Gerät ist nicht mehr einsatzbereit.
|
||||
KRITISCH
|
||||
}
|
||||
|
||||
/// Bearbeitungsstand eines Mangels: melden → zugewiesen → in Arbeit → behoben.
|
||||
enum MangelStatus {
|
||||
GEMELDET
|
||||
ZUGEWIESEN
|
||||
IN_ARBEIT
|
||||
BEHOBEN
|
||||
/// Kein Mangel — Fehlmeldung, bleibt aber nachvollziehbar stehen.
|
||||
VERWORFEN
|
||||
}
|
||||
|
||||
/// Bearbeitungsstand eines Rückrufs.
|
||||
enum RueckrufStatus {
|
||||
OFFEN
|
||||
IN_BEARBEITUNG
|
||||
ERLEDIGT
|
||||
}
|
||||
|
||||
/// Stand je betroffenem Gerät innerhalb eines Rückrufs.
|
||||
enum BetroffenheitStatus {
|
||||
OFFEN
|
||||
/// Geprüft und doch nicht betroffen — falsche Charge, anderes Baujahr.
|
||||
NICHT_BETROFFEN
|
||||
MASSNAHME_LAEUFT
|
||||
ERLEDIGT
|
||||
}
|
||||
|
||||
/// Vorwarnstufe einer Erinnerung. Je Termin und Stufe wird höchstens einmal
|
||||
/// erinnert — der Eintrag ist gleichzeitig die Sperre gegen Doppelversand.
|
||||
enum ErinnerungStufe {
|
||||
T_30
|
||||
T_7
|
||||
T_0
|
||||
UEBERFAELLIG
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/// Was an einem Gerätetyp regelmäßig zu prüfen ist.
|
||||
///
|
||||
/// Die Vorlage ist die Regel, der Wartungstermin ihre Anwendung auf ein
|
||||
/// einzelnes Gerät. Ändert sich die Vorlage, ändern sich künftige Termine —
|
||||
/// bereits abgeschlossene Prüfungen bleiben unberührt, weil ihre Ergebnisse
|
||||
/// samt Fragestellung erhalten bleiben.
|
||||
model Wartungsvorlage {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
name String @db.VarChar(160)
|
||||
/// Für welchen Gerätetyp sie gilt. Leer bedeutet: nur von Hand zuweisbar.
|
||||
geraetetypId String? @db.Uuid
|
||||
|
||||
/// Prüfintervall in Monaten. Zählt ab der letzten durchgeführten Prüfung.
|
||||
intervallMonate Int
|
||||
/// Wie viele Tage vorher gewarnt und erinnert wird.
|
||||
vorwarnzeitTage Int @default(30)
|
||||
/// Fundstelle, z. B. "DGUV Vorschrift 49" oder "DIN 14800-16".
|
||||
pruefvorschrift String? @db.VarChar(200)
|
||||
|
||||
/// Erfahrungswert für die Planung, in Minuten.
|
||||
standardDauerMinuten Int?
|
||||
/// Vorbelegung: diese Prüfung macht üblicherweise eine Fachfirma.
|
||||
standardExtern Boolean @default(false)
|
||||
/// Der nächtliche Lauf erzeugt Termine für alle Geräte dieses Typs.
|
||||
automatisch Boolean @default(true)
|
||||
|
||||
beschreibung String? @db.VarChar(1000)
|
||||
aktiv Boolean @default(true)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
geloeschtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
geraetetyp Geraetetyp? @relation(fields: [geraetetypId], references: [id], onDelete: SetNull)
|
||||
punkte Checklistenpunkt[]
|
||||
termine Wartungstermin[]
|
||||
pruefungen Pruefung[]
|
||||
|
||||
@@unique([organisationId, name])
|
||||
@@index([organisationId, geraetetypId])
|
||||
@@index([organisationId, aktiv])
|
||||
@@map("wartungsvorlage")
|
||||
}
|
||||
|
||||
/// Eine einzelne Frage der Checkliste.
|
||||
model Checklistenpunkt {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
vorlageId String @db.Uuid
|
||||
|
||||
text String @db.VarChar(300)
|
||||
typ ChecklistenpunktTyp @default(JA_NEIN)
|
||||
/// Pflichtpunkte müssen beantwortet sein, bevor sich die Prüfung
|
||||
/// abschließen lässt.
|
||||
pflicht Boolean @default(true)
|
||||
|
||||
/// Sollwert als Klartext für die Anzeige, z. B. "mindestens 270 bar".
|
||||
sollwert String? @db.VarChar(120)
|
||||
/// Einheit eines Messwerts, z. B. "bar", "mbar", "V", "kN".
|
||||
einheit String? @db.VarChar(20)
|
||||
/// Zulässiger Bereich eines Messwerts. Außerhalb davon gilt der Punkt als
|
||||
/// nicht in Ordnung — gerechnet wird das im Server, nicht im Browser.
|
||||
minWert Decimal? @db.Decimal(12, 3)
|
||||
maxWert Decimal? @db.Decimal(12, 3)
|
||||
|
||||
hinweis String? @db.VarChar(500)
|
||||
sortierung Int @default(0)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
geloeschtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
vorlage Wartungsvorlage @relation(fields: [vorlageId], references: [id], onDelete: Cascade)
|
||||
ergebnisse Pruefergebnis[]
|
||||
|
||||
@@index([organisationId, vorlageId])
|
||||
@@map("checklistenpunkt")
|
||||
}
|
||||
|
||||
/// Ein fälliger Prüftermin an einem einzelnen Gerät.
|
||||
///
|
||||
/// Termine entstehen entweder im nächtlichen Lauf aus den Vorlagen oder von
|
||||
/// Hand. Die Eindeutigkeit über Gerät, Vorlage und Fälligkeitstag verhindert,
|
||||
/// dass ein zweiter Lauf denselben Termin noch einmal anlegt.
|
||||
model Wartungstermin {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
geraetId String @db.Uuid
|
||||
vorlageId String? @db.Uuid
|
||||
|
||||
faelligAm DateTime @db.Date
|
||||
status WartungsterminStatus @default(OFFEN)
|
||||
|
||||
/// true, wenn der nächtliche Lauf den Termin erzeugt hat.
|
||||
automatischErzeugt Boolean @default(false)
|
||||
/// Wer den Termin übernehmen soll.
|
||||
zugewiesenAnId String? @db.Uuid
|
||||
|
||||
bemerkung String? @db.VarChar(1000)
|
||||
erledigtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
geloeschtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
geraet Geraet @relation(fields: [geraetId], references: [id], onDelete: Cascade)
|
||||
vorlage Wartungsvorlage? @relation(fields: [vorlageId], references: [id], onDelete: SetNull)
|
||||
zugewiesenAn Person? @relation("TerminZustaendig", fields: [zugewiesenAnId], references: [id], onDelete: SetNull)
|
||||
pruefungen Pruefung[]
|
||||
erinnerungen Erinnerung[]
|
||||
|
||||
/// Doppelte Termine aus wiederholten Läufen ausschließen. Für Termine ohne
|
||||
/// Vorlage greift die Bedingung nicht — Postgres wertet NULL als
|
||||
/// unterschiedlich —, die legt aber ohnehin nur ein Mensch an.
|
||||
@@unique([geraetId, vorlageId, faelligAm])
|
||||
@@index([organisationId, faelligAm])
|
||||
@@index([organisationId, status, faelligAm])
|
||||
@@index([organisationId, geraetId])
|
||||
@@index([organisationId, zugewiesenAnId])
|
||||
@@map("wartungstermin")
|
||||
}
|
||||
|
||||
/// Eine durchgeführte Prüfung.
|
||||
///
|
||||
/// Ist der Status ABGESCHLOSSEN, ist der Datensatz festgeschrieben: die
|
||||
/// Datenbank weist jede weitere Änderung ab. Was korrigiert werden muss, wird
|
||||
/// als neue Prüfung erfasst, die über ersetztPruefungId auf die alte zeigt.
|
||||
model Pruefung {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
geraetId String @db.Uuid
|
||||
terminId String? @db.Uuid
|
||||
vorlageId String? @db.Uuid
|
||||
|
||||
art PruefungArt @default(INTERN)
|
||||
status PruefungStatus @default(ENTWURF)
|
||||
|
||||
prueferId String? @db.Uuid
|
||||
/// Klarname des Prüfers zum Zeitpunkt der Prüfung. Bleibt lesbar, auch wenn
|
||||
/// die Person später austritt und anonymisiert wird — die Nachweispflicht
|
||||
/// gegenüber der Unfallkasse verlangt einen benennbaren Prüfer.
|
||||
prueferName String? @db.VarChar(200)
|
||||
|
||||
durchgefuehrtAm DateTime @db.Date
|
||||
/// Beginn der Zeiterfassung im Wizard.
|
||||
begonnenUm DateTime? @db.Timestamptz(3)
|
||||
dauerMinuten Int?
|
||||
|
||||
ergebnis PruefErgebnis?
|
||||
|
||||
/// Nur bei art = EXTERN: wer geprüft hat, was es gekostet hat und das
|
||||
/// Protokoll der Fachfirma als PDF.
|
||||
externeFirma String? @db.VarChar(200)
|
||||
externeProtokollDateiId String? @db.Uuid
|
||||
kosten Decimal? @db.Decimal(10, 2)
|
||||
|
||||
/// Nächster Termin laut dieser Prüfung. Kann vom Intervall abweichen, wenn
|
||||
/// der Prüfer eine kürzere Frist ansetzt.
|
||||
naechstePruefungAm DateTime? @db.Date
|
||||
|
||||
/// Das erzeugte Protokoll-PDF in MinIO und seine laufende Nummer.
|
||||
protokollDateiId String? @db.Uuid
|
||||
protokollNummer String? @db.VarChar(40)
|
||||
|
||||
abgeschlossenAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
/// Korrektur: diese Prüfung ersetzt eine frühere.
|
||||
ersetztPruefungId String? @db.Uuid
|
||||
korrekturGrund String? @db.VarChar(500)
|
||||
|
||||
bemerkung String? @db.VarChar(2000)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
geloeschtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
geraet Geraet @relation(fields: [geraetId], references: [id], onDelete: Cascade)
|
||||
termin Wartungstermin? @relation(fields: [terminId], references: [id], onDelete: SetNull)
|
||||
vorlage Wartungsvorlage? @relation(fields: [vorlageId], references: [id], onDelete: SetNull)
|
||||
pruefer Person? @relation("PruefungPruefer", fields: [prueferId], references: [id], onDelete: SetNull)
|
||||
protokoll Datei? @relation("PruefungProtokoll", fields: [protokollDateiId], references: [id], onDelete: SetNull)
|
||||
externesProtokoll Datei? @relation("PruefungFremdprotokoll", fields: [externeProtokollDateiId], references: [id], onDelete: SetNull)
|
||||
ersetzt Pruefung? @relation("PruefungKorrektur", fields: [ersetztPruefungId], references: [id], onDelete: SetNull)
|
||||
ersetztDurch Pruefung[] @relation("PruefungKorrektur")
|
||||
ergebnisse Pruefergebnis[]
|
||||
signaturen Signatur[]
|
||||
maengel Mangel[]
|
||||
|
||||
@@unique([organisationId, protokollNummer])
|
||||
@@index([organisationId, geraetId, durchgefuehrtAm(sort: Desc)])
|
||||
@@index([organisationId, status])
|
||||
@@index([organisationId, durchgefuehrtAm(sort: Desc)])
|
||||
@@index([organisationId, prueferId])
|
||||
@@map("pruefung")
|
||||
}
|
||||
|
||||
/// Die Antwort auf einen Checklistenpunkt.
|
||||
model Pruefergebnis {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
pruefungId String @db.Uuid
|
||||
punktId String @db.Uuid
|
||||
|
||||
/// Je nach Typ des Punktes ist eines der drei Felder belegt.
|
||||
jaNein Boolean?
|
||||
zahlwert Decimal? @db.Decimal(12, 3)
|
||||
text String? @db.VarChar(1000)
|
||||
|
||||
fotoDateiId String? @db.Uuid
|
||||
|
||||
/// Ob der Punkt in Ordnung ist. Bei Messwerten rechnet der Server das aus
|
||||
/// dem zulässigen Bereich aus, sonst folgt es der Antwort.
|
||||
inOrdnung Boolean @default(true)
|
||||
bemerkung String? @db.VarChar(500)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
pruefung Pruefung @relation(fields: [pruefungId], references: [id], onDelete: Cascade)
|
||||
punkt Checklistenpunkt @relation(fields: [punktId], references: [id], onDelete: Restrict)
|
||||
foto Datei? @relation("PruefergebnisFoto", fields: [fotoDateiId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@unique([pruefungId, punktId])
|
||||
@@index([organisationId, pruefungId])
|
||||
@@map("pruefergebnis")
|
||||
}
|
||||
|
||||
/// Unterschrift unter ein Prüfprotokoll. Das Bild ist ein PNG aus dem
|
||||
/// Zeichenfeld und liegt wie alles andere in MinIO.
|
||||
model Signatur {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
pruefungId String @db.Uuid
|
||||
|
||||
rolle SignaturRolle @default(PRUEFER)
|
||||
personId String? @db.Uuid
|
||||
/// Klarname, so wie er im Protokoll steht.
|
||||
name String @db.VarChar(200)
|
||||
|
||||
bildDateiId String? @db.Uuid
|
||||
unterzeichnetAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
pruefung Pruefung @relation(fields: [pruefungId], references: [id], onDelete: Cascade)
|
||||
person Person? @relation("SignaturUnterzeichner", fields: [personId], references: [id], onDelete: SetNull)
|
||||
bild Datei? @relation("SignaturBild", fields: [bildDateiId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([organisationId, pruefungId])
|
||||
@@map("signatur")
|
||||
}
|
||||
|
||||
/// Ein Mangel an einem Gerät — gemeldet aus einer Prüfung heraus oder einzeln.
|
||||
model Mangel {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
geraetId String @db.Uuid
|
||||
/// Gesetzt, wenn der Mangel bei einer Prüfung aufgefallen ist.
|
||||
pruefungId String? @db.Uuid
|
||||
|
||||
melderId String? @db.Uuid
|
||||
melderName String? @db.VarChar(200)
|
||||
|
||||
beschreibung String @db.VarChar(2000)
|
||||
fotoDateiId String? @db.Uuid
|
||||
|
||||
prioritaet MangelPrioritaet @default(MITTEL)
|
||||
status MangelStatus @default(GEMELDET)
|
||||
|
||||
zugewiesenAnId String? @db.Uuid
|
||||
gemeldetAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
behobenAm DateTime? @db.Timestamptz(3)
|
||||
behobenVonId String? @db.Uuid
|
||||
behebung String? @db.VarChar(2000)
|
||||
|
||||
/// Beim Melden wurde das Gerät gesperrt. Merkt sich, dass die Sperre zu
|
||||
/// diesem Mangel gehört — beim Beheben wird sie wieder aufgehoben.
|
||||
geraetGesperrt Boolean @default(false)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
geloeschtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
geraet Geraet @relation(fields: [geraetId], references: [id], onDelete: Cascade)
|
||||
pruefung Pruefung? @relation(fields: [pruefungId], references: [id], onDelete: SetNull)
|
||||
melder Person? @relation("MangelMelder", fields: [melderId], references: [id], onDelete: SetNull)
|
||||
zugewiesenAn Person? @relation("MangelZustaendig", fields: [zugewiesenAnId], references: [id], onDelete: SetNull)
|
||||
behobenVon Person? @relation("MangelBehebung", fields: [behobenVonId], references: [id], onDelete: SetNull)
|
||||
foto Datei? @relation("MangelFoto", fields: [fotoDateiId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([organisationId, status, prioritaet])
|
||||
@@index([organisationId, geraetId])
|
||||
@@index([organisationId, zugewiesenAnId])
|
||||
@@map("mangel")
|
||||
}
|
||||
|
||||
/// Eine Herstellerwarnung. Betroffene Geräte werden gesucht, markiert und auf
|
||||
/// Wunsch gesperrt.
|
||||
model Rueckruf {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
hersteller String @db.VarChar(160)
|
||||
titel String @db.VarChar(200)
|
||||
/// Nummer der Bekanntmachung des Herstellers.
|
||||
referenz String? @db.VarChar(80)
|
||||
|
||||
beschreibung String? @db.VarChar(4000)
|
||||
massnahme String? @db.VarChar(2000)
|
||||
|
||||
veroeffentlichtAm DateTime? @db.Date
|
||||
/// Fundstelle, z. B. die Adresse der Herstellermitteilung.
|
||||
quelle String? @db.VarChar(500)
|
||||
|
||||
/// Betroffene Geräte beim Zuordnen automatisch sperren.
|
||||
sperrtGeraete Boolean @default(false)
|
||||
status RueckrufStatus @default(OFFEN)
|
||||
erledigtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
geloeschtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
typen RueckrufGeraetetyp[]
|
||||
betroffenheiten RueckrufBetroffenheit[]
|
||||
|
||||
@@index([organisationId, status])
|
||||
@@index([organisationId, hersteller])
|
||||
@@map("rueckruf")
|
||||
}
|
||||
|
||||
/// Welche Bauart und welche Charge ein Rückruf betrifft.
|
||||
model RueckrufGeraetetyp {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
rueckrufId String @db.Uuid
|
||||
geraetetypId String? @db.Uuid
|
||||
|
||||
/// Freitext, wenn die betroffene Bauart nicht als Gerätetyp erfasst ist.
|
||||
modell String? @db.VarChar(160)
|
||||
|
||||
/// Eingrenzung der betroffenen Stücke. Alle Angaben sind freiwillig; was
|
||||
/// leer bleibt, schränkt nicht ein.
|
||||
seriennummerVon String? @db.VarChar(120)
|
||||
seriennummerBis String? @db.VarChar(120)
|
||||
herstellungVon DateTime? @db.Date
|
||||
herstellungBis DateTime? @db.Date
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
rueckruf Rueckruf @relation(fields: [rueckrufId], references: [id], onDelete: Cascade)
|
||||
geraetetyp Geraetetyp? @relation(fields: [geraetetypId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([organisationId, rueckrufId])
|
||||
@@map("rueckruf_geraetetyp")
|
||||
}
|
||||
|
||||
/// Ein einzelnes Gerät innerhalb eines Rückrufs.
|
||||
model RueckrufBetroffenheit {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
rueckrufId String @db.Uuid
|
||||
geraetId String @db.Uuid
|
||||
|
||||
status BetroffenheitStatus @default(OFFEN)
|
||||
festgestelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
erledigtAm DateTime? @db.Timestamptz(3)
|
||||
bemerkung String? @db.VarChar(1000)
|
||||
|
||||
/// Der Rückruf hat dieses Gerät gesperrt — beim Erledigen wird die Sperre
|
||||
/// wieder aufgehoben.
|
||||
geraetGesperrt Boolean @default(false)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
rueckruf Rueckruf @relation(fields: [rueckrufId], references: [id], onDelete: Cascade)
|
||||
geraet Geraet @relation(fields: [geraetId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([rueckrufId, geraetId])
|
||||
@@index([organisationId, rueckrufId])
|
||||
@@index([organisationId, geraetId])
|
||||
@@map("rueckruf_betroffenheit")
|
||||
}
|
||||
|
||||
/// Nachweis einer versendeten Erinnerung. Der Eintrag ist gleichzeitig die
|
||||
/// Sperre gegen Doppelversand: je Termin und Stufe gibt es ihn nur einmal.
|
||||
model Erinnerung {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
terminId String @db.Uuid
|
||||
|
||||
stufe ErinnerungStufe
|
||||
versendetAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
/// Kanal laut lib/integrations — "mail", "telegram", "protokoll".
|
||||
kanal String @default("mail") @db.VarChar(40)
|
||||
empfaenger String[] @default([])
|
||||
erfolgreich Boolean @default(true)
|
||||
meldung String? @db.VarChar(500)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
termin Wartungstermin @relation(fields: [terminId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([terminId, stufe])
|
||||
@@index([organisationId, versendetAm(sort: Desc)])
|
||||
@@map("erinnerung")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user