Phase 4: eine eigene Oberflaeche unter /kiosk fuer das Wandtablet in der Fahrzeughalle — mit Handschuhen bedienbar und arbeitsfaehig, wenn das Netz weg ist. Datenmodell: Fahrt, Anwesenheit, Waescheabgabe, Inventurgang, Inventurposten, KioskVorgang und SyncKonflikt. Die Kennung eines Vorgangs erzeugt der Browser beim Einreihen und ist damit der Idempotenzschluessel; kommt derselbe Vorgang nach abgebrochener Uebertragung erneut an, erkennt ihn der Server wieder. Die Datenbank setzt vier Regeln selbst durch und verweigert UPDATE und DELETE auf kiosk_vorgang. Offline: von Hand geschriebener Service Worker, Warteschlange in der IndexedDB (Dexie), Fotos als Blob daneben, sichtbarer Zustand als Leiste ueber allem. Was sich beim Nachtragen nicht anwenden laesst, landet als Konflikt unter /terminal statt verworfen zu werden. Dazu: Anmeldung per Ausweis, PIN oder signierter QR-Marke vom Handy, Hallenmonitor mit optionalen DWD-Warnungen, Installation als App. Behoben: die Warteschlange zog ihren Wiederholungstakt bei jedem Rendern neu auf und haette einen liegen gebliebenen Vorgang nie ein zweites Mal geschickt. Gefunden vom Offline-E2E-Test. 31 neue Tests (20 Unit, 11 gegen eine echte Datenbank, 9 E2E je Browser). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2216 lines
81 KiB
Plaintext
2216 lines
81 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[]
|
|
|
|
// Phase 3 — Einsätze und Übungen
|
|
einsaetze Einsatz[]
|
|
einsatzfahrzeuge Einsatzfahrzeug[]
|
|
einsatzkraefte Einsatzkraft[]
|
|
materialverbrauch Materialverbrauch[]
|
|
lagemeldungen Lagemeldung[]
|
|
portalzugaenge PortalZugang[]
|
|
uebungen Uebung[]
|
|
uebungsteilnahmen Uebungsteilnahme[]
|
|
|
|
// Phase 4 — Terminal im Gerätehaus
|
|
fahrten Fahrt[]
|
|
anwesenheiten Anwesenheit[]
|
|
waescheabgaben Waescheabgabe[]
|
|
inventurgaenge Inventurgang[]
|
|
inventurposten Inventurposten[]
|
|
kioskvorgaenge KioskVorgang[]
|
|
syncKonflikte SyncKonflikt[]
|
|
|
|
@@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")
|
|
|
|
// Phase 3 — Einsätze und Übungen
|
|
geleiteteEinsaetze Einsatz[] @relation("EinsatzLeitung")
|
|
freigegebeneBerichte Einsatz[] @relation("EinsatzFreigabe")
|
|
einsatzteilnahmen Einsatzkraft[] @relation("EinsatzTeilnahme")
|
|
lagemeldungen Lagemeldung[] @relation("LagemeldungVerfasser")
|
|
portalzugaenge PortalZugang[] @relation("PortalzugangErsteller")
|
|
geleiteteUebungen Uebung[] @relation("UebungLeitung")
|
|
uebungsteilnahmen Uebungsteilnahme[] @relation("UebungTeilnahme")
|
|
|
|
// Phase 4 — Terminal im Gerätehaus
|
|
fahrten Fahrt[] @relation("FahrtFahrer")
|
|
anwesenheiten Anwesenheit[] @relation("AnwesenheitPerson")
|
|
waescheabgaben Waescheabgabe[] @relation("WaescheTraeger")
|
|
angenommeneWaesche Waescheabgabe[] @relation("WaescheAnnahme")
|
|
geleiteteInventuren Inventurgang[] @relation("InventurLeitung")
|
|
inventurposten Inventurposten[] @relation("InventurErfasser")
|
|
kioskvorgaenge KioskVorgang[] @relation("KioskVorgangPerson")
|
|
verursachteKonflikte SyncKonflikt[] @relation("KonfliktVerursacher")
|
|
erledigteKonflikte SyncKonflikt[] @relation("KonfliktErledigung")
|
|
|
|
/// 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")
|
|
|
|
// Phase 3
|
|
alsEinsatzbericht Einsatz[] @relation("EinsatzBericht")
|
|
alsLagemeldungsfoto Lagemeldung[] @relation("LagemeldungFoto")
|
|
alsUebungsbericht Uebung[] @relation("UebungBericht")
|
|
|
|
@@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[]
|
|
inventurgaenge Inventurgang[]
|
|
|
|
@@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[]
|
|
|
|
// Phase 3
|
|
einsaetze Einsatzfahrzeug[]
|
|
einsatzkraefte Einsatzkraft[]
|
|
|
|
// Phase 4
|
|
fahrten Fahrt[]
|
|
inventurgaenge Inventurgang[]
|
|
|
|
@@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[]
|
|
materialverbrauch Materialverbrauch[]
|
|
|
|
// Phase 4
|
|
waescheabgaben Waescheabgabe[]
|
|
inventurposten Inventurposten[]
|
|
|
|
@@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")
|
|
}
|
|
|
|
// =============================================================================
|
|
// Phase 3 — Einsätze und Übungen
|
|
//
|
|
// Ordnungsprinzip:
|
|
// Einsatz der Vorgang selbst, mit Ort, Zeitstempeln und Bericht
|
|
// Einsatzfahrzeug welches Fahrzeug wann ausgerückt und eingerückt ist
|
|
// Einsatzkraft wer mitgefahren ist, in welcher Funktion, wie lange
|
|
// Materialverbrauch was dabei verbraucht oder beschädigt wurde
|
|
// Lagemeldung was währenddessen gemeldet wurde, mit Foto
|
|
// PortalZugang ein zeitlich begrenzter Link für eine Fremdwehr
|
|
// Uebung dasselbe für Übungen, mit Thema und Lernzielen
|
|
//
|
|
// Drei Festlegungen, die den Aufbau erklären:
|
|
//
|
|
// 1. Die Zeitstempel sind der Kern. Alarmierung, Ausrücken, Eintreffen,
|
|
// Einsatzende, Einrücken, Einsatzbereitschaft — daraus rechnet sich die
|
|
// Ausrückzeit, die Anfahrtszeit und die Einsatzdauer. Sie stehen deshalb
|
|
// einzeln und nicht als Sammelfeld.
|
|
//
|
|
// 2. Koordinaten liegen doppelt: als Dezimalgrad, die Prisma lesen kann, und
|
|
// als PostGIS-Punkt für die Karte ab Phase 6. Ein Trigger hält beides
|
|
// zusammen — sonst müsste jede Schreiboperation Roh-SQL sein.
|
|
//
|
|
// 3. Ein freigegebener Einsatzbericht ist unveränderbar, dieselbe Logik wie
|
|
// beim Prüfprotokoll: er geht an die Gemeinde und an die Versicherung.
|
|
// =============================================================================
|
|
|
|
/// Einsatzkategorie nach der bayerischen Feuerwehrstatistik. Danach meldet die
|
|
/// Wehr ihre Zahlen an die Gemeinde und den Landkreis.
|
|
enum EinsatzKategorie {
|
|
BRAND_KLEIN
|
|
BRAND_MITTEL
|
|
BRAND_GROSS
|
|
TECHNISCHE_HILFE_KLEIN
|
|
TECHNISCHE_HILFE_MITTEL
|
|
TECHNISCHE_HILFE_GROSS
|
|
GEFAHRGUT
|
|
/// Unterstützung des Rettungsdienstes, z. B. Türöffnung, Tragehilfe.
|
|
RETTUNGSDIENST
|
|
/// Auslösung einer Brandmeldeanlage ohne Schadenfeuer.
|
|
BRANDMELDEANLAGE
|
|
BOESWILLIGER_ALARM
|
|
SICHERHEITSWACHE
|
|
/// Amtshilfe, Nachbarschaftshilfe, Sonstiges.
|
|
UNTERSTUETZUNG
|
|
SONSTIGES
|
|
}
|
|
|
|
/// Lebenszyklus eines Einsatzes.
|
|
enum EinsatzStatus {
|
|
/// Schnellerfassung: Stichwort und Fahrzeuge stehen, der Rest fehlt noch.
|
|
ENTWURF
|
|
/// Läuft gerade — der Alarmmonitor und das Portal zeigen ihn an.
|
|
LAUFEND
|
|
/// Nachbereitet und abgeschlossen.
|
|
ABGESCHLOSSEN
|
|
/// Bericht freigegeben und festgeschrieben.
|
|
FREIGEGEBEN
|
|
}
|
|
|
|
/// Wie alarmiert wurde. Für die Auswertung, ob die Alarmierungswege greifen.
|
|
enum Alarmierungsart {
|
|
SIRENE
|
|
MELDEEMPFAENGER
|
|
TELEFON
|
|
APP
|
|
PERSOENLICH
|
|
SONSTIGES
|
|
}
|
|
|
|
/// Funktion einer Einsatzkraft. Aus ihr ergibt sich die Stärke nach FwDV 3.
|
|
enum EinsatzFunktion {
|
|
EINSATZLEITER
|
|
ZUGFUEHRER
|
|
GRUPPENFUEHRER
|
|
STAFFELFUEHRER
|
|
TRUPPFUEHRER
|
|
MASCHINIST
|
|
MELDER
|
|
TRUPPMANN
|
|
ATEMSCHUTZTRAEGER
|
|
SICHERHEITSTRUPP
|
|
SANITAETER
|
|
SONSTIGES
|
|
}
|
|
|
|
/// Art einer Übung.
|
|
enum UebungsArt {
|
|
PRAXIS
|
|
THEORIE
|
|
ATEMSCHUTZ
|
|
MASCHINIST
|
|
FUNK
|
|
ERSTE_HILFE
|
|
/// Gemeinsam mit Nachbarwehren, THW oder Rettungsdienst.
|
|
GEMEINSCHAFTSUEBUNG
|
|
SONSTIGES
|
|
}
|
|
|
|
/// Woher eine Lagemeldung kommt.
|
|
enum LagemeldungQuelle {
|
|
/// Aus der Anwendung, von einer angemeldeten Person.
|
|
INTERN
|
|
/// Über den Portal-Link, von einer Fremdwehr ohne Konto.
|
|
PORTAL
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/// Ein Einsatz.
|
|
model Einsatz {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
|
|
/// Fortlaufend je Wehr und Jahr, Format "JJJJ-NNNN".
|
|
einsatznummer String @db.VarChar(40)
|
|
/// Einsatzstichwort der Leitstelle, z. B. "B 3 Wohnhausbrand".
|
|
stichwort String @db.VarChar(160)
|
|
kategorie EinsatzKategorie @default(SONSTIGES)
|
|
status EinsatzStatus @default(ENTWURF)
|
|
|
|
/// Einsatznummer der Integrierten Leitstelle — die Klammer zu deren Akte.
|
|
ilsNummer String? @db.VarChar(60)
|
|
alarmierungsart Alarmierungsart?
|
|
|
|
// Ort
|
|
strasse String? @db.VarChar(200)
|
|
hausnummer String? @db.VarChar(20)
|
|
plz String? @db.VarChar(10)
|
|
ort String? @db.VarChar(120)
|
|
ortsteil String? @db.VarChar(120)
|
|
/// Objektbezeichnung, z. B. "Grundschule", "Autobahn A9, km 312".
|
|
objekt String? @db.VarChar(200)
|
|
|
|
/// Koordinaten als Dezimalgrad — von Prisma lesbar.
|
|
breitengrad Decimal? @db.Decimal(9, 6)
|
|
laengengrad Decimal? @db.Decimal(9, 6)
|
|
/// Dieselben Koordinaten als PostGIS-Punkt, von einem Trigger gepflegt.
|
|
/// Ab Phase 6 zeichnet die Karte daraus.
|
|
koordinaten Unsupported("geography(Point, 4326)")?
|
|
/// Woher sie stammen: "nominatim", "manuell", "leitstelle".
|
|
koordinatenQuelle String? @db.VarChar(40)
|
|
|
|
// Zeitstempel — der Kern jeder Einsatzauswertung
|
|
alarmiertUm DateTime? @db.Timestamptz(3)
|
|
ausgeruecktUm DateTime? @db.Timestamptz(3)
|
|
eingetroffenUm DateTime? @db.Timestamptz(3)
|
|
einsatzendeUm DateTime? @db.Timestamptz(3)
|
|
eingeruecktUm DateTime? @db.Timestamptz(3)
|
|
einsatzbereitUm DateTime? @db.Timestamptz(3)
|
|
|
|
einsatzleiterId String? @db.Uuid
|
|
|
|
beschreibung String? @db.VarChar(4000)
|
|
bericht String?
|
|
|
|
wetter String? @db.VarChar(200)
|
|
temperatur Int?
|
|
/// Wer sonst vor Ort war: Polizei, Rettungsdienst, THW, Nachbarwehren.
|
|
weitereKraefte String? @db.VarChar(500)
|
|
|
|
/// Zahlen für die Statistik der Gemeinde.
|
|
geretteteePersonen Int @default(0)
|
|
verletztePersonen Int @default(0)
|
|
totePersonen Int @default(0)
|
|
sachschaden Decimal? @db.Decimal(12, 2)
|
|
/// Kostenersatzpflichtiger Einsatz (Art. 28 BayFwG).
|
|
kostenpflichtig Boolean @default(false)
|
|
|
|
/// Der erzeugte Einsatzbericht als PDF.
|
|
berichtDateiId String? @db.Uuid
|
|
freigegebenAm DateTime? @db.Timestamptz(3)
|
|
freigegebenVonId 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)
|
|
einsatzleiter Person? @relation("EinsatzLeitung", fields: [einsatzleiterId], references: [id], onDelete: SetNull)
|
|
freigegebenVon Person? @relation("EinsatzFreigabe", fields: [freigegebenVonId], references: [id], onDelete: SetNull)
|
|
berichtDatei Datei? @relation("EinsatzBericht", fields: [berichtDateiId], references: [id], onDelete: SetNull)
|
|
fahrzeuge Einsatzfahrzeug[]
|
|
kraefte Einsatzkraft[]
|
|
material Materialverbrauch[]
|
|
lagemeldungen Lagemeldung[]
|
|
portalzugaenge PortalZugang[]
|
|
fahrts Fahrt[]
|
|
anwesenheits Anwesenheit[]
|
|
|
|
@@unique([organisationId, einsatznummer])
|
|
@@index([organisationId, alarmiertUm(sort: Desc)])
|
|
@@index([organisationId, status])
|
|
@@index([organisationId, kategorie])
|
|
@@map("einsatz")
|
|
}
|
|
|
|
/// Ein Fahrzeug in einem Einsatz, mit eigenen Zeiten: nicht jedes Fahrzeug
|
|
/// rückt gleichzeitig aus, und die Nachalarmierung soll sichtbar bleiben.
|
|
model Einsatzfahrzeug {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
einsatzId String @db.Uuid
|
|
fahrzeugId String @db.Uuid
|
|
|
|
ausgeruecktUm DateTime? @db.Timestamptz(3)
|
|
eingetroffenUm DateTime? @db.Timestamptz(3)
|
|
eingeruecktUm DateTime? @db.Timestamptz(3)
|
|
|
|
kilometerAbfahrt Int?
|
|
kilometerRueckkehr Int?
|
|
/// Verbrauchtes Löschwasser in Litern.
|
|
wasserabgabe Int?
|
|
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)
|
|
einsatz Einsatz @relation(fields: [einsatzId], references: [id], onDelete: Cascade)
|
|
fahrzeug Fahrzeug @relation(fields: [fahrzeugId], references: [id], onDelete: Restrict)
|
|
|
|
@@unique([einsatzId, fahrzeugId])
|
|
@@index([organisationId, einsatzId])
|
|
@@index([organisationId, fahrzeugId])
|
|
@@map("einsatzfahrzeug")
|
|
}
|
|
|
|
/// Wer mitgefahren ist. Grundlage für die Stärkemeldung nach FwDV 3 und für
|
|
/// den Beteiligungsnachweis je Person.
|
|
model Einsatzkraft {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
einsatzId String @db.Uuid
|
|
personId String @db.Uuid
|
|
|
|
funktion EinsatzFunktion @default(TRUPPMANN)
|
|
/// Auf welchem Fahrzeug — leer, wenn jemand selbst angefahren ist.
|
|
fahrzeugId String? @db.Uuid
|
|
|
|
vonUm DateTime? @db.Timestamptz(3)
|
|
bisUm DateTime? @db.Timestamptz(3)
|
|
|
|
/// War unter Atemschutz im Einsatz. Zählt auf die Nachweise nach FwDV 7.
|
|
atemschutz Boolean @default(false)
|
|
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)
|
|
einsatz Einsatz @relation(fields: [einsatzId], references: [id], onDelete: Cascade)
|
|
person Person @relation("EinsatzTeilnahme", fields: [personId], references: [id], onDelete: Cascade)
|
|
fahrzeug Fahrzeug? @relation(fields: [fahrzeugId], references: [id], onDelete: SetNull)
|
|
|
|
@@unique([einsatzId, personId])
|
|
@@index([organisationId, einsatzId])
|
|
@@index([organisationId, personId])
|
|
@@map("einsatzkraft")
|
|
}
|
|
|
|
/// Was verbraucht oder beschädigt wurde — Schaummittel, Bindemittel, ein
|
|
/// zerschnittener Schlauch. Grundlage für Nachbeschaffung und Kostenersatz.
|
|
model Materialverbrauch {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
einsatzId String @db.Uuid
|
|
/// Verweis auf ein erfasstes Gerät, wo es eines gibt.
|
|
geraetId String? @db.Uuid
|
|
|
|
bezeichnung String @db.VarChar(200)
|
|
menge Decimal @db.Decimal(10, 2)
|
|
einheit String @db.VarChar(20)
|
|
kosten Decimal? @db.Decimal(10, 2)
|
|
/// Nicht verbraucht, sondern kaputtgegangen — dann folgt ein Mangel.
|
|
beschaedigt Boolean @default(false)
|
|
bemerkung String? @db.VarChar(500)
|
|
|
|
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
|
einsatz Einsatz @relation(fields: [einsatzId], references: [id], onDelete: Cascade)
|
|
geraet Geraet? @relation(fields: [geraetId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([organisationId, einsatzId])
|
|
@@map("materialverbrauch")
|
|
}
|
|
|
|
/// Eine Meldung während des Einsatzes. Aus der Anwendung oder über den
|
|
/// Portal-Link einer Fremdwehr.
|
|
model Lagemeldung {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
einsatzId String @db.Uuid
|
|
|
|
zeitpunkt DateTime @default(now()) @db.Timestamptz(3)
|
|
text String @db.VarChar(2000)
|
|
quelle LagemeldungQuelle @default(INTERN)
|
|
|
|
verfasserId String? @db.Uuid
|
|
/// Klarname oder Wehrname — bei Portalmeldungen die einzige Angabe.
|
|
verfasserName String? @db.VarChar(200)
|
|
|
|
fotoDateiId String? @db.Uuid
|
|
/// Hebt die Meldung im Verlauf hervor: Menschenrettung, Nachforderung,
|
|
/// Lageänderung.
|
|
wichtig Boolean @default(false)
|
|
|
|
/// Über welchen Portalzugang die Meldung kam.
|
|
portalZugangId String? @db.Uuid
|
|
|
|
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
|
geloeschtAm DateTime? @db.Timestamptz(3)
|
|
|
|
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
|
einsatz Einsatz @relation(fields: [einsatzId], references: [id], onDelete: Cascade)
|
|
verfasser Person? @relation("LagemeldungVerfasser", fields: [verfasserId], references: [id], onDelete: SetNull)
|
|
foto Datei? @relation("LagemeldungFoto", fields: [fotoDateiId], references: [id], onDelete: SetNull)
|
|
portalZugang PortalZugang? @relation(fields: [portalZugangId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([organisationId, einsatzId, zeitpunkt])
|
|
@@map("lagemeldung")
|
|
}
|
|
|
|
/// Ein zeitlich begrenzter Zugang zum Einsatzportal für eine Fremdwehr.
|
|
///
|
|
/// Der Link enthält die Kennung und ein Geheimnis; gespeichert wird nur dessen
|
|
/// Hash. Er gilt bis gueltigBis, lässt sich jederzeit widerrufen und zählt
|
|
/// jeden Aufruf mit — ein Link, der nach dem Einsatz noch benutzt wird, fällt
|
|
/// so auf.
|
|
model PortalZugang {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
einsatzId String @db.Uuid
|
|
|
|
/// Für wen der Link ist, z. B. "FF Nachbarhausen".
|
|
bezeichnung String @db.VarChar(160)
|
|
/// bcrypt-Hash des Geheimnisses aus dem Link.
|
|
tokenHash String @db.VarChar(120)
|
|
|
|
gueltigBis DateTime @db.Timestamptz(3)
|
|
widerrufenAm DateTime? @db.Timestamptz(3)
|
|
|
|
aufrufe Int @default(0)
|
|
letzterAufruf DateTime? @db.Timestamptz(3)
|
|
/// Zuletzt sehende IP — für den Fall, dass ein Link weitergegeben wurde.
|
|
letzteIp String? @db.Inet
|
|
|
|
erstelltVonId String? @db.Uuid
|
|
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
|
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
|
|
|
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
|
einsatz Einsatz @relation(fields: [einsatzId], references: [id], onDelete: Cascade)
|
|
erstelltVon Person? @relation("PortalzugangErsteller", fields: [erstelltVonId], references: [id], onDelete: SetNull)
|
|
lagemeldungen Lagemeldung[]
|
|
|
|
@@index([organisationId, einsatzId])
|
|
@@index([gueltigBis])
|
|
@@map("portal_zugang")
|
|
}
|
|
|
|
/// Eine Übung. Aufgebaut wie ein Einsatz, aber mit Ausbildungsthema und
|
|
/// Lernzielen statt Schadenlage — und ohne Portal.
|
|
model Uebung {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
|
|
/// Fortlaufend je Wehr und Jahr, Format "JJJJ-NNNN".
|
|
nummer String @db.VarChar(40)
|
|
thema String @db.VarChar(200)
|
|
art UebungsArt @default(PRAXIS)
|
|
|
|
datum DateTime @db.Date
|
|
beginnUm DateTime? @db.Timestamptz(3)
|
|
endeUm DateTime? @db.Timestamptz(3)
|
|
|
|
ort String? @db.VarChar(200)
|
|
leiterId String? @db.Uuid
|
|
|
|
/// Was gelernt werden soll — die Lernziele der Ausbildung.
|
|
lernziele String? @db.VarChar(2000)
|
|
beschreibung String? @db.VarChar(4000)
|
|
bericht String?
|
|
|
|
berichtDateiId 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)
|
|
leiter Person? @relation("UebungLeitung", fields: [leiterId], references: [id], onDelete: SetNull)
|
|
berichtDatei Datei? @relation("UebungBericht", fields: [berichtDateiId], references: [id], onDelete: SetNull)
|
|
teilnahmen Uebungsteilnahme[]
|
|
fahrten Fahrt[]
|
|
anwesenheiten Anwesenheit[]
|
|
|
|
@@unique([organisationId, nummer])
|
|
@@index([organisationId, datum(sort: Desc)])
|
|
@@index([organisationId, art])
|
|
@@map("uebung")
|
|
}
|
|
|
|
/// Wer an einer Übung teilgenommen hat. Der Nachweis, den eine Wehr für die
|
|
/// Beförderung und für die Statistik braucht.
|
|
model Uebungsteilnahme {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
uebungId String @db.Uuid
|
|
personId String @db.Uuid
|
|
|
|
funktion EinsatzFunktion?
|
|
vonUm DateTime? @db.Timestamptz(3)
|
|
bisUm DateTime? @db.Timestamptz(3)
|
|
bemerkung String? @db.VarChar(500)
|
|
|
|
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
|
uebung Uebung @relation(fields: [uebungId], references: [id], onDelete: Cascade)
|
|
person Person @relation("UebungTeilnahme", fields: [personId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([uebungId, personId])
|
|
@@index([organisationId, personId])
|
|
@@map("uebungsteilnahme")
|
|
}
|
|
|
|
// =============================================================================
|
|
// Phase 4 — Terminal im Gerätehaus
|
|
//
|
|
// Was am Wandtablet in der Fahrzeughalle erfasst wird:
|
|
//
|
|
// Fahrt das Fahrtenbuch — wer ist wann womit wohin gefahren
|
|
// Anwesenheit ein- und auschecken zu Dienst, Übung oder Einsatz
|
|
// Waescheabgabe Schutzkleidung zur Reinigung geben und zurückbekommen
|
|
// Inventurgang Bestand am Regal scannen, Fehlendes finden
|
|
// KioskVorgang Idempotenz: was das Terminal offline erfasst hat
|
|
// SyncKonflikt was sich beim Nachtragen nicht auflösen ließ
|
|
//
|
|
// Die beiden letzten sind der Kern des Offline-Betriebs. Ein Tablet in einer
|
|
// Halle aus Stahlbeton hat regelmäßig kein Netz; erfasst wird trotzdem, und
|
|
// nachgetragen wird, sobald es wieder geht. Damit ein zweimal abgeschickter
|
|
// Vorgang nicht zweimal ankommt, trägt jeder eine im Browser erzeugte Kennung.
|
|
// =============================================================================
|
|
|
|
/// Wozu eine Fahrt unternommen wurde.
|
|
enum FahrtZweck {
|
|
EINSATZ
|
|
UEBUNG
|
|
AUSBILDUNG
|
|
WERKSTATT
|
|
BESCHAFFUNG
|
|
/// Probefahrt, Überführung, Fahrsicherheitstraining.
|
|
SONSTIGES
|
|
}
|
|
|
|
/// Weg der Schutzkleidung durch die Reinigung.
|
|
enum WaescheStatus {
|
|
ABGEGEBEN
|
|
IN_REINIGUNG
|
|
ZURUECK
|
|
/// Nicht wieder aufgetaucht — muss ersetzt werden.
|
|
VERSCHOLLEN
|
|
}
|
|
|
|
/// Wozu jemand am Terminal eincheckt.
|
|
enum AnwesenheitAnlass {
|
|
DIENST
|
|
UEBUNG
|
|
EINSATZ
|
|
ARBEITSDIENST
|
|
SONSTIGES
|
|
}
|
|
|
|
/// Stand eines Inventurgangs.
|
|
enum InventurStatus {
|
|
LAEUFT
|
|
ABGESCHLOSSEN
|
|
ABGEBROCHEN
|
|
}
|
|
|
|
/// Was ein offline erfasster Vorgang bewirken soll.
|
|
enum KioskVorgangArt {
|
|
MANGEL_MELDEN
|
|
FAHRT_BEGINNEN
|
|
FAHRT_BEENDEN
|
|
ANWESENHEIT_KOMMEN
|
|
ANWESENHEIT_GEHEN
|
|
WAESCHE_ABGEBEN
|
|
INVENTUR_SCANNEN
|
|
PRUEFUNG_ERFASSEN
|
|
}
|
|
|
|
/// Warum sich ein nachgetragener Vorgang nicht anwenden ließ.
|
|
enum KonfliktGrund {
|
|
/// Der Datensatz, auf den er sich bezieht, gibt es nicht mehr.
|
|
ZIEL_FEHLT
|
|
/// Der Datensatz wurde zwischenzeitlich festgeschrieben.
|
|
ZIEL_GESPERRT
|
|
/// Zwei Terminals haben dasselbe erfasst.
|
|
DOPPELT
|
|
/// Die Angaben passen nicht mehr zum Stand auf dem Server.
|
|
VERALTET
|
|
SONSTIGES
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/// Eine Fahrt im Fahrtenbuch.
|
|
///
|
|
/// Nach § 6 StVO-Nachweispflicht und nach der Dienstanweisung der meisten
|
|
/// Wehren muss nachvollziehbar sein, wer wann mit welchem Fahrzeug unterwegs
|
|
/// war. Am Terminal ist das ein Vorgang von zwanzig Sekunden: Fahrzeug wählen,
|
|
/// Kilometerstand eintippen, Zweck antippen.
|
|
model Fahrt {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
fahrzeugId String @db.Uuid
|
|
fahrerId String @db.Uuid
|
|
|
|
zweck FahrtZweck @default(SONSTIGES)
|
|
/// Wenn die Fahrt zu einem Einsatz oder einer Übung gehört.
|
|
einsatzId String? @db.Uuid
|
|
uebungId String? @db.Uuid
|
|
|
|
abfahrtUm DateTime @default(now()) @db.Timestamptz(3)
|
|
rueckkehrUm DateTime? @db.Timestamptz(3)
|
|
|
|
kilometerAbfahrt Int
|
|
kilometerRueckkehr Int?
|
|
|
|
ziel String? @db.VarChar(200)
|
|
/// Getankte Menge in Litern, wenn unterwegs getankt wurde.
|
|
tankmenge Decimal? @db.Decimal(8, 2)
|
|
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)
|
|
fahrzeug Fahrzeug @relation(fields: [fahrzeugId], references: [id], onDelete: Restrict)
|
|
fahrer Person @relation("FahrtFahrer", fields: [fahrerId], references: [id], onDelete: Restrict)
|
|
einsatz Einsatz? @relation(fields: [einsatzId], references: [id], onDelete: SetNull)
|
|
uebung Uebung? @relation(fields: [uebungId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([organisationId, fahrzeugId, abfahrtUm(sort: Desc)])
|
|
@@index([organisationId, fahrerId])
|
|
@@map("fahrt")
|
|
}
|
|
|
|
/// Anwesenheit am Gerätehaus.
|
|
///
|
|
/// Wer eincheckt, ist da — für die Übungsteilnahme, für den Arbeitsdienst und
|
|
/// im Einsatzfall dafür, dass die Einsatzleitung weiß, wer im Haus ist.
|
|
model Anwesenheit {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
personId String @db.Uuid
|
|
|
|
anlass AnwesenheitAnlass @default(DIENST)
|
|
angemeldetUm DateTime @default(now()) @db.Timestamptz(3)
|
|
abgemeldetUm DateTime? @db.Timestamptz(3)
|
|
|
|
einsatzId String? @db.Uuid
|
|
uebungId String? @db.Uuid
|
|
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)
|
|
person Person @relation("AnwesenheitPerson", fields: [personId], references: [id], onDelete: Cascade)
|
|
einsatz Einsatz? @relation(fields: [einsatzId], references: [id], onDelete: SetNull)
|
|
uebung Uebung? @relation(fields: [uebungId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([organisationId, personId, angemeldetUm(sort: Desc)])
|
|
@@index([organisationId, abgemeldetUm])
|
|
@@map("anwesenheit")
|
|
}
|
|
|
|
/// Schutzkleidung in der Reinigung.
|
|
///
|
|
/// Wer seine Einsatzjacke abgibt, steht ohne da — deshalb hängt an der Abgabe
|
|
/// auch die Frage, ob Ersatz vorhanden ist. Erfasst wird am Terminal beim
|
|
/// Einwurf in den Wäschekorb.
|
|
model Waescheabgabe {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
personId String @db.Uuid
|
|
/// Wenn das Kleidungsstück als Gerät erfasst ist.
|
|
geraetId String? @db.Uuid
|
|
|
|
bezeichnung String @db.VarChar(200)
|
|
menge Int @default(1)
|
|
status WaescheStatus @default(ABGEGEBEN)
|
|
/// Grund: Einsatzverschmutzung, Kontamination, Routine.
|
|
grund String? @db.VarChar(200)
|
|
|
|
abgegebenAm DateTime @default(now()) @db.Timestamptz(3)
|
|
zurueckAm DateTime? @db.Timestamptz(3)
|
|
/// Wer die Rückgabe bestätigt hat.
|
|
angenommenVonId String? @db.Uuid
|
|
|
|
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)
|
|
person Person @relation("WaescheTraeger", fields: [personId], references: [id], onDelete: Cascade)
|
|
geraet Geraet? @relation(fields: [geraetId], references: [id], onDelete: SetNull)
|
|
angenommenVon Person? @relation("WaescheAnnahme", fields: [angenommenVonId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([organisationId, status])
|
|
@@index([organisationId, personId])
|
|
@@map("waescheabgabe")
|
|
}
|
|
|
|
/// Ein Inventurgang.
|
|
///
|
|
/// Der Gerätewart geht mit dem Tablet durch die Halle und scannt, was da ist.
|
|
/// Am Ende steht die Liste dessen, was fehlt — das ist der Zweck der Sache.
|
|
model Inventurgang {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
|
|
bezeichnung String @db.VarChar(160)
|
|
status InventurStatus @default(LAEUFT)
|
|
|
|
/// Worauf sich der Gang bezieht — ein Standort oder ein Fahrzeug.
|
|
standortId String? @db.Uuid
|
|
fahrzeugId String? @db.Uuid
|
|
|
|
begonnenAm DateTime @default(now()) @db.Timestamptz(3)
|
|
abgeschlossenAm DateTime? @db.Timestamptz(3)
|
|
begonnenVonId String? @db.Uuid
|
|
bemerkung String? @db.VarChar(1000)
|
|
|
|
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
|
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
|
|
|
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
|
standort Standort? @relation(fields: [standortId], references: [id], onDelete: SetNull)
|
|
fahrzeug Fahrzeug? @relation(fields: [fahrzeugId], references: [id], onDelete: SetNull)
|
|
begonnenVon Person? @relation("InventurLeitung", fields: [begonnenVonId], references: [id], onDelete: SetNull)
|
|
posten Inventurposten[]
|
|
|
|
@@index([organisationId, status])
|
|
@@map("inventurgang")
|
|
}
|
|
|
|
/// Ein gescanntes Gerät innerhalb eines Inventurgangs.
|
|
model Inventurposten {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
inventurId String @db.Uuid
|
|
geraetId String @db.Uuid
|
|
|
|
gescanntAm DateTime @default(now()) @db.Timestamptz(3)
|
|
/// Am erwarteten Ort vorgefunden.
|
|
gefunden Boolean @default(true)
|
|
/// Wo es tatsächlich lag, wenn es nicht dort war, wo es sein sollte.
|
|
vorgefundenAn String? @db.VarChar(200)
|
|
bemerkung String? @db.VarChar(500)
|
|
erfasstVonId String? @db.Uuid
|
|
|
|
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
|
inventur Inventurgang @relation(fields: [inventurId], references: [id], onDelete: Cascade)
|
|
geraet Geraet @relation(fields: [geraetId], references: [id], onDelete: Cascade)
|
|
erfasstVon Person? @relation("InventurErfasser", fields: [erfasstVonId], references: [id], onDelete: SetNull)
|
|
|
|
@@unique([inventurId, geraetId])
|
|
@@index([organisationId, inventurId])
|
|
@@map("inventurposten")
|
|
}
|
|
|
|
/// Ein vom Terminal übermittelter Vorgang.
|
|
///
|
|
/// Der Schlüssel zum Offline-Betrieb: die Kennung erzeugt der Browser, bevor
|
|
/// er den Vorgang in seine Warteschlange legt. Kommt derselbe Vorgang nach
|
|
/// einem abgebrochenen Sendeversuch ein zweites Mal an, erkennt der Server ihn
|
|
/// an dieser Kennung wieder und antwortet mit dem Ergebnis von damals, statt
|
|
/// alles noch einmal anzulegen.
|
|
///
|
|
/// Deshalb ist die Kennung der Primärschlüssel und wird nicht vergeben,
|
|
/// sondern angenommen.
|
|
model KioskVorgang {
|
|
/// Vom Browser erzeugt (crypto.randomUUID) — nicht vom Server.
|
|
id String @id @db.Uuid
|
|
organisationId String @db.Uuid
|
|
|
|
art KioskVorgangArt
|
|
personId String? @db.Uuid
|
|
|
|
/// Wann der Vorgang am Terminal erfasst wurde — kann deutlich vor dem
|
|
/// Eingang liegen.
|
|
erfasstAm DateTime @db.Timestamptz(3)
|
|
empfangenAm DateTime @default(now()) @db.Timestamptz(3)
|
|
|
|
/// Was der Vorgang bewirkt hat: die Kennung des angelegten Datensatzes.
|
|
ergebnisId String? @db.Uuid
|
|
/// Die Nutzlast, wie sie das Terminal geschickt hat — für die Fehlersuche
|
|
/// und für den Fall, dass ein Vorgang von Hand nachgezogen werden muss.
|
|
nutzlast Json? @db.JsonB
|
|
|
|
erfolgreich Boolean @default(true)
|
|
meldung String? @db.VarChar(500)
|
|
|
|
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
|
person Person? @relation("KioskVorgangPerson", fields: [personId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([organisationId, empfangenAm(sort: Desc)])
|
|
@@index([organisationId, art])
|
|
@@map("kiosk_vorgang")
|
|
}
|
|
|
|
/// Ein Vorgang, der sich beim Nachtragen nicht anwenden ließ.
|
|
///
|
|
/// Die Regel dahinter: bei Stammdaten gewinnt der Server, bei neuen
|
|
/// Datensätzen der Client. Was in keine der beiden Schubladen passt — ein
|
|
/// Mangel zu einem inzwischen ausgesonderten Gerät, eine Prüfung zu einem
|
|
/// festgeschriebenen Protokoll — landet hier und wartet auf einen Menschen.
|
|
model SyncKonflikt {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
|
|
/// Die Kennung des Vorgangs, der nicht durchging.
|
|
vorgangId String @db.Uuid
|
|
art KioskVorgangArt
|
|
grund KonfliktGrund @default(SONSTIGES)
|
|
|
|
beschreibung String @db.VarChar(500)
|
|
nutzlast Json? @db.JsonB
|
|
|
|
personId String? @db.Uuid
|
|
|
|
entstandenAm DateTime @default(now()) @db.Timestamptz(3)
|
|
erledigtAm DateTime? @db.Timestamptz(3)
|
|
erledigtVonId String? @db.Uuid
|
|
erledigung String? @db.VarChar(500)
|
|
|
|
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
|
person Person? @relation("KonfliktVerursacher", fields: [personId], references: [id], onDelete: SetNull)
|
|
erledigtVon Person? @relation("KonfliktErledigung", fields: [erledigtVonId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([organisationId, erledigtAm])
|
|
@@map("sync_konflikt")
|
|
}
|