Phase 0: Fundament der Feuerwehr-Verwaltungsplattform
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>
This commit is contained in:
@@ -0,0 +1,528 @@
|
||||
// =============================================================================
|
||||
// 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")
|
||||
}
|
||||
Reference in New Issue
Block a user