// ============================================================================= // 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[] @@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") /// 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//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") } // ============================================================================= // Phase 1 — Stammdaten // // Ordnungsprinzip: // Standort wo etwas steht (Gerätehaus, Lager, Außenstelle) // Fahrzeug ein Einsatzfahrzeug, es hat Geräteräume // Geraeteraum ein Fach am Fahrzeug (G1 … G6, Dachkasten, Mannschaftsraum) // Geraetetyp die Bauart samt Prüfvorgabe — "Pressluftatmer PA 94" // Geraet das einzelne Stück mit Inventarnummer // // Ein Gerät liegt ENTWEDER an einem Standort ODER verlastet in einem // Geräteraum eines Fahrzeugs. Beides gleichzeitig oder gar nichts wäre eine // Falschangabe; eine Prüfbedingung in der Datenbank verhindert das. // ============================================================================= /// Art eines Standorts. enum StandortArt { GERAETEHAUS LAGER AUSSENSTELLE WERKSTATT SONSTIGES } /// Fachliche Einordnung eines Gerätetyps — bestimmt die Gruppierung in Listen. enum GeraeteKategorie { ATEMSCHUTZ BELEUCHTUNG BRANDBEKAEMPFUNG ELEKTRO FUNK HYGIENE MEDIZIN MESSTECHNIK PERSOENLICHE_SCHUTZAUSRUESTUNG RETTUNG SCHLAUCHMATERIAL TECHNISCHE_HILFELEISTUNG UMWELTSCHUTZ WASSERFOERDERUNG SONSTIGES } /// Lebensdauerkategorie. Diese Gruppen haben eine maximale Nutzungsdauer ab /// Herstellungs- oder Indienststellungsdatum, unabhängig vom Zustand. enum LebensdauerKategorie { KEINE HELM SCHUTZKLEIDUNG HANDSCHUHE STIEFEL SEIL_GURT PRESSLUFTATMER ATEMANSCHLUSS DRUCKBEHAELTER SCHLAUCH LEITER SONSTIGE } /// Woran sich die maximale Nutzungsdauer bemisst. enum LebensdauerBezug { HERSTELLUNG INDIENSTSTELLUNG } /// Art einer Qualifikation. enum QualifikationsArt { LEHRGANG BEFAEHIGUNG UNTERSUCHUNG FUEHRERSCHEIN UNTERWEISUNG SONSTIGES } // ----------------------------------------------------------------------------- /// Ein Ort, an dem Fahrzeuge stehen und Geräte lagern. model Standort { id String @id @default(uuid()) @db.Uuid organisationId String @db.Uuid name String @db.VarChar(160) /// Kurzzeichen für Listen und Etiketten, z. B. "GH" oder "LAG". kuerzel String? @db.VarChar(20) art StandortArt @default(GERAETEHAUS) strasse String? @db.VarChar(200) plz String? @db.VarChar(10) ort String? @db.VarChar(120) /// Koordinaten als PostGIS-Punkt (WGS 84). Prisma kennt den Typ nicht, /// deshalb Unsupported — gelesen und geschrieben wird per Rohabfrage. /// Ab Phase 6 zeigt die Karte diese Punkte an. koordinaten Unsupported("geography(Point, 4326)")? bemerkung String? @db.VarChar(1000) aktiv Boolean @default(true) erstelltAm DateTime @default(now()) @db.Timestamptz(3) geaendertAm DateTime @updatedAt @db.Timestamptz(3) geloeschtAm DateTime? @db.Timestamptz(3) organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade) fahrzeuge Fahrzeug[] geraete Geraet[] @@unique([organisationId, name]) @@index([organisationId, aktiv]) @@map("standort") } /// Ein Einsatzfahrzeug. model Fahrzeug { id String @id @default(uuid()) @db.Uuid organisationId String @db.Uuid /// Funkrufname, z. B. "Florian Musterhausen 40/1". funkrufname String @db.VarChar(120) bezeichnung String @db.VarChar(120) art FahrzeugArt @default(LOESCHFAHRZEUG) /// Typkurzzeichen nach DIN, z. B. "LF 20", "TSF-W", "DLA(K) 23/12". dinTyp String? @db.VarChar(40) kennzeichen String? @db.VarChar(20) hersteller String? @db.VarChar(120) aufbauhersteller String? @db.VarChar(120) fahrgestellnummer String? @db.VarChar(30) baujahr Int? erstzulassung DateTime? @db.Date indienststellung DateTime? @db.Date /// Sitzplätze für die Stärkeberechnung ab Phase 3. sitzplaetze Int? /// Löschwasser in Litern. tankinhalt Int? /// Zulässige Gesamtmasse in kg. gesamtmasse Int? kilometerstand Int? /// Nächste Hauptuntersuchung. tuevBis DateTime? @db.Date /// Nächste Sicherheitsprüfung nach UVV. uvvBis DateTime? @db.Date naechsteWartung DateTime? @db.Date standortId String? @db.Uuid aktiv Boolean @default(true) bemerkung String? @db.VarChar(1000) erstelltAm DateTime @default(now()) @db.Timestamptz(3) geaendertAm DateTime @updatedAt @db.Timestamptz(3) geloeschtAm DateTime? @db.Timestamptz(3) organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade) standort Standort? @relation(fields: [standortId], references: [id], onDelete: SetNull) geraeteraeume Geraeteraum[] geraete Geraet[] @@unique([organisationId, funkrufname]) @@unique([organisationId, kennzeichen]) @@index([organisationId, aktiv]) @@index([organisationId, tuevBis]) @@map("fahrzeug") } /// Ein Fach am Fahrzeug. Die Nummerierung folgt der Beladeordnung: G1 bis G6 /// an den Seiten, dazu Dachkasten, Heck und Mannschaftsraum. model Geraeteraum { id String @id @default(uuid()) @db.Uuid organisationId String @db.Uuid fahrzeugId String @db.Uuid /// Kurzzeichen wie "G1", "GR", "Dach". bezeichnung String @db.VarChar(40) beschreibung String? @db.VarChar(200) /// Reihenfolge in der Anzeige — entspricht der Beladeordnung. sortierung Int @default(0) erstelltAm DateTime @default(now()) @db.Timestamptz(3) geaendertAm DateTime @updatedAt @db.Timestamptz(3) geloeschtAm DateTime? @db.Timestamptz(3) organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade) fahrzeug Fahrzeug @relation(fields: [fahrzeugId], references: [id], onDelete: Cascade) geraete Geraet[] @@unique([fahrzeugId, bezeichnung]) @@index([organisationId, fahrzeugId]) @@map("geraeteraum") } /// Die Bauart eines Geräts samt Prüf- und Lebensdauervorgabe. Was hier steht, /// gilt als Vorgabe für jedes Gerät dieses Typs; einzelne Geräte dürfen davon /// abweichen, wenn der Hersteller etwas anderes vorschreibt. model Geraetetyp { id String @id @default(uuid()) @db.Uuid organisationId String @db.Uuid name String @db.VarChar(160) kategorie GeraeteKategorie @default(SONSTIGES) hersteller String? @db.VarChar(120) modell String? @db.VarChar(120) /// Vorgabe für das Prüfintervall in Monaten. pruefintervallMonate Int? /// Fundstelle der Prüfvorschrift, z. B. "DGUV Vorschrift 49" oder /// "DIN 14800-16". pruefvorschrift String? @db.VarChar(200) lebensdauerKategorie LebensdauerKategorie @default(KEINE) /// Maximale Nutzungsdauer in Jahren. lebensdauerJahre Int? lebensdauerBezug LebensdauerBezug @default(INDIENSTSTELLUNG) /// Wie viele Monate vor Ablauf gewarnt wird. vorwarnzeitMonate Int @default(3) beschreibung String? @db.VarChar(1000) aktiv Boolean @default(true) erstelltAm DateTime @default(now()) @db.Timestamptz(3) geaendertAm DateTime @updatedAt @db.Timestamptz(3) geloeschtAm DateTime? @db.Timestamptz(3) organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade) geraete Geraet[] @@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) @@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") }