Files
FireAdministration/prisma/schema.prisma
T
Florian WildeandClaude Opus 5 91954c402b Phase 3: Einsatz- und Uebungserfassung
Schnellerfassung im Alarmfall, sechs Zeitstempel mit Ausrueckzeit, Anfahrt
und Hilfsfrist, Staerke nach FwDV 3, Personalvorschlag mit
Qualifikationsstand, Einsatzbericht als PDF, Jahresexport fuer die Gemeinde,
Statistik und Uebungen mit Anwesenheitsliste.

Dazu das Einsatzportal fuer Fremdwehren: ein signierter Link ohne Anmeldung,
zeitlich und mengenbegrenzt, der Stichwort, Ort, Zeiten und Lagemeldungen
zeigt und Meldungen mit Foto annimmt. Es sind die ersten oeffentlich
erreichbaren Endpunkte in FLORIAN.

Acht neue Tabellen, alle mit organisationId und Row Level Security. Ein
freigegebener Einsatzbericht ist unveraenderlich; ein Datenbanktrigger setzt
das durch. Koordinaten liegen doppelt — als Dezimalgrad und als
PostGIS-Punkt, zusammengehalten von einem Trigger.

57 neue Tests (32 Unit, 14 gegen eine echte Datenbank, 11 E2E).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 13:50:58 +02:00

1859 lines
68 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[]
@@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")
/// 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[]
@@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[]
@@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[]
@@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[]
@@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[]
@@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")
}