Legt den kompletten Unterbau an, auf dem die Fachmodule aufsetzen. Betrieb: - docker-compose.yml mit Traefik, App, Worker, PostgreSQL 17 + PostGIS, Redis, MinIO und Gotenberg; Entwicklungs-Override ohne Traefik, dafuer mit Mailpit und Adminer - Mehrstufiges Dockerfile: eigener Benutzer, tini als PID 1, Healthcheck - .env.example mit einem Kommentar zu jeder Variable Mandantentrennung: - Row Level Security auf allen mandantenpflichtigen Tabellen - Zwei getrennte Datenbankverbindungen: florian_app mit aktiver RLS fuer die Anwendung, Eigentuemer-Zugang nur fuer Migration, Seed und Anmeldung - fuerOrganisation() als einziger Weg zu Fachdaten Nachvollziehbarkeit: - AuditLog append-only per Trigger; UPDATE ist dauerhaft gesperrt, DELETE nur im gekennzeichneten Aufbewahrungslauf - Geheimnisse werden vor dem Schreiben entfernt Anmeldung: - OIDC gegen ein extern betriebenes authentik, Rollen aus Gruppen nur als Vorbelegung beim ersten Login - Lokaler E-Mail/Passwort-Login als Rueckfallweg - Terminal per PIN und RFID mit zwei Schranken (Geraet, dann Person) Weiteres: - lib/authz.ts mit 9 Rollen und 38 Einzelrechten - lib/storage.ts (MinIO), lib/audit.ts, lib/warteschlange.ts (BullMQ) - Integrationen fuer Mail, Telegram und LLM hinter abschaltbaren Interfaces - tRPC v11, Basis-Layout mit Seitenleiste und dunkler Darstellung - Seed mit einer Beispielwehr: 16 Personen, 3 Fahrzeuge, 40 Geraete - 32 Vitest-Tests (davon 8 gegen eine echte Datenbank), 10 Playwright-Tests - README, Anleitung mit Screenshots, Roadmap, Aufwandsschaetzung, Devlog, Versionierungsregeln und Changelog Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
529 lines
18 KiB
Plaintext
529 lines
18 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[]
|
|
|
|
@@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")
|
|
|
|
/// 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")
|
|
|
|
@@unique([bucket, objektSchluessel])
|
|
@@index([organisationId, entitaet, entitaetId])
|
|
@@index([organisationId, kategorie])
|
|
@@map("datei")
|
|
}
|
|
|
|
// =============================================================================
|
|
// Vorgriff auf Phase 1
|
|
// Diese beiden Modelle sind bewusst schmal gehalten. Sie existieren, damit das
|
|
// Seed-Skript eine realistische Wehr aufbauen kann und die Mandantentrennung
|
|
// an echten Daten prüfbar ist. Phase 1 erweitert sie um Funkrufname-Systematik,
|
|
// DIN-Typisierung, Geräteräume, Prüfintervalle und Lebensdauerkategorien.
|
|
// =============================================================================
|
|
|
|
/// Ein Einsatzfahrzeug der Wehr.
|
|
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)
|
|
kennzeichen String? @db.VarChar(20)
|
|
hersteller String? @db.VarChar(120)
|
|
baujahr Int?
|
|
/// Nächste Hauptuntersuchung.
|
|
tuevBis DateTime? @db.Date
|
|
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[]
|
|
|
|
@@unique([organisationId, funkrufname])
|
|
@@index([organisationId, aktiv])
|
|
@@map("fahrzeug")
|
|
}
|
|
|
|
/// Ein prüfpflichtiges Gerät — Pressluftatmer, Schlauch, Leiter, Motorsäge.
|
|
model Geraet {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
organisationId String @db.Uuid
|
|
|
|
inventarnummer String @db.VarChar(60)
|
|
bezeichnung String @db.VarChar(160)
|
|
hersteller String? @db.VarChar(120)
|
|
seriennummer String? @db.VarChar(120)
|
|
|
|
herstellungsdatum DateTime? @db.Date
|
|
indienststellung DateTime? @db.Date
|
|
|
|
status GeraetStatus @default(EINSATZBEREIT)
|
|
/// Prüfintervall in Monaten aus Vorschrift oder Herstellerangabe.
|
|
pruefintervallMonate Int?
|
|
naechstePruefung DateTime? @db.Date
|
|
/// Maximale Nutzungsdauer in Jahren (Helme, Seile, Schutzkleidung).
|
|
lebensdauerJahre Int?
|
|
|
|
/// Verlastet auf einem Fahrzeug — oder im Gerätehaus, dann null.
|
|
fahrzeugId String? @db.Uuid
|
|
lagerort String? @db.VarChar(160)
|
|
|
|
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: SetNull)
|
|
|
|
@@unique([organisationId, inventarnummer])
|
|
@@index([organisationId, status])
|
|
@@index([organisationId, naechstePruefung])
|
|
@@index([organisationId, fahrzeugId])
|
|
@@map("geraet")
|
|
}
|