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>
1406 lines
51 KiB
Plaintext
1406 lines
51 KiB
Plaintext
// =============================================================================
|
|
// FLORIAN — Datenmodell, Phase 0 (Fundament)
|
|
//
|
|
// Grundregeln, die für JEDES Modell gelten:
|
|
// 1. Mandantentrennung: jede Tabelle trägt organisationId. Postgres Row Level
|
|
// Security setzt das durch (siehe migrations/.../rls). Ausnahme ist der
|
|
// systemweite Berechtigungskatalog, siehe Kommentar dort.
|
|
// 2. Jede schreibende Operation erzeugt einen AuditLog-Eintrag (lib/audit.ts).
|
|
// 3. Prüf-, Wartungs- und Buchungsdaten werden nie hart gelöscht:
|
|
// geloeschtAm setzen statt DELETE.
|
|
// 4. Alle Zeitstempel in UTC speichern, Anzeige in Europe/Berlin.
|
|
//
|
|
// Bezeichner sind englisch, wo es Technik ist, und deutsch, wo es Fachlichkeit
|
|
// ist — so wie es in der Wehr auch gesprochen wird.
|
|
// =============================================================================
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
binaryTargets = ["native", "linux-musl-openssl-3.0.x", "debian-openssl-3.0.x"]
|
|
previewFeatures = ["postgresqlExtensions"]
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
directUrl = env("DIRECT_DATABASE_URL")
|
|
extensions = [postgis, pgcrypto, citext, unaccent, pg_trgm(map: "pg_trgm")]
|
|
}
|
|
|
|
// =============================================================================
|
|
// Aufzählungen
|
|
// =============================================================================
|
|
|
|
/// Rollen im Sinne der Feuerwehrorganisation. Die Berechtigungen dahinter
|
|
/// stehen in lib/authz.ts und werden beim Seed in die Datenbank gespiegelt.
|
|
enum RolleSchluessel {
|
|
SUPERADMIN
|
|
KOMMANDANT
|
|
GERAETEWART
|
|
ATEMSCHUTZGERAETEWART
|
|
KASSIER
|
|
SCHRIFTFUEHRER
|
|
AUSBILDER
|
|
MITGLIED
|
|
KIOSK
|
|
}
|
|
|
|
/// Art des Ausweises, mit dem sich jemand am Terminal im Gerätehaus anmeldet.
|
|
enum AusweisTokenTyp {
|
|
PIN
|
|
RFID
|
|
}
|
|
|
|
/// Wie sich jemand angemeldet hat — landet im Audit-Log.
|
|
enum AnmeldeArt {
|
|
LOKAL
|
|
OIDC
|
|
KIOSK_PIN
|
|
KIOSK_RFID
|
|
SYSTEM
|
|
}
|
|
|
|
/// Aktionen im Audit-Log.
|
|
enum AuditAktion {
|
|
ERSTELLT
|
|
GEAENDERT
|
|
GELOESCHT
|
|
WIEDERHERGESTELLT
|
|
ANGEMELDET
|
|
ABGEMELDET
|
|
ANMELDUNG_FEHLGESCHLAGEN
|
|
EXPORTIERT
|
|
DATEI_HOCHGELADEN
|
|
DATEI_ABGERUFEN
|
|
BERECHTIGUNG_GEAENDERT
|
|
}
|
|
|
|
/// Wer die Aktion ausgelöst hat.
|
|
enum AkteurTyp {
|
|
PERSON
|
|
SYSTEM
|
|
WORKER
|
|
KIOSK
|
|
API
|
|
}
|
|
|
|
/// Betriebsstatus eines Geräts nach DGUV-Logik.
|
|
enum GeraetStatus {
|
|
EINSATZBEREIT
|
|
PRUEFUNG_FAELLIG
|
|
GESPERRT
|
|
AUSGESONDERT
|
|
IN_REPARATUR
|
|
}
|
|
|
|
/// Grobe Fahrzeugkategorie. Die vollständige DIN-Typisierung kommt in Phase 1.
|
|
enum FahrzeugArt {
|
|
LOESCHFAHRZEUG
|
|
DREHLEITER
|
|
RUESTWAGEN
|
|
GERAETEWAGEN
|
|
MANNSCHAFTSTRANSPORT
|
|
EINSATZLEITWAGEN
|
|
WECHSELLADER
|
|
ANHAENGER
|
|
SONSTIGES
|
|
}
|
|
|
|
/// Dienstgrad nach bayerischer Feuerwehr-Dienstgradverordnung (Auswahl).
|
|
enum Dienstgrad {
|
|
FEUERWEHRANWAERTER
|
|
FEUERWEHRMANN
|
|
OBERFEUERWEHRMANN
|
|
HAUPTFEUERWEHRMANN
|
|
LOESCHMEISTER
|
|
OBERLOESCHMEISTER
|
|
HAUPTLOESCHMEISTER
|
|
BRANDMEISTER
|
|
OBERBRANDMEISTER
|
|
HAUPTBRANDMEISTER
|
|
KEIN
|
|
}
|
|
|
|
// =============================================================================
|
|
// Mandant
|
|
// =============================================================================
|
|
|
|
/// Eine Feuerwehr. Oberste Klammer — an ihr hängt alles andere.
|
|
model Organisation {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
/// Kurzform für URLs und Konfiguration, z. B. "ff-musterhausen".
|
|
slug String @unique @db.VarChar(64)
|
|
name String @db.VarChar(200)
|
|
kurzname String @db.VarChar(60)
|
|
|
|
strasse String? @db.VarChar(200)
|
|
plz String? @db.VarChar(10)
|
|
ort String? @db.VarChar(120)
|
|
landkreis String? @db.VarChar(120)
|
|
bundesland String @default("Bayern") @db.VarChar(60)
|
|
telefon String? @db.VarChar(40)
|
|
email String? @db.VarChar(200)
|
|
/// Kennung im Einsatzleitsystem der Integrierten Leitstelle.
|
|
ilsKennung String? @db.VarChar(40)
|
|
/// Funkrufname-Präfix der Wehr, z. B. "Florian Musterhausen".
|
|
funkPraefix String? @db.VarChar(80)
|
|
|
|
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[]
|
|
qualifikationstypen Qualifikationstyp[]
|
|
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")
|
|
}
|
|
|
|
// =============================================================================
|
|
// Personen und Rechte
|
|
// =============================================================================
|
|
|
|
/// Ein Mitglied der Wehr. Gleichzeitig der Anmeldebenutzer — es gibt bewusst
|
|
/// keine getrennte "User"-Tabelle, damit Dienstgrad, Qualifikation und Zugang
|
|
/// nicht auseinanderlaufen.
|
|
model Person {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
|
|
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)
|
|
|
|
email String? @db.VarChar(200)
|
|
telefon String? @db.VarChar(40)
|
|
mobil String? @db.VarChar(40)
|
|
|
|
strasse String? @db.VarChar(200)
|
|
plz String? @db.VarChar(10)
|
|
ort String? @db.VarChar(120)
|
|
|
|
eintrittsdatum DateTime? @db.Date
|
|
austrittsdatum DateTime? @db.Date
|
|
/// Aktiver Dienst, passiv, Jugendfeuerwehr, Ehrenmitglied.
|
|
abteilung String? @db.VarChar(60)
|
|
|
|
/// bcrypt-Hash für den lokalen Login. Null, wenn nur OIDC genutzt wird.
|
|
passwortHash String? @db.VarChar(120)
|
|
passwortGeaendertAm DateTime? @db.Timestamptz(3)
|
|
/// "sub"-Claim aus authentik. Verknüpft die Person mit dem OIDC-Konto.
|
|
oidcSubject String? @db.VarChar(255)
|
|
/// Gruppen aus dem letzten OIDC-Login — nur zur Nachvollziehbarkeit.
|
|
oidcGruppen String[] @default([])
|
|
|
|
letzteAnmeldung DateTime? @db.Timestamptz(3)
|
|
fehlversuche Int @default(0)
|
|
gesperrtBis DateTime? @db.Timestamptz(3)
|
|
aktiv Boolean @default(true)
|
|
|
|
/// Profilbild. Liegt wie alles andere in MinIO.
|
|
avatarDateiId String? @db.Uuid
|
|
|
|
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)
|
|
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])
|
|
@@unique([organisationId, oidcSubject])
|
|
@@unique([organisationId, mitgliedsnummer])
|
|
@@index([organisationId, nachname, vorname])
|
|
@@index([organisationId, aktiv])
|
|
@@index([oidcSubject])
|
|
@@map("person")
|
|
}
|
|
|
|
/// Eine Rolle innerhalb einer Wehr. Die Systemrollen legt der Seed an; eigene
|
|
/// Rollen kann der Kommandant ergänzen.
|
|
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)
|
|
/// Systemrollen dürfen nicht gelöscht werden.
|
|
systemRolle 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)
|
|
personen PersonRolle[]
|
|
rechte RolleBerechtigung[]
|
|
|
|
@@unique([organisationId, schluessel])
|
|
@@index([organisationId])
|
|
@@map("rolle")
|
|
}
|
|
|
|
/// Zuweisung Person zu Rolle. Eine Person kann mehrere Rollen tragen — der
|
|
/// Gerätewart ist meist auch aktives Mitglied.
|
|
model PersonRolle {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
personId String @db.Uuid
|
|
rolleId 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)
|
|
/// 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)
|
|
|
|
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
|
person Person @relation("PersonRollenInhaber", fields: [personId], references: [id], onDelete: Cascade)
|
|
rolle Rolle @relation(fields: [rolleId], references: [id], onDelete: Cascade)
|
|
zugewiesenVon Person? @relation("PersonRollenVergeber", fields: [zugewiesenVonId], references: [id], onDelete: SetNull)
|
|
|
|
@@unique([personId, rolleId])
|
|
@@index([organisationId, personId])
|
|
@@index([organisationId, rolleId])
|
|
@@map("person_rolle")
|
|
}
|
|
|
|
/// Systemweiter Katalog der Einzelrechte, z. B. "geraet.bearbeiten".
|
|
///
|
|
/// BEWUSSTE AUSNAHME von der organisationId-Regel: das hier sind keine
|
|
/// Mandantendaten, sondern eine Liste dessen, was die Software überhaupt kann.
|
|
/// Der Katalog wird vom Seed aus lib/authz.ts befüllt und ist für alle Wehren
|
|
/// identisch. Mandantenbezogen wird es erst durch RolleBerechtigung, und die
|
|
/// 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)
|
|
/// Fachbereich für die Gruppierung in der Oberfläche.
|
|
bereich String @db.VarChar(60)
|
|
bezeichnung String @db.VarChar(160)
|
|
beschreibung String? @db.VarChar(500)
|
|
|
|
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
rollen RolleBerechtigung[]
|
|
|
|
@@index([bereich])
|
|
@@map("berechtigung")
|
|
}
|
|
|
|
/// Welche Rolle welches Recht hat — je Wehr getrennt, damit eine Wehr dem
|
|
/// Gerätewart mehr erlauben kann als eine andere.
|
|
model RolleBerechtigung {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
rolleId String @db.Uuid
|
|
berechtigungId String @db.Uuid
|
|
|
|
/// false setzt ein Recht gezielt ab, das die Rolle sonst hätte.
|
|
gewaehrt Boolean @default(true)
|
|
|
|
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
|
rolle Rolle @relation(fields: [rolleId], references: [id], onDelete: Cascade)
|
|
berechtigung Berechtigung @relation(fields: [berechtigungId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([rolleId, berechtigungId])
|
|
@@index([organisationId, rolleId])
|
|
@@map("rolle_berechtigung")
|
|
}
|
|
|
|
/// Anmeldemittel für das Terminal im Gerätehaus: PIN oder RFID-Karte.
|
|
/// Gespeichert wird immer nur ein Hash, nie die PIN oder die UID im Klartext.
|
|
model AusweisToken {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
personId String @db.Uuid
|
|
|
|
typ AusweisTokenTyp
|
|
/// bcrypt-Hash der PIN bzw. der RFID-UID.
|
|
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)
|
|
|
|
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)
|
|
geloeschtAm DateTime? @db.Timestamptz(3)
|
|
|
|
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
|
person Person @relation(fields: [personId], references: [id], onDelete: Cascade)
|
|
|
|
/// Je Person und Art höchstens ein aktives Mittel dieser Bezeichnung.
|
|
@@index([organisationId, personId])
|
|
@@index([organisationId, typ, aktiv])
|
|
@@map("ausweis_token")
|
|
}
|
|
|
|
// =============================================================================
|
|
// Nachvollziehbarkeit
|
|
// =============================================================================
|
|
|
|
/// Append-only. Es gibt keine UPDATE- oder DELETE-Policy für die Anwendung;
|
|
/// zusätzlich verhindert ein Trigger Änderungen auch für den Eigentümer.
|
|
model AuditLog {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
|
|
zeitpunkt DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
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)
|
|
anmeldeArt AnmeldeArt?
|
|
|
|
aktion AuditAktion
|
|
/// Modellname, z. B. "Geraet".
|
|
entitaet String @db.VarChar(80)
|
|
entitaetId String? @db.Uuid
|
|
/// Menschenlesbare Kurzfassung für die Oberfläche.
|
|
beschreibung String? @db.VarChar(500)
|
|
|
|
vorher Json? @db.JsonB
|
|
nachher Json? @db.JsonB
|
|
/// Zusatzangaben: Anfrage-ID, Endpunkt, betroffene Felder.
|
|
kontext Json? @db.JsonB
|
|
|
|
ipAdresse String? @db.Inet
|
|
userAgent String? @db.VarChar(400)
|
|
|
|
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
|
akteur Person? @relation(fields: [akteurPersonId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([organisationId, zeitpunkt(sort: Desc)])
|
|
@@index([organisationId, entitaet, entitaetId])
|
|
@@index([organisationId, akteurPersonId])
|
|
@@map("audit_log")
|
|
}
|
|
|
|
// =============================================================================
|
|
// Dateien
|
|
// =============================================================================
|
|
|
|
/// Metadaten zu einem Objekt in MinIO. Der Inhalt liegt NIE im Dateisystem und
|
|
/// nie in der Datenbank — hier steht nur, wo er zu finden ist.
|
|
model Datei {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
|
|
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)
|
|
groesseBytes BigInt
|
|
/// SHA-256 des Inhalts. Erkennt Dubletten und belegt Unverändertheit.
|
|
pruefsumme String? @db.VarChar(64)
|
|
|
|
/// Woran die Datei hängt (z. B. "Geraet") und an welchem Datensatz.
|
|
entitaet String? @db.VarChar(80)
|
|
entitaetId String? @db.Uuid
|
|
/// 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)
|
|
/// 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")
|
|
|
|
// 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])
|
|
@@index([organisationId, kategorie])
|
|
@@map("datei")
|
|
}
|
|
|
|
// =============================================================================
|
|
// Phase 1 — Stammdaten
|
|
//
|
|
// Ordnungsprinzip:
|
|
// Standort wo etwas steht (Gerätehaus, Lager, Außenstelle)
|
|
// Fahrzeug ein Einsatzfahrzeug, es hat Geräteräume
|
|
// Geraeteraum ein Fach am Fahrzeug (G1 … G6, Dachkasten, Mannschaftsraum)
|
|
// Geraetetyp die Bauart samt Prüfvorgabe — "Pressluftatmer PA 94"
|
|
// Geraet das einzelne Stück mit Inventarnummer
|
|
//
|
|
// Ein Gerät liegt ENTWEDER an einem Standort ODER verlastet in einem
|
|
// Geräteraum eines Fahrzeugs. Beides gleichzeitig oder gar nichts wäre eine
|
|
// Falschangabe; eine Prüfbedingung in der Datenbank verhindert das.
|
|
// =============================================================================
|
|
|
|
/// Art eines Standorts.
|
|
enum StandortArt {
|
|
GERAETEHAUS
|
|
LAGER
|
|
AUSSENSTELLE
|
|
WERKSTATT
|
|
SONSTIGES
|
|
}
|
|
|
|
/// Fachliche Einordnung eines Gerätetyps — bestimmt die Gruppierung in Listen.
|
|
enum GeraeteKategorie {
|
|
ATEMSCHUTZ
|
|
BELEUCHTUNG
|
|
BRANDBEKAEMPFUNG
|
|
ELEKTRO
|
|
FUNK
|
|
HYGIENE
|
|
MEDIZIN
|
|
MESSTECHNIK
|
|
PERSOENLICHE_SCHUTZAUSRUESTUNG
|
|
RETTUNG
|
|
SCHLAUCHMATERIAL
|
|
TECHNISCHE_HILFELEISTUNG
|
|
UMWELTSCHUTZ
|
|
WASSERFOERDERUNG
|
|
SONSTIGES
|
|
}
|
|
|
|
/// Lebensdauerkategorie. Diese Gruppen haben eine maximale Nutzungsdauer ab
|
|
/// Herstellungs- oder Indienststellungsdatum, unabhängig vom Zustand.
|
|
enum LebensdauerKategorie {
|
|
KEINE
|
|
HELM
|
|
SCHUTZKLEIDUNG
|
|
HANDSCHUHE
|
|
STIEFEL
|
|
SEIL_GURT
|
|
PRESSLUFTATMER
|
|
ATEMANSCHLUSS
|
|
DRUCKBEHAELTER
|
|
SCHLAUCH
|
|
LEITER
|
|
SONSTIGE
|
|
}
|
|
|
|
/// Woran sich die maximale Nutzungsdauer bemisst.
|
|
enum LebensdauerBezug {
|
|
HERSTELLUNG
|
|
INDIENSTSTELLUNG
|
|
}
|
|
|
|
/// Art einer Qualifikation.
|
|
enum QualifikationsArt {
|
|
LEHRGANG
|
|
BEFAEHIGUNG
|
|
UNTERSUCHUNG
|
|
FUEHRERSCHEIN
|
|
UNTERWEISUNG
|
|
SONSTIGES
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/// Ein Ort, an dem Fahrzeuge stehen und Geräte lagern.
|
|
model Standort {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
|
|
name String @db.VarChar(160)
|
|
/// Kurzzeichen für Listen und Etiketten, z. B. "GH" oder "LAG".
|
|
kuerzel String? @db.VarChar(20)
|
|
art StandortArt @default(GERAETEHAUS)
|
|
|
|
strasse String? @db.VarChar(200)
|
|
plz String? @db.VarChar(10)
|
|
ort String? @db.VarChar(120)
|
|
|
|
/// Koordinaten als PostGIS-Punkt (WGS 84). Prisma kennt den Typ nicht,
|
|
/// deshalb Unsupported — gelesen und geschrieben wird per Rohabfrage.
|
|
/// Ab Phase 6 zeigt die Karte diese Punkte an.
|
|
koordinaten Unsupported("geography(Point, 4326)")?
|
|
|
|
bemerkung 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)
|
|
fahrzeuge Fahrzeug[]
|
|
geraete Geraet[]
|
|
|
|
@@unique([organisationId, name])
|
|
@@index([organisationId, aktiv])
|
|
@@map("standort")
|
|
}
|
|
|
|
/// Ein Einsatzfahrzeug.
|
|
model Fahrzeug {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
|
|
/// Funkrufname, z. B. "Florian Musterhausen 40/1".
|
|
funkrufname String @db.VarChar(120)
|
|
bezeichnung String @db.VarChar(120)
|
|
art FahrzeugArt @default(LOESCHFAHRZEUG)
|
|
/// Typkurzzeichen nach DIN, z. B. "LF 20", "TSF-W", "DLA(K) 23/12".
|
|
dinTyp String? @db.VarChar(40)
|
|
|
|
kennzeichen String? @db.VarChar(20)
|
|
hersteller String? @db.VarChar(120)
|
|
aufbauhersteller String? @db.VarChar(120)
|
|
fahrgestellnummer String? @db.VarChar(30)
|
|
baujahr Int?
|
|
erstzulassung DateTime? @db.Date
|
|
indienststellung DateTime? @db.Date
|
|
|
|
/// Sitzplätze für die Stärkeberechnung ab Phase 3.
|
|
sitzplaetze Int?
|
|
/// Löschwasser in Litern.
|
|
tankinhalt Int?
|
|
/// Zulässige Gesamtmasse in kg.
|
|
gesamtmasse Int?
|
|
kilometerstand Int?
|
|
|
|
/// Nächste Hauptuntersuchung.
|
|
tuevBis DateTime? @db.Date
|
|
/// Nächste Sicherheitsprüfung nach UVV.
|
|
uvvBis DateTime? @db.Date
|
|
naechsteWartung DateTime? @db.Date
|
|
|
|
standortId String? @db.Uuid
|
|
aktiv Boolean @default(true)
|
|
bemerkung String? @db.VarChar(1000)
|
|
|
|
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)
|
|
standort Standort? @relation(fields: [standortId], references: [id], onDelete: SetNull)
|
|
geraeteraeume Geraeteraum[]
|
|
geraete Geraet[]
|
|
|
|
@@unique([organisationId, funkrufname])
|
|
@@unique([organisationId, kennzeichen])
|
|
@@index([organisationId, aktiv])
|
|
@@index([organisationId, tuevBis])
|
|
@@map("fahrzeug")
|
|
}
|
|
|
|
/// Ein Fach am Fahrzeug. Die Nummerierung folgt der Beladeordnung: G1 bis G6
|
|
/// an den Seiten, dazu Dachkasten, Heck und Mannschaftsraum.
|
|
model Geraeteraum {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
fahrzeugId String @db.Uuid
|
|
|
|
/// Kurzzeichen wie "G1", "GR", "Dach".
|
|
bezeichnung String @db.VarChar(40)
|
|
beschreibung String? @db.VarChar(200)
|
|
/// Reihenfolge in der Anzeige — entspricht der Beladeordnung.
|
|
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)
|
|
fahrzeug Fahrzeug @relation(fields: [fahrzeugId], references: [id], onDelete: Cascade)
|
|
geraete Geraet[]
|
|
|
|
@@unique([fahrzeugId, bezeichnung])
|
|
@@index([organisationId, fahrzeugId])
|
|
@@map("geraeteraum")
|
|
}
|
|
|
|
/// Die Bauart eines Geräts samt Prüf- und Lebensdauervorgabe. Was hier steht,
|
|
/// gilt als Vorgabe für jedes Gerät dieses Typs; einzelne Geräte dürfen davon
|
|
/// abweichen, wenn der Hersteller etwas anderes vorschreibt.
|
|
model Geraetetyp {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
|
|
name String @db.VarChar(160)
|
|
kategorie GeraeteKategorie @default(SONSTIGES)
|
|
hersteller String? @db.VarChar(120)
|
|
modell String? @db.VarChar(120)
|
|
|
|
/// Vorgabe für das Prüfintervall in Monaten.
|
|
pruefintervallMonate Int?
|
|
/// Fundstelle der Prüfvorschrift, z. B. "DGUV Vorschrift 49" oder
|
|
/// "DIN 14800-16".
|
|
pruefvorschrift String? @db.VarChar(200)
|
|
|
|
lebensdauerKategorie LebensdauerKategorie @default(KEINE)
|
|
/// Maximale Nutzungsdauer in Jahren.
|
|
lebensdauerJahre Int?
|
|
lebensdauerBezug LebensdauerBezug @default(INDIENSTSTELLUNG)
|
|
/// Wie viele Monate vor Ablauf gewarnt wird.
|
|
vorwarnzeitMonate Int @default(3)
|
|
|
|
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)
|
|
geraete Geraet[]
|
|
|
|
// Phase 2
|
|
wartungsvorlagen Wartungsvorlage[]
|
|
rueckrufTypen RueckrufGeraetetyp[]
|
|
|
|
@@unique([organisationId, name])
|
|
@@index([organisationId, kategorie])
|
|
@@index([organisationId, aktiv])
|
|
@@map("geraetetyp")
|
|
}
|
|
|
|
/// Ein einzelnes Gerät mit eigener Inventarnummer.
|
|
model Geraet {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
|
|
inventarnummer String @db.VarChar(60)
|
|
bezeichnung String @db.VarChar(160)
|
|
geraetetypId String? @db.Uuid
|
|
|
|
hersteller String? @db.VarChar(120)
|
|
modell String? @db.VarChar(120)
|
|
seriennummer String? @db.VarChar(120)
|
|
|
|
herstellungsdatum DateTime? @db.Date
|
|
indienststellung DateTime? @db.Date
|
|
anschaffungspreis Decimal? @db.Decimal(12, 2)
|
|
|
|
status GeraetStatus @default(EINSATZBEREIT)
|
|
|
|
/// Prüfintervall in Monaten. Leer bedeutet: Vorgabe des Gerätetyps gilt.
|
|
pruefintervallMonate Int?
|
|
letztePruefung DateTime? @db.Date
|
|
naechstePruefung DateTime? @db.Date
|
|
|
|
/// Lebensdauer. Leer bedeutet ebenfalls: Vorgabe des Gerätetyps gilt.
|
|
lebensdauerKategorie LebensdauerKategorie?
|
|
lebensdauerJahre Int?
|
|
/// Errechnetes Ende der Nutzungsdauer, vom Server gesetzt.
|
|
lebensdauerBis DateTime? @db.Date
|
|
|
|
/// ENTWEDER standortId ODER fahrzeugId + geraeteraumId.
|
|
/// Eine Prüfbedingung in der Datenbank setzt das durch.
|
|
standortId String? @db.Uuid
|
|
fahrzeugId String? @db.Uuid
|
|
geraeteraumId String? @db.Uuid
|
|
/// Freitext für die genaue Stelle im Raum: "Regal 3, Fach B".
|
|
lagerort String? @db.VarChar(160)
|
|
|
|
ausgesondertAm DateTime? @db.Date
|
|
ausgesondertGrund String? @db.VarChar(500)
|
|
bemerkung String? @db.VarChar(1000)
|
|
|
|
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)
|
|
standort Standort? @relation(fields: [standortId], references: [id], onDelete: SetNull)
|
|
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])
|
|
@@index([organisationId, lebensdauerBis])
|
|
@@index([organisationId, fahrzeugId])
|
|
@@index([organisationId, standortId])
|
|
@@index([organisationId, geraetetypId])
|
|
@@index([organisationId, seriennummer])
|
|
@@map("geraet")
|
|
}
|
|
|
|
/// Die Art einer Qualifikation — Lehrgang, Untersuchung, Führerschein.
|
|
model Qualifikationstyp {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
|
|
name String @db.VarChar(160)
|
|
/// Kurzzeichen für Listen, z. B. "MA", "AGT", "G26.3", "C1".
|
|
kuerzel String? @db.VarChar(20)
|
|
art QualifikationsArt @default(LEHRGANG)
|
|
|
|
/// Gültigkeit in Monaten. Leer bedeutet: gilt unbefristet.
|
|
gueltigkeitMonate Int?
|
|
/// Wie viele Tage vor Ablauf gewarnt wird.
|
|
vorwarnzeitTage Int @default(90)
|
|
/// Voraussetzung für den Einsatzdienst — wird im Dashboard hervorgehoben.
|
|
einsatzrelevant Boolean @default(false)
|
|
|
|
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)
|
|
qualifikationen Qualifikation[]
|
|
|
|
@@unique([organisationId, name])
|
|
@@index([organisationId, aktiv])
|
|
@@map("qualifikationstyp")
|
|
}
|
|
|
|
/// Eine erworbene Qualifikation einer Person.
|
|
model Qualifikation {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
personId String @db.Uuid
|
|
typId String @db.Uuid
|
|
|
|
erworbenAm DateTime @db.Date
|
|
/// Ablaufdatum. Wird beim Speichern aus gueltigkeitMonate errechnet, kann
|
|
/// aber überschrieben werden — auf einer Urkunde steht manchmal etwas
|
|
/// anderes als die Regel hergibt.
|
|
gueltigBis DateTime? @db.Date
|
|
|
|
ausstellendeStelle String? @db.VarChar(200)
|
|
/// Lehrgangs- oder Urkundennummer.
|
|
nachweisnummer String? @db.VarChar(80)
|
|
/// Eingescannte Urkunde, liegt in MinIO.
|
|
urkundeDateiId String? @db.Uuid
|
|
|
|
bestaetigtVonId String? @db.Uuid
|
|
bestaetigtAm DateTime? @db.Timestamptz(3)
|
|
bemerkung String? @db.VarChar(1000)
|
|
|
|
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)
|
|
person Person @relation("QualifikationInhaber", fields: [personId], references: [id], onDelete: Cascade)
|
|
typ Qualifikationstyp @relation(fields: [typId], references: [id], onDelete: Restrict)
|
|
bestaetigtVon Person? @relation("QualifikationBestaetiger", fields: [bestaetigtVonId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([organisationId, personId])
|
|
@@index([organisationId, typId])
|
|
@@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")
|
|
}
|