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"
|
||||
Reference in New Issue
Block a user