Phase 2: Wartungs- und Pruefmanagement
Wartungsvorlagen mit Checklisten, Prueftermine aus einem naechtlichen Lauf, Prueffristen-Dashboard, Pruefwizard mit Foto und Unterschrift, unveraenderliche Protokolle als PDF, Maengel-Workflow, Rueckrufe und Lebensdauerueberwachung. Elf neue Tabellen, alle mit organisationId und Row Level Security. Die Unveraenderbarkeit abgeschlossener Protokolle setzt ein Datenbanktrigger durch, nicht der Anwendungscode: eine Korrektur erzeugt eine neue Pruefung mit Verweis auf die alte, beide bleiben lesbar. 79 neue Tests (47 Unit, 10 gegen eine echte Datenbank, 22 E2E). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7f866ac35c
commit
e503aafc18
@@ -0,0 +1,469 @@
|
||||
-- =============================================================================
|
||||
-- Phase 2: Tabellen für Wartung und Prüfung.
|
||||
--
|
||||
-- Erzeugt mit `prisma migrate dev --create-only` und danach an einer Stelle von
|
||||
-- Hand bereinigt: Prisma wollte die Volltext- und Geoindizes aus Phase 1
|
||||
-- (gin_trgm_ops, gist auf Koordinaten) löschen, weil es sie im Schema nicht
|
||||
-- kennt — Prisma kann diese Indexarten nicht abbilden. Die DROP INDEX-Zeilen
|
||||
-- sind deshalb entfernt. Wer diese Migration neu erzeugt, muss das wieder tun.
|
||||
--
|
||||
-- Mandantentrennung, Unveränderbarkeit der Protokolle und die Suchindizes für
|
||||
-- die neuen Tabellen stehen in der nachfolgenden Migration.
|
||||
-- =============================================================================
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WartungsterminStatus" AS ENUM ('OFFEN', 'ERLEDIGT', 'ENTFALLEN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PruefungStatus" AS ENUM ('ENTWURF', 'ABGESCHLOSSEN', 'ERSETZT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PruefErgebnis" AS ENUM ('BESTANDEN', 'BESTANDEN_MIT_MAENGELN', 'NICHT_BESTANDEN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PruefungArt" AS ENUM ('INTERN', 'EXTERN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ChecklistenpunktTyp" AS ENUM ('JA_NEIN', 'MESSWERT', 'TEXT', 'FOTO');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SignaturRolle" AS ENUM ('PRUEFER', 'GERAETEWART', 'FREMDFIRMA');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "MangelPrioritaet" AS ENUM ('NIEDRIG', 'MITTEL', 'HOCH', 'KRITISCH');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "MangelStatus" AS ENUM ('GEMELDET', 'ZUGEWIESEN', 'IN_ARBEIT', 'BEHOBEN', 'VERWORFEN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RueckrufStatus" AS ENUM ('OFFEN', 'IN_BEARBEITUNG', 'ERLEDIGT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "BetroffenheitStatus" AS ENUM ('OFFEN', 'NICHT_BETROFFEN', 'MASSNAHME_LAEUFT', 'ERLEDIGT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ErinnerungStufe" AS ENUM ('T_30', 'T_7', 'T_0', 'UEBERFAELLIG');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "wartungsvorlage" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"name" VARCHAR(160) NOT NULL,
|
||||
"geraetetypId" UUID,
|
||||
"intervallMonate" INTEGER NOT NULL,
|
||||
"vorwarnzeitTage" INTEGER NOT NULL DEFAULT 30,
|
||||
"pruefvorschrift" VARCHAR(200),
|
||||
"standardDauerMinuten" INTEGER,
|
||||
"standardExtern" BOOLEAN NOT NULL DEFAULT false,
|
||||
"automatisch" BOOLEAN NOT NULL DEFAULT true,
|
||||
"beschreibung" VARCHAR(1000),
|
||||
"aktiv" BOOLEAN NOT NULL DEFAULT true,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
"geloeschtAm" TIMESTAMPTZ(3),
|
||||
|
||||
CONSTRAINT "wartungsvorlage_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "checklistenpunkt" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"vorlageId" UUID NOT NULL,
|
||||
"text" VARCHAR(300) NOT NULL,
|
||||
"typ" "ChecklistenpunktTyp" NOT NULL DEFAULT 'JA_NEIN',
|
||||
"pflicht" BOOLEAN NOT NULL DEFAULT true,
|
||||
"sollwert" VARCHAR(120),
|
||||
"einheit" VARCHAR(20),
|
||||
"minWert" DECIMAL(12,3),
|
||||
"maxWert" DECIMAL(12,3),
|
||||
"hinweis" VARCHAR(500),
|
||||
"sortierung" INTEGER NOT NULL DEFAULT 0,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
"geloeschtAm" TIMESTAMPTZ(3),
|
||||
|
||||
CONSTRAINT "checklistenpunkt_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "wartungstermin" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"geraetId" UUID NOT NULL,
|
||||
"vorlageId" UUID,
|
||||
"faelligAm" DATE NOT NULL,
|
||||
"status" "WartungsterminStatus" NOT NULL DEFAULT 'OFFEN',
|
||||
"automatischErzeugt" BOOLEAN NOT NULL DEFAULT false,
|
||||
"zugewiesenAnId" UUID,
|
||||
"bemerkung" VARCHAR(1000),
|
||||
"erledigtAm" TIMESTAMPTZ(3),
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
"geloeschtAm" TIMESTAMPTZ(3),
|
||||
|
||||
CONSTRAINT "wartungstermin_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "pruefung" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"geraetId" UUID NOT NULL,
|
||||
"terminId" UUID,
|
||||
"vorlageId" UUID,
|
||||
"art" "PruefungArt" NOT NULL DEFAULT 'INTERN',
|
||||
"status" "PruefungStatus" NOT NULL DEFAULT 'ENTWURF',
|
||||
"prueferId" UUID,
|
||||
"prueferName" VARCHAR(200),
|
||||
"durchgefuehrtAm" DATE NOT NULL,
|
||||
"begonnenUm" TIMESTAMPTZ(3),
|
||||
"dauerMinuten" INTEGER,
|
||||
"ergebnis" "PruefErgebnis",
|
||||
"externeFirma" VARCHAR(200),
|
||||
"externeProtokollDateiId" UUID,
|
||||
"kosten" DECIMAL(10,2),
|
||||
"naechstePruefungAm" DATE,
|
||||
"protokollDateiId" UUID,
|
||||
"protokollNummer" VARCHAR(40),
|
||||
"abgeschlossenAm" TIMESTAMPTZ(3),
|
||||
"ersetztPruefungId" UUID,
|
||||
"korrekturGrund" VARCHAR(500),
|
||||
"bemerkung" VARCHAR(2000),
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
"geloeschtAm" TIMESTAMPTZ(3),
|
||||
|
||||
CONSTRAINT "pruefung_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "pruefergebnis" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"pruefungId" UUID NOT NULL,
|
||||
"punktId" UUID NOT NULL,
|
||||
"jaNein" BOOLEAN,
|
||||
"zahlwert" DECIMAL(12,3),
|
||||
"text" VARCHAR(1000),
|
||||
"fotoDateiId" UUID,
|
||||
"inOrdnung" BOOLEAN NOT NULL DEFAULT true,
|
||||
"bemerkung" VARCHAR(500),
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "pruefergebnis_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "signatur" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"pruefungId" UUID NOT NULL,
|
||||
"rolle" "SignaturRolle" NOT NULL DEFAULT 'PRUEFER',
|
||||
"personId" UUID,
|
||||
"name" VARCHAR(200) NOT NULL,
|
||||
"bildDateiId" UUID,
|
||||
"unterzeichnetAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "signatur_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "mangel" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"geraetId" UUID NOT NULL,
|
||||
"pruefungId" UUID,
|
||||
"melderId" UUID,
|
||||
"melderName" VARCHAR(200),
|
||||
"beschreibung" VARCHAR(2000) NOT NULL,
|
||||
"fotoDateiId" UUID,
|
||||
"prioritaet" "MangelPrioritaet" NOT NULL DEFAULT 'MITTEL',
|
||||
"status" "MangelStatus" NOT NULL DEFAULT 'GEMELDET',
|
||||
"zugewiesenAnId" UUID,
|
||||
"gemeldetAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"behobenAm" TIMESTAMPTZ(3),
|
||||
"behobenVonId" UUID,
|
||||
"behebung" VARCHAR(2000),
|
||||
"geraetGesperrt" BOOLEAN NOT NULL DEFAULT false,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
"geloeschtAm" TIMESTAMPTZ(3),
|
||||
|
||||
CONSTRAINT "mangel_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "rueckruf" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"hersteller" VARCHAR(160) NOT NULL,
|
||||
"titel" VARCHAR(200) NOT NULL,
|
||||
"referenz" VARCHAR(80),
|
||||
"beschreibung" VARCHAR(4000),
|
||||
"massnahme" VARCHAR(2000),
|
||||
"veroeffentlichtAm" DATE,
|
||||
"quelle" VARCHAR(500),
|
||||
"sperrtGeraete" BOOLEAN NOT NULL DEFAULT false,
|
||||
"status" "RueckrufStatus" NOT NULL DEFAULT 'OFFEN',
|
||||
"erledigtAm" TIMESTAMPTZ(3),
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
"geloeschtAm" TIMESTAMPTZ(3),
|
||||
|
||||
CONSTRAINT "rueckruf_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "rueckruf_geraetetyp" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"rueckrufId" UUID NOT NULL,
|
||||
"geraetetypId" UUID,
|
||||
"modell" VARCHAR(160),
|
||||
"seriennummerVon" VARCHAR(120),
|
||||
"seriennummerBis" VARCHAR(120),
|
||||
"herstellungVon" DATE,
|
||||
"herstellungBis" DATE,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "rueckruf_geraetetyp_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "rueckruf_betroffenheit" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"rueckrufId" UUID NOT NULL,
|
||||
"geraetId" UUID NOT NULL,
|
||||
"status" "BetroffenheitStatus" NOT NULL DEFAULT 'OFFEN',
|
||||
"festgestelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"erledigtAm" TIMESTAMPTZ(3),
|
||||
"bemerkung" VARCHAR(1000),
|
||||
"geraetGesperrt" BOOLEAN NOT NULL DEFAULT false,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "rueckruf_betroffenheit_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "erinnerung" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"terminId" UUID NOT NULL,
|
||||
"stufe" "ErinnerungStufe" NOT NULL,
|
||||
"versendetAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"kanal" VARCHAR(40) NOT NULL DEFAULT 'mail',
|
||||
"empfaenger" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
"erfolgreich" BOOLEAN NOT NULL DEFAULT true,
|
||||
"meldung" VARCHAR(500),
|
||||
|
||||
CONSTRAINT "erinnerung_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "wartungsvorlage_organisationId_geraetetypId_idx" ON "wartungsvorlage"("organisationId", "geraetetypId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "wartungsvorlage_organisationId_aktiv_idx" ON "wartungsvorlage"("organisationId", "aktiv");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "wartungsvorlage_organisationId_name_key" ON "wartungsvorlage"("organisationId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "checklistenpunkt_organisationId_vorlageId_idx" ON "checklistenpunkt"("organisationId", "vorlageId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "wartungstermin_organisationId_faelligAm_idx" ON "wartungstermin"("organisationId", "faelligAm");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "wartungstermin_organisationId_status_faelligAm_idx" ON "wartungstermin"("organisationId", "status", "faelligAm");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "wartungstermin_organisationId_geraetId_idx" ON "wartungstermin"("organisationId", "geraetId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "wartungstermin_organisationId_zugewiesenAnId_idx" ON "wartungstermin"("organisationId", "zugewiesenAnId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "wartungstermin_geraetId_vorlageId_faelligAm_key" ON "wartungstermin"("geraetId", "vorlageId", "faelligAm");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "pruefung_organisationId_geraetId_durchgefuehrtAm_idx" ON "pruefung"("organisationId", "geraetId", "durchgefuehrtAm" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "pruefung_organisationId_status_idx" ON "pruefung"("organisationId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "pruefung_organisationId_durchgefuehrtAm_idx" ON "pruefung"("organisationId", "durchgefuehrtAm" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "pruefung_organisationId_prueferId_idx" ON "pruefung"("organisationId", "prueferId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "pruefung_organisationId_protokollNummer_key" ON "pruefung"("organisationId", "protokollNummer");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "pruefergebnis_organisationId_pruefungId_idx" ON "pruefergebnis"("organisationId", "pruefungId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "pruefergebnis_pruefungId_punktId_key" ON "pruefergebnis"("pruefungId", "punktId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "signatur_organisationId_pruefungId_idx" ON "signatur"("organisationId", "pruefungId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "mangel_organisationId_status_prioritaet_idx" ON "mangel"("organisationId", "status", "prioritaet");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "mangel_organisationId_geraetId_idx" ON "mangel"("organisationId", "geraetId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "mangel_organisationId_zugewiesenAnId_idx" ON "mangel"("organisationId", "zugewiesenAnId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "rueckruf_organisationId_status_idx" ON "rueckruf"("organisationId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "rueckruf_organisationId_hersteller_idx" ON "rueckruf"("organisationId", "hersteller");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "rueckruf_geraetetyp_organisationId_rueckrufId_idx" ON "rueckruf_geraetetyp"("organisationId", "rueckrufId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "rueckruf_betroffenheit_organisationId_rueckrufId_idx" ON "rueckruf_betroffenheit"("organisationId", "rueckrufId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "rueckruf_betroffenheit_organisationId_geraetId_idx" ON "rueckruf_betroffenheit"("organisationId", "geraetId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "rueckruf_betroffenheit_rueckrufId_geraetId_key" ON "rueckruf_betroffenheit"("rueckrufId", "geraetId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "erinnerung_organisationId_versendetAm_idx" ON "erinnerung"("organisationId", "versendetAm" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "erinnerung_terminId_stufe_key" ON "erinnerung"("terminId", "stufe");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "wartungsvorlage" ADD CONSTRAINT "wartungsvorlage_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "wartungsvorlage" ADD CONSTRAINT "wartungsvorlage_geraetetypId_fkey" FOREIGN KEY ("geraetetypId") REFERENCES "geraetetyp"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "checklistenpunkt" ADD CONSTRAINT "checklistenpunkt_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "checklistenpunkt" ADD CONSTRAINT "checklistenpunkt_vorlageId_fkey" FOREIGN KEY ("vorlageId") REFERENCES "wartungsvorlage"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "wartungstermin" ADD CONSTRAINT "wartungstermin_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "wartungstermin" ADD CONSTRAINT "wartungstermin_geraetId_fkey" FOREIGN KEY ("geraetId") REFERENCES "geraet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "wartungstermin" ADD CONSTRAINT "wartungstermin_vorlageId_fkey" FOREIGN KEY ("vorlageId") REFERENCES "wartungsvorlage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "wartungstermin" ADD CONSTRAINT "wartungstermin_zugewiesenAnId_fkey" FOREIGN KEY ("zugewiesenAnId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pruefung" ADD CONSTRAINT "pruefung_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pruefung" ADD CONSTRAINT "pruefung_geraetId_fkey" FOREIGN KEY ("geraetId") REFERENCES "geraet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pruefung" ADD CONSTRAINT "pruefung_terminId_fkey" FOREIGN KEY ("terminId") REFERENCES "wartungstermin"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pruefung" ADD CONSTRAINT "pruefung_vorlageId_fkey" FOREIGN KEY ("vorlageId") REFERENCES "wartungsvorlage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pruefung" ADD CONSTRAINT "pruefung_prueferId_fkey" FOREIGN KEY ("prueferId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pruefung" ADD CONSTRAINT "pruefung_protokollDateiId_fkey" FOREIGN KEY ("protokollDateiId") REFERENCES "datei"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pruefung" ADD CONSTRAINT "pruefung_externeProtokollDateiId_fkey" FOREIGN KEY ("externeProtokollDateiId") REFERENCES "datei"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pruefung" ADD CONSTRAINT "pruefung_ersetztPruefungId_fkey" FOREIGN KEY ("ersetztPruefungId") REFERENCES "pruefung"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pruefergebnis" ADD CONSTRAINT "pruefergebnis_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pruefergebnis" ADD CONSTRAINT "pruefergebnis_pruefungId_fkey" FOREIGN KEY ("pruefungId") REFERENCES "pruefung"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pruefergebnis" ADD CONSTRAINT "pruefergebnis_punktId_fkey" FOREIGN KEY ("punktId") REFERENCES "checklistenpunkt"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pruefergebnis" ADD CONSTRAINT "pruefergebnis_fotoDateiId_fkey" FOREIGN KEY ("fotoDateiId") REFERENCES "datei"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "signatur" ADD CONSTRAINT "signatur_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "signatur" ADD CONSTRAINT "signatur_pruefungId_fkey" FOREIGN KEY ("pruefungId") REFERENCES "pruefung"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "signatur" ADD CONSTRAINT "signatur_personId_fkey" FOREIGN KEY ("personId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "signatur" ADD CONSTRAINT "signatur_bildDateiId_fkey" FOREIGN KEY ("bildDateiId") REFERENCES "datei"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "mangel" ADD CONSTRAINT "mangel_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "mangel" ADD CONSTRAINT "mangel_geraetId_fkey" FOREIGN KEY ("geraetId") REFERENCES "geraet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "mangel" ADD CONSTRAINT "mangel_pruefungId_fkey" FOREIGN KEY ("pruefungId") REFERENCES "pruefung"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "mangel" ADD CONSTRAINT "mangel_melderId_fkey" FOREIGN KEY ("melderId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "mangel" ADD CONSTRAINT "mangel_zugewiesenAnId_fkey" FOREIGN KEY ("zugewiesenAnId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "mangel" ADD CONSTRAINT "mangel_behobenVonId_fkey" FOREIGN KEY ("behobenVonId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "mangel" ADD CONSTRAINT "mangel_fotoDateiId_fkey" FOREIGN KEY ("fotoDateiId") REFERENCES "datei"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "rueckruf" ADD CONSTRAINT "rueckruf_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "rueckruf_geraetetyp" ADD CONSTRAINT "rueckruf_geraetetyp_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "rueckruf_geraetetyp" ADD CONSTRAINT "rueckruf_geraetetyp_rueckrufId_fkey" FOREIGN KEY ("rueckrufId") REFERENCES "rueckruf"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "rueckruf_geraetetyp" ADD CONSTRAINT "rueckruf_geraetetyp_geraetetypId_fkey" FOREIGN KEY ("geraetetypId") REFERENCES "geraetetyp"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "rueckruf_betroffenheit" ADD CONSTRAINT "rueckruf_betroffenheit_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "rueckruf_betroffenheit" ADD CONSTRAINT "rueckruf_betroffenheit_rueckrufId_fkey" FOREIGN KEY ("rueckrufId") REFERENCES "rueckruf"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "rueckruf_betroffenheit" ADD CONSTRAINT "rueckruf_betroffenheit_geraetId_fkey" FOREIGN KEY ("geraetId") REFERENCES "geraet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "erinnerung" ADD CONSTRAINT "erinnerung_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "erinnerung" ADD CONSTRAINT "erinnerung_terminId_fkey" FOREIGN KEY ("terminId") REFERENCES "wartungstermin"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,243 @@
|
||||
-- =============================================================================
|
||||
-- Phase 2: Mandantentrennung, Unveränderbarkeit der Prüfprotokolle, Indizes.
|
||||
--
|
||||
-- Die vorige Migration hat die Tabellen angelegt. Drei Dinge lassen sich nur
|
||||
-- in der Datenbank verlässlich durchsetzen und stehen deshalb hier:
|
||||
--
|
||||
-- 1. Row Level Security für die elf neuen Tabellen — dieselbe Behandlung wie
|
||||
-- für die Tabellen aus Phase 0 und 1.
|
||||
--
|
||||
-- 2. Die Unveränderbarkeit abgeschlossener Prüfprotokolle. Ein Protokoll ist
|
||||
-- ein Nachweis gegenüber der Unfallkasse. Wäre es nachträglich änderbar,
|
||||
-- wäre es als Nachweis wertlos — und ein vergessener Zustandscheck im
|
||||
-- Code reicht, um genau das zu ermöglichen. Deshalb sitzt die Sperre in
|
||||
-- der Datenbank, nicht nur in lib/ und server/.
|
||||
--
|
||||
-- 3. Indizes für das Fristen-Dashboard und die Suche über Mängel und
|
||||
-- Rückrufe.
|
||||
-- =============================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Row Level Security
|
||||
-- -----------------------------------------------------------------------------
|
||||
DO $$
|
||||
DECLARE
|
||||
tabelle text;
|
||||
BEGIN
|
||||
FOREACH tabelle IN ARRAY ARRAY[
|
||||
'wartungsvorlage',
|
||||
'checklistenpunkt',
|
||||
'wartungstermin',
|
||||
'pruefung',
|
||||
'pruefergebnis',
|
||||
'signatur',
|
||||
'mangel',
|
||||
'rueckruf',
|
||||
'rueckruf_geraetetyp',
|
||||
'rueckruf_betroffenheit',
|
||||
'erinnerung'
|
||||
]
|
||||
LOOP
|
||||
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', tabelle);
|
||||
EXECUTE format(
|
||||
'CREATE POLICY %I ON %I FOR ALL '
|
||||
'USING ("organisationId" = aktuelle_organisation()) '
|
||||
'WITH CHECK ("organisationId" = aktuelle_organisation())',
|
||||
tabelle || '_mandant', tabelle
|
||||
);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Unveränderbarkeit abgeschlossener Prüfungen
|
||||
--
|
||||
-- Solange eine Prüfung im Status ENTWURF ist, darf der Wizard alles ändern.
|
||||
-- Mit dem Abschluss ist Schluss. Genau drei Änderungen bleiben danach zulässig,
|
||||
-- und alle drei ergänzen das Protokoll, statt es zu verfälschen:
|
||||
--
|
||||
-- * status ABGESCHLOSSEN -> ERSETZT, wenn eine Korrekturprüfung nachfolgt
|
||||
-- * protokollDateiId und protokollNummer, solange sie noch leer sind: das
|
||||
-- PDF entsteht im Hintergrundprozess und wird nachgetragen
|
||||
-- * geaendertAm, das Prisma bei jedem Schreibvorgang mitsetzt
|
||||
--
|
||||
-- Alles andere — Ergebnis, Datum, Prüfer, Messwerte — ist festgeschrieben.
|
||||
-- Korrekturen laufen über eine neue Prüfung mit Verweis auf die alte.
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION pruefung_unveraenderlich()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
vergleich "pruefung"%ROWTYPE;
|
||||
BEGIN
|
||||
-- Entwürfe sind frei änderbar.
|
||||
IF OLD."status" = 'ENTWURF' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
-- Die zulässigen Änderungen auf den alten Stand zurückdrehen. Bleibt danach
|
||||
-- ein Unterschied übrig, wurde etwas Unzulässiges angefasst.
|
||||
vergleich := NEW;
|
||||
vergleich."geaendertAm" := OLD."geaendertAm";
|
||||
|
||||
IF OLD."status" = 'ABGESCHLOSSEN' AND NEW."status" = 'ERSETZT' THEN
|
||||
vergleich."status" := OLD."status";
|
||||
END IF;
|
||||
|
||||
IF OLD."protokollDateiId" IS NULL THEN
|
||||
vergleich."protokollDateiId" := OLD."protokollDateiId";
|
||||
END IF;
|
||||
|
||||
IF OLD."protokollNummer" IS NULL THEN
|
||||
vergleich."protokollNummer" := OLD."protokollNummer";
|
||||
END IF;
|
||||
|
||||
IF vergleich IS DISTINCT FROM OLD THEN
|
||||
RAISE EXCEPTION
|
||||
'Die Pruefung % ist abgeschlossen und damit unveraenderlich. '
|
||||
'Eine Korrektur wird als neue Pruefung mit Verweis auf diese erfasst.',
|
||||
OLD."id"
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION pruefung_unveraenderlich() IS
|
||||
'Sperrt Aenderungen an abgeschlossenen Pruefungen, bis auf Ersetzung und Nachtrag des Protokolls.';
|
||||
|
||||
-- Gelöscht wird ein Protokoll gar nicht — auch nicht weich. Die einzige
|
||||
-- Ausnahme ist der Aufbewahrungslauf, derselbe Schalter wie beim Audit-Log.
|
||||
CREATE OR REPLACE FUNCTION pruefung_kein_loeschen()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF OLD."status" = 'ENTWURF' THEN
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
|
||||
IF coalesce(current_setting('app.audit_bereinigung', true), '') = 'an' THEN
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
|
||||
RAISE EXCEPTION
|
||||
'Die Pruefung % ist abgeschlossen und darf nicht geloescht werden '
|
||||
'(nur im Aufbewahrungslauf).',
|
||||
OLD."id"
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER pruefung_abschluss_schuetzen
|
||||
BEFORE UPDATE ON "pruefung"
|
||||
FOR EACH ROW EXECUTE FUNCTION pruefung_unveraenderlich();
|
||||
|
||||
CREATE TRIGGER pruefung_loeschschutz
|
||||
BEFORE DELETE ON "pruefung"
|
||||
FOR EACH ROW EXECUTE FUNCTION pruefung_kein_loeschen();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Dasselbe für die Bestandteile eines Protokolls.
|
||||
--
|
||||
-- Ohne diese Sperre ließe sich ein festgeschriebenes Protokoll über den Umweg
|
||||
-- der Einzelergebnisse verändern: die Prüfung bliebe unangetastet, ihr Inhalt
|
||||
-- nicht. Beim Löschen der Prüfung selbst greift die Kaskade — dann gibt es die
|
||||
-- Prüfung schon nicht mehr, und die Abfrage unten liefert NULL.
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION pruefungsinhalt_gesperrt()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
betroffene_pruefung uuid;
|
||||
zustand "PruefungStatus";
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
betroffene_pruefung := OLD."pruefungId";
|
||||
ELSE
|
||||
betroffene_pruefung := NEW."pruefungId";
|
||||
END IF;
|
||||
|
||||
SELECT "status" INTO zustand FROM "pruefung" WHERE "id" = betroffene_pruefung;
|
||||
|
||||
-- Prüfung existiert nicht mehr (Kaskade) oder ist noch Entwurf: erlaubt.
|
||||
IF zustand IS NULL OR zustand = 'ENTWURF' THEN
|
||||
IF TG_OP = 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF;
|
||||
END IF;
|
||||
|
||||
IF coalesce(current_setting('app.audit_bereinigung', true), '') = 'an' THEN
|
||||
IF TG_OP = 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF;
|
||||
END IF;
|
||||
|
||||
RAISE EXCEPTION
|
||||
'Die Pruefung % ist abgeschlossen. % auf % ist nicht mehr zulaessig.',
|
||||
betroffene_pruefung, TG_OP, TG_TABLE_NAME
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER pruefergebnis_gesperrt
|
||||
BEFORE INSERT OR UPDATE OR DELETE ON "pruefergebnis"
|
||||
FOR EACH ROW EXECUTE FUNCTION pruefungsinhalt_gesperrt();
|
||||
|
||||
CREATE TRIGGER signatur_gesperrt
|
||||
BEFORE INSERT OR UPDATE OR DELETE ON "signatur"
|
||||
FOR EACH ROW EXECUTE FUNCTION pruefungsinhalt_gesperrt();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Ein Messwert außerhalb seines Bereichs ist kein Messwert, sondern ein Fehler
|
||||
-- in der Vorlage: die Untergrenze darf nicht über der Obergrenze liegen.
|
||||
-- -----------------------------------------------------------------------------
|
||||
ALTER TABLE "checklistenpunkt" ADD CONSTRAINT "checklistenpunkt_bereich_sinnvoll" CHECK (
|
||||
"minWert" IS NULL OR "maxWert" IS NULL OR "minWert" <= "maxWert"
|
||||
);
|
||||
|
||||
-- Ein Intervall von null Monaten würde im nächtlichen Lauf endlos Termine
|
||||
-- erzeugen.
|
||||
ALTER TABLE "wartungsvorlage" ADD CONSTRAINT "wartungsvorlage_intervall_positiv" CHECK (
|
||||
"intervallMonate" > 0 AND "vorwarnzeitTage" >= 0
|
||||
);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Indizes
|
||||
--
|
||||
-- Das Fristen-Dashboard fragt immer dasselbe: offene Termine, sortiert nach
|
||||
-- Fälligkeit. Ein Teilindex darauf ist klein und bleibt es auch, wenn die
|
||||
-- Tabelle nach Jahren zehntausende erledigte Termine enthält.
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE INDEX "wartungstermin_offen" ON "wartungstermin" ("organisationId", "faelligAm")
|
||||
WHERE "status" = 'OFFEN' AND "geloeschtAm" IS NULL;
|
||||
|
||||
CREATE INDEX "mangel_offen" ON "mangel" ("organisationId", "prioritaet", "gemeldetAm")
|
||||
WHERE "status" <> 'BEHOBEN' AND "status" <> 'VERWORFEN' AND "geloeschtAm" IS NULL;
|
||||
|
||||
-- Volltextsuche wie in Phase 1: pg_trgm macht ILIKE '%text%' indexfähig.
|
||||
CREATE INDEX "mangel_suche_beschreibung" ON "mangel" USING gin ("beschreibung" gin_trgm_ops);
|
||||
CREATE INDEX "rueckruf_suche_titel" ON "rueckruf" USING gin ("titel" gin_trgm_ops);
|
||||
CREATE INDEX "rueckruf_suche_hersteller" ON "rueckruf" USING gin ("hersteller" gin_trgm_ops);
|
||||
CREATE INDEX "wartungsvorlage_suche_name" ON "wartungsvorlage" USING gin ("name" gin_trgm_ops);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Rechte für die Anwendungsrolle auf die neuen Tabellen.
|
||||
-- -----------------------------------------------------------------------------
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'florian_app') THEN
|
||||
EXECUTE 'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO florian_app';
|
||||
EXECUTE 'GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO florian_app';
|
||||
|
||||
-- Die Einschraenkungen aus Phase 0 gelten unveraendert weiter.
|
||||
EXECUTE 'REVOKE INSERT, UPDATE, DELETE ON "berechtigung" FROM florian_app';
|
||||
EXECUTE 'REVOKE UPDATE, DELETE ON "audit_log" FROM florian_app';
|
||||
|
||||
-- DELETE auf "pruefung" bleibt erlaubt: ein abgebrochener Entwurf soll
|
||||
-- verschwinden koennen. Abgeschlossene Protokolle schuetzt der Trigger
|
||||
-- pruefung_loeschschutz, und der gilt fuer jede Rolle.
|
||||
ELSE
|
||||
RAISE NOTICE 'Rolle florian_app nicht vorhanden — Rechtevergabe uebersprungen.';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
+644
-67
@@ -145,27 +145,40 @@ model Organisation {
|
||||
/// Funkrufname-Präfix der Wehr, z. B. "Florian Musterhausen".
|
||||
funkPraefix String? @db.VarChar(80)
|
||||
|
||||
zeitzone String @default("Europe/Berlin") @db.VarChar(60)
|
||||
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[]
|
||||
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[]
|
||||
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[]
|
||||
|
||||
@@index([aktiv])
|
||||
@@map("organisation")
|
||||
@@ -182,16 +195,16 @@ model Person {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
vorname String @db.VarChar(100)
|
||||
nachname String @db.VarChar(100)
|
||||
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)
|
||||
mitgliedsnummer String? @db.VarChar(30)
|
||||
geburtsdatum DateTime? @db.Date
|
||||
dienstgrad Dienstgrad @default(KEIN)
|
||||
|
||||
email String? @db.VarChar(200)
|
||||
email String? @db.VarChar(200)
|
||||
telefon String? @db.VarChar(40)
|
||||
mobil String? @db.VarChar(40)
|
||||
mobil String? @db.VarChar(40)
|
||||
|
||||
strasse String? @db.VarChar(200)
|
||||
plz String? @db.VarChar(10)
|
||||
@@ -222,16 +235,24 @@ model Person {
|
||||
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")
|
||||
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")
|
||||
|
||||
/// E-Mail und OIDC-Subject sind je Wehr eindeutig, nicht global — dieselbe
|
||||
/// Person kann in zwei Wehren Mitglied sein.
|
||||
@@unique([organisationId, email])
|
||||
@@ -249,11 +270,11 @@ 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)
|
||||
schluessel RolleSchluessel
|
||||
name String @db.VarChar(100)
|
||||
beschreibung String? @db.VarChar(500)
|
||||
/// Systemrollen dürfen nicht gelöscht werden.
|
||||
systemRolle Boolean @default(false)
|
||||
systemRolle Boolean @default(false)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
@@ -275,14 +296,14 @@ model PersonRolle {
|
||||
personId String @db.Uuid
|
||||
rolleId String @db.Uuid
|
||||
|
||||
zugewiesenAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
zugewiesenVonId 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)
|
||||
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)
|
||||
ausOidcGruppe Boolean @default(false)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
person Person @relation("PersonRollenInhaber", fields: [personId], references: [id], onDelete: Cascade)
|
||||
@@ -304,11 +325,11 @@ model PersonRolle {
|
||||
/// 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)
|
||||
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)
|
||||
bereich String @db.VarChar(60)
|
||||
bezeichnung String @db.VarChar(160)
|
||||
beschreibung String? @db.VarChar(500)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
@@ -350,17 +371,17 @@ model AusweisToken {
|
||||
|
||||
typ AusweisTokenTyp
|
||||
/// bcrypt-Hash der PIN bzw. der RFID-UID.
|
||||
tokenHash String @db.VarChar(120)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
@@ -387,19 +408,19 @@ model AuditLog {
|
||||
|
||||
zeitpunkt DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
akteurTyp AkteurTyp @default(PERSON)
|
||||
akteurPersonId String? @db.Uuid
|
||||
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)
|
||||
akteurName String? @db.VarChar(200)
|
||||
anmeldeArt AnmeldeArt?
|
||||
|
||||
aktion AuditAktion
|
||||
aktion AuditAktion
|
||||
/// Modellname, z. B. "Geraet".
|
||||
entitaet String @db.VarChar(80)
|
||||
entitaetId String? @db.Uuid
|
||||
entitaet String @db.VarChar(80)
|
||||
entitaetId String? @db.Uuid
|
||||
/// Menschenlesbare Kurzfassung für die Oberfläche.
|
||||
beschreibung String? @db.VarChar(500)
|
||||
beschreibung String? @db.VarChar(500)
|
||||
|
||||
vorher Json? @db.JsonB
|
||||
nachher Json? @db.JsonB
|
||||
@@ -428,15 +449,15 @@ model Datei {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
bucket String @db.VarChar(80)
|
||||
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)
|
||||
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)
|
||||
pruefsumme String? @db.VarChar(64)
|
||||
|
||||
/// Woran die Datei hängt (z. B. "Geraet") und an welchem Datensatz.
|
||||
entitaet String? @db.VarChar(80)
|
||||
@@ -444,15 +465,22 @@ model Datei {
|
||||
/// 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)
|
||||
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")
|
||||
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")
|
||||
|
||||
@@unique([bucket, objektSchluessel])
|
||||
@@index([organisationId, entitaet, entitaetId])
|
||||
@@ -688,6 +716,10 @@ model Geraetetyp {
|
||||
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])
|
||||
@@ -746,6 +778,12 @@ model Geraet {
|
||||
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[]
|
||||
|
||||
@@unique([organisationId, inventarnummer])
|
||||
@@index([organisationId, status])
|
||||
@@index([organisationId, naechstePruefung])
|
||||
@@ -826,3 +864,542 @@ model Qualifikation {
|
||||
@@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")
|
||||
}
|
||||
|
||||
+427
-1
@@ -5,7 +5,9 @@
|
||||
* Terminal-Ausweisen, 3 Standorte, 3 Fahrzeuge samt Geräteräumen,
|
||||
* 14 Gerätetypen mit echten Prüfvorgaben, 45 Geräte mit QR-fähigen
|
||||
* Inventarnummern, 14 Qualifikationstypen und die Qualifikationen der
|
||||
* Mannschaft.
|
||||
* Mannschaft. Dazu ab Phase 2: 5 Wartungsvorlagen mit Checklisten, die daraus
|
||||
* fälligen Prüftermine, ein abgeschlossenes Protokoll samt Mangel und ein
|
||||
* Beispielrückruf.
|
||||
*
|
||||
* Das Skript läuft über DIRECT_DATABASE_URL, also mit dem Eigentümer-Zugang,
|
||||
* der Row Level Security umgehen darf. Anders ginge es nicht: der Seed legt
|
||||
@@ -19,15 +21,22 @@
|
||||
import "dotenv/config";
|
||||
import {
|
||||
AusweisTokenTyp,
|
||||
ChecklistenpunktTyp,
|
||||
Dienstgrad,
|
||||
FahrzeugArt,
|
||||
GeraeteKategorie,
|
||||
GeraetStatus,
|
||||
LebensdauerBezug,
|
||||
LebensdauerKategorie,
|
||||
MangelPrioritaet,
|
||||
MangelStatus,
|
||||
PrismaClient,
|
||||
PruefErgebnis,
|
||||
PruefungArt,
|
||||
PruefungStatus,
|
||||
QualifikationsArt,
|
||||
RolleSchluessel,
|
||||
SignaturRolle,
|
||||
StandortArt,
|
||||
} from "@prisma/client";
|
||||
import bcrypt from "bcryptjs";
|
||||
@@ -1092,6 +1101,417 @@ async function qualifikationenAnlegen(organisationId: string, typen: Map<string,
|
||||
log(`Qualifikationen: ${angelegt}`);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Phase 2 — Wartungsvorlagen, Termine, ein Beispielprotokoll
|
||||
//
|
||||
// Die Vorlagen sind bewusst echt: Intervalle und Vorschriften stammen aus den
|
||||
// gängigen Regelwerken. Eine Beispielwehr, in der jede Prüfung "Prüfung 1"
|
||||
// heißt, zeigt nicht, worum es geht.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
interface PunktVorlage {
|
||||
text: string;
|
||||
typ: ChecklistenpunktTyp;
|
||||
pflicht?: boolean;
|
||||
sollwert?: string;
|
||||
einheit?: string;
|
||||
minWert?: number;
|
||||
maxWert?: number;
|
||||
hinweis?: string;
|
||||
}
|
||||
|
||||
interface WartungsVorlage {
|
||||
name: string;
|
||||
geraetetyp: string;
|
||||
intervallMonate: number;
|
||||
vorwarnzeitTage?: number;
|
||||
pruefvorschrift?: string;
|
||||
standardDauerMinuten?: number;
|
||||
punkte: PunktVorlage[];
|
||||
}
|
||||
|
||||
const WARTUNGSVORLAGEN: WartungsVorlage[] = [
|
||||
{
|
||||
name: "Pressluftatmer — Jahresprüfung",
|
||||
geraetetyp: "Pressluftatmer PA 94",
|
||||
intervallMonate: 12,
|
||||
pruefvorschrift: "DGUV Regel 112-190, vfdb 0804",
|
||||
standardDauerMinuten: 25,
|
||||
punkte: [
|
||||
{ text: "Trageeinrichtung auf Beschädigung und Vollständigkeit prüfen", typ: ChecklistenpunktTyp.JA_NEIN },
|
||||
{ text: "Bänderung auf Risse, Schnitte und Verhärtung prüfen", typ: ChecklistenpunktTyp.JA_NEIN },
|
||||
{
|
||||
text: "Hochdruckdichtheit: Druckabfall in einer Minute",
|
||||
typ: ChecklistenpunktTyp.MESSWERT,
|
||||
sollwert: "höchstens 10 bar in einer Minute",
|
||||
einheit: "bar",
|
||||
minWert: 0,
|
||||
maxWert: 10,
|
||||
},
|
||||
{
|
||||
text: "Ansprechdruck der Warneinrichtung",
|
||||
typ: ChecklistenpunktTyp.MESSWERT,
|
||||
sollwert: "50 bis 60 bar",
|
||||
einheit: "bar",
|
||||
minWert: 50,
|
||||
maxWert: 60,
|
||||
},
|
||||
{
|
||||
text: "Lungenautomat: Öffnungsdruck",
|
||||
typ: ChecklistenpunktTyp.MESSWERT,
|
||||
sollwert: "bis 3,5 mbar",
|
||||
einheit: "mbar",
|
||||
maxWert: 3.5,
|
||||
},
|
||||
{ text: "Auffälligkeiten", typ: ChecklistenpunktTyp.TEXT, pflicht: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Atemanschluss — Halbjahresprüfung",
|
||||
geraetetyp: "Atemschutzmaske FPS 7000",
|
||||
intervallMonate: 6,
|
||||
pruefvorschrift: "DGUV Regel 112-190",
|
||||
standardDauerMinuten: 15,
|
||||
punkte: [
|
||||
{ text: "Maskenkörper auf Risse, Verformung und Verhärtung prüfen", typ: ChecklistenpunktTyp.JA_NEIN },
|
||||
{ text: "Sichtscheibe frei von Kratzern im Sichtfeld", typ: ChecklistenpunktTyp.JA_NEIN },
|
||||
{ text: "Ausatemventil auf Dichtheit und Sitz prüfen", typ: ChecklistenpunktTyp.JA_NEIN },
|
||||
{
|
||||
text: "Dichtprüfung: Druckabfall in einer Minute",
|
||||
typ: ChecklistenpunktTyp.MESSWERT,
|
||||
sollwert: "höchstens 0,5 mbar",
|
||||
einheit: "mbar",
|
||||
minWert: 0,
|
||||
maxWert: 0.5,
|
||||
},
|
||||
{ text: "Foto der Maske nach der Reinigung", typ: ChecklistenpunktTyp.FOTO, pflicht: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Feuerwehrleine — Jahresprüfung",
|
||||
geraetetyp: "Feuerwehrleine 30 m",
|
||||
intervallMonate: 12,
|
||||
pruefvorschrift: "DGUV Information 205-014, DIN 14920",
|
||||
standardDauerMinuten: 10,
|
||||
punkte: [
|
||||
{ text: "Leine über die gesamte Länge durch die Hand ziehen, Beschädigungen prüfen", typ: ChecklistenpunktTyp.JA_NEIN },
|
||||
{ text: "Kennzeichnung und Herstellungsjahr lesbar", typ: ChecklistenpunktTyp.JA_NEIN },
|
||||
{ text: "Leinenbeutel unbeschädigt und trocken", typ: ChecklistenpunktTyp.JA_NEIN, pflicht: false },
|
||||
{ text: "Befund", typ: ChecklistenpunktTyp.TEXT, pflicht: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Druckschlauch — Druckprüfung",
|
||||
geraetetyp: "B-Druckschlauch 20 m",
|
||||
intervallMonate: 12,
|
||||
pruefvorschrift: "DIN 14811",
|
||||
standardDauerMinuten: 8,
|
||||
punkte: [
|
||||
{
|
||||
text: "Prüfdruck halten",
|
||||
typ: ChecklistenpunktTyp.MESSWERT,
|
||||
sollwert: "mindestens 8 bar über 5 Minuten",
|
||||
einheit: "bar",
|
||||
minWert: 8,
|
||||
},
|
||||
{ text: "Kupplungen fest, Dichtringe vorhanden", typ: ChecklistenpunktTyp.JA_NEIN },
|
||||
{ text: "Schlauchgewebe ohne Scheuerstellen und Löcher", typ: ChecklistenpunktTyp.JA_NEIN },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Elektrische Betriebsmittel — DGUV-V3-Prüfung",
|
||||
geraetetyp: "Stromerzeuger 8 kVA",
|
||||
intervallMonate: 12,
|
||||
pruefvorschrift: "DGUV Vorschrift 3, DIN VDE 0701-0702",
|
||||
standardDauerMinuten: 30,
|
||||
punkte: [
|
||||
{ text: "Sichtprüfung Gehäuse, Leitungen und Steckvorrichtungen", typ: ChecklistenpunktTyp.JA_NEIN },
|
||||
{
|
||||
text: "Schutzleiterwiderstand",
|
||||
typ: ChecklistenpunktTyp.MESSWERT,
|
||||
sollwert: "höchstens 0,3 Ohm",
|
||||
einheit: "Ohm",
|
||||
minWert: 0,
|
||||
maxWert: 0.3,
|
||||
},
|
||||
{
|
||||
text: "Isolationswiderstand",
|
||||
typ: ChecklistenpunktTyp.MESSWERT,
|
||||
sollwert: "mindestens 1 Megaohm",
|
||||
einheit: "MOhm",
|
||||
minWert: 1,
|
||||
},
|
||||
{ text: "Funktionsprüfung unter Last", typ: ChecklistenpunktTyp.JA_NEIN },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
async function wartungsvorlagenAnlegen(organisationId: string, typen: Map<string, string>) {
|
||||
const karte = new Map<string, { id: string; intervallMonate: number; punkte: string[] }>();
|
||||
|
||||
for (const vorlage of WARTUNGSVORLAGEN) {
|
||||
const geraetetypId = typen.get(vorlage.geraetetyp) ?? null;
|
||||
|
||||
const angelegt = await prisma.wartungsvorlage.upsert({
|
||||
where: { organisationId_name: { organisationId, name: vorlage.name } },
|
||||
update: {},
|
||||
create: {
|
||||
organisationId,
|
||||
name: vorlage.name,
|
||||
geraetetypId,
|
||||
intervallMonate: vorlage.intervallMonate,
|
||||
vorwarnzeitTage: vorlage.vorwarnzeitTage ?? 30,
|
||||
pruefvorschrift: vorlage.pruefvorschrift ?? null,
|
||||
standardDauerMinuten: vorlage.standardDauerMinuten ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
const punktIds: string[] = [];
|
||||
const vorhandene = await prisma.checklistenpunkt.count({ where: { vorlageId: angelegt.id } });
|
||||
|
||||
if (vorhandene === 0) {
|
||||
for (const [index, punkt] of vorlage.punkte.entries()) {
|
||||
const erzeugt = await prisma.checklistenpunkt.create({
|
||||
data: {
|
||||
organisationId,
|
||||
vorlageId: angelegt.id,
|
||||
text: punkt.text,
|
||||
typ: punkt.typ,
|
||||
pflicht: punkt.pflicht ?? true,
|
||||
sollwert: punkt.sollwert ?? null,
|
||||
einheit: punkt.einheit ?? null,
|
||||
minWert: punkt.minWert ?? null,
|
||||
maxWert: punkt.maxWert ?? null,
|
||||
hinweis: punkt.hinweis ?? null,
|
||||
sortierung: (index + 1) * 10,
|
||||
},
|
||||
});
|
||||
punktIds.push(erzeugt.id);
|
||||
}
|
||||
} else {
|
||||
const bestand = await prisma.checklistenpunkt.findMany({
|
||||
where: { vorlageId: angelegt.id },
|
||||
orderBy: { sortierung: "asc" },
|
||||
select: { id: true },
|
||||
});
|
||||
punktIds.push(...bestand.map((punkt) => punkt.id));
|
||||
}
|
||||
|
||||
karte.set(vorlage.name, {
|
||||
id: angelegt.id,
|
||||
intervallMonate: vorlage.intervallMonate,
|
||||
punkte: punktIds,
|
||||
});
|
||||
}
|
||||
|
||||
log(`Wartungsvorlagen: ${karte.size}`);
|
||||
return karte;
|
||||
}
|
||||
|
||||
/**
|
||||
* Termine für alle Geräte der jeweiligen Bauart.
|
||||
*
|
||||
* Das ist dieselbe Rechnung, die der nächtliche Lauf anstellt — hier nur ohne
|
||||
* Worker, damit eine frisch aufgesetzte Wehr sofort ein gefülltes
|
||||
* Fristen-Dashboard hat.
|
||||
*/
|
||||
async function wartungstermineAnlegen(
|
||||
organisationId: string,
|
||||
vorlagen: Map<string, { id: string; intervallMonate: number; punkte: string[] }>,
|
||||
) {
|
||||
let angelegt = 0;
|
||||
|
||||
for (const vorlage of WARTUNGSVORLAGEN) {
|
||||
const eintrag = vorlagen.get(vorlage.name);
|
||||
if (!eintrag) continue;
|
||||
|
||||
const geraete = await prisma.geraet.findMany({
|
||||
where: {
|
||||
organisationId,
|
||||
geloeschtAm: null,
|
||||
geraetetyp: { name: vorlage.geraetetyp },
|
||||
},
|
||||
select: { id: true, letztePruefung: true, indienststellung: true },
|
||||
});
|
||||
|
||||
for (const geraet of geraete) {
|
||||
const basis = geraet.letztePruefung ?? geraet.indienststellung;
|
||||
if (!basis) continue;
|
||||
|
||||
const faelligAm = plusMonate(basis, vorlage.intervallMonate);
|
||||
|
||||
const ergebnis = await prisma.wartungstermin.createMany({
|
||||
data: [
|
||||
{
|
||||
organisationId,
|
||||
geraetId: geraet.id,
|
||||
vorlageId: eintrag.id,
|
||||
faelligAm,
|
||||
automatischErzeugt: true,
|
||||
},
|
||||
],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
angelegt += ergebnis.count;
|
||||
}
|
||||
}
|
||||
|
||||
log(`Wartungstermine: ${angelegt}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein abgeschlossenes Protokoll samt Mangel.
|
||||
*
|
||||
* Ohne ein Beispiel bleibt die Protokollansicht leer, und niemand sieht, wie
|
||||
* ein fertiger Nachweis aussieht. Das PDF fehlt bewusst: es entsteht im
|
||||
* Worker, sobald einer läuft.
|
||||
*/
|
||||
async function beispielPruefungAnlegen(
|
||||
organisationId: string,
|
||||
vorlagen: Map<string, { id: string; intervallMonate: number; punkte: string[] }>,
|
||||
) {
|
||||
const vorlage = vorlagen.get("Feuerwehrleine — Jahresprüfung");
|
||||
if (!vorlage) return;
|
||||
|
||||
const vorhanden = await prisma.pruefung.count({ where: { organisationId } });
|
||||
if (vorhanden > 0) {
|
||||
log("Beispielprüfung: bereits vorhanden");
|
||||
return;
|
||||
}
|
||||
|
||||
const geraet = await prisma.geraet.findFirst({
|
||||
where: { organisationId, geraetetyp: { name: "Feuerwehrleine 30 m" }, geloeschtAm: null },
|
||||
select: { id: true, inventarnummer: true },
|
||||
});
|
||||
if (!geraet) return;
|
||||
|
||||
const pruefer = await prisma.person.findFirst({
|
||||
where: {
|
||||
organisationId,
|
||||
rollen: { some: { rolle: { schluessel: RolleSchluessel.GERAETEWART } } },
|
||||
},
|
||||
select: { id: true, vorname: true, nachname: true },
|
||||
});
|
||||
|
||||
const geprueftAm = plusMonate(new Date(), -2);
|
||||
const prueferName = pruefer ? `${pruefer.vorname} ${pruefer.nachname}` : "Gerätewart";
|
||||
|
||||
const punkte = await prisma.checklistenpunkt.findMany({
|
||||
where: { vorlageId: vorlage.id },
|
||||
orderBy: { sortierung: "asc" },
|
||||
});
|
||||
|
||||
// Erst als Entwurf anlegen, dann abschließen — genau wie im Wizard. Der
|
||||
// Trigger aus Migration 20260903000300 lässt an einer bereits
|
||||
// abgeschlossenen Prüfung keine Ergebnisse und Unterschriften mehr zu, und
|
||||
// das ist auch für den Seed richtig so.
|
||||
const pruefung = await prisma.pruefung.create({
|
||||
data: {
|
||||
organisationId,
|
||||
geraetId: geraet.id,
|
||||
vorlageId: vorlage.id,
|
||||
art: PruefungArt.INTERN,
|
||||
status: PruefungStatus.ENTWURF,
|
||||
ergebnis: PruefErgebnis.BESTANDEN_MIT_MAENGELN,
|
||||
prueferId: pruefer?.id ?? null,
|
||||
prueferName,
|
||||
durchgefuehrtAm: geprueftAm,
|
||||
begonnenUm: geprueftAm,
|
||||
dauerMinuten: 12,
|
||||
naechstePruefungAm: plusMonate(geprueftAm, vorlage.intervallMonate),
|
||||
protokollNummer: `${geprueftAm.getUTCFullYear()}-0001`,
|
||||
abgeschlossenAm: geprueftAm,
|
||||
bemerkung:
|
||||
"Leine vollständig durch die Hand gezogen. Leinenbeutel an einer Naht aufgescheuert.",
|
||||
ergebnisse: {
|
||||
create: punkte.map((punkt, index) => ({
|
||||
organisationId,
|
||||
punktId: punkt.id,
|
||||
jaNein: punkt.typ === ChecklistenpunktTyp.JA_NEIN ? index !== 2 : null,
|
||||
text:
|
||||
punkt.typ === ChecklistenpunktTyp.TEXT
|
||||
? "Leine selbst ohne Befund, Beutel beschädigt."
|
||||
: null,
|
||||
inOrdnung: index !== 2,
|
||||
bemerkung: index === 2 ? "Naht am Beutel aufgescheuert" : null,
|
||||
})),
|
||||
},
|
||||
signaturen: {
|
||||
create: [
|
||||
{
|
||||
organisationId,
|
||||
rolle: SignaturRolle.PRUEFER,
|
||||
personId: pruefer?.id ?? null,
|
||||
name: prueferName,
|
||||
unterzeichnetAm: geprueftAm,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.pruefung.update({
|
||||
where: { id: pruefung.id },
|
||||
data: { status: PruefungStatus.ABGESCHLOSSEN },
|
||||
});
|
||||
|
||||
await prisma.mangel.create({
|
||||
data: {
|
||||
organisationId,
|
||||
geraetId: geraet.id,
|
||||
pruefungId: pruefung.id,
|
||||
melderId: pruefer?.id ?? null,
|
||||
melderName: prueferName,
|
||||
beschreibung: "Leinenbeutel an einer Naht aufgescheuert — Beutel tauschen.",
|
||||
prioritaet: MangelPrioritaet.NIEDRIG,
|
||||
status: MangelStatus.ZUGEWIESEN,
|
||||
zugewiesenAnId: pruefer?.id ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
log(`Beispielprüfung: Protokoll ${pruefung.protokollNummer} an ${geraet.inventarnummer}`);
|
||||
}
|
||||
|
||||
/** Ein Rückruf, damit die Ansicht nicht leer ist. */
|
||||
async function rueckrufAnlegen(organisationId: string) {
|
||||
const vorhanden = await prisma.rueckruf.count({ where: { organisationId } });
|
||||
if (vorhanden > 0) return;
|
||||
|
||||
const typ = await prisma.geraetetyp.findFirst({
|
||||
where: { organisationId, name: "Atemschutzmaske FPS 7000" },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
await prisma.rueckruf.create({
|
||||
data: {
|
||||
organisationId,
|
||||
hersteller: "Dräger",
|
||||
titel: "Sicherheitshinweis Ausatemventil Baureihe FPS 7000",
|
||||
referenz: "SI-2026-03",
|
||||
beschreibung:
|
||||
"Bei einzelnen Chargen kann die Ventilscheibe des Ausatemventils verkleben. "
|
||||
+ "Betroffen sind Masken mit Herstellung vor 2020.",
|
||||
massnahme: "Ventilscheibe tauschen, danach Dichtprüfung wiederholen.",
|
||||
veroeffentlichtAm: plusMonate(new Date(), -1),
|
||||
sperrtGeraete: false,
|
||||
typen: typ
|
||||
? {
|
||||
create: [
|
||||
{
|
||||
organisationId,
|
||||
geraetetypId: typ.id,
|
||||
herstellungBis: datum(2019, 12, 31),
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
log("Rückruf: 1 Beispiel");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Ablauf
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -1113,6 +1533,12 @@ async function main(): Promise<void> {
|
||||
const qualiTypen = await qualifikationstypenAnlegen(organisation.id);
|
||||
await qualifikationenAnlegen(organisation.id, qualiTypen);
|
||||
|
||||
// Phase 2 — Wartung und Prüfung
|
||||
const wartungsvorlagen = await wartungsvorlagenAnlegen(organisation.id, typen);
|
||||
await wartungstermineAnlegen(organisation.id, wartungsvorlagen);
|
||||
await beispielPruefungAnlegen(organisation.id, wartungsvorlagen);
|
||||
await rueckrufAnlegen(organisation.id);
|
||||
|
||||
log("Fertig.");
|
||||
log("");
|
||||
log("Anmeldung für den Erststart:");
|
||||
|
||||
Reference in New Issue
Block a user