Phase 3: Einsatz- und Uebungserfassung
Schnellerfassung im Alarmfall, sechs Zeitstempel mit Ausrueckzeit, Anfahrt und Hilfsfrist, Staerke nach FwDV 3, Personalvorschlag mit Qualifikationsstand, Einsatzbericht als PDF, Jahresexport fuer die Gemeinde, Statistik und Uebungen mit Anwesenheitsliste. Dazu das Einsatzportal fuer Fremdwehren: ein signierter Link ohne Anmeldung, zeitlich und mengenbegrenzt, der Stichwort, Ort, Zeiten und Lagemeldungen zeigt und Meldungen mit Foto annimmt. Es sind die ersten oeffentlich erreichbaren Endpunkte in FLORIAN. Acht neue Tabellen, alle mit organisationId und Row Level Security. Ein freigegebener Einsatzbericht ist unveraenderlich; ein Datenbanktrigger setzt das durch. Koordinaten liegen doppelt — als Dezimalgrad und als PostGIS-Punkt, zusammengehalten von einem Trigger. 57 neue Tests (32 Unit, 14 gegen eine echte Datenbank, 11 E2E). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bc62a9c1e8
commit
91954c402b
@@ -0,0 +1,347 @@
|
||||
-- =============================================================================
|
||||
-- Phase 3: Tabellen für Einsätze und Übungen.
|
||||
--
|
||||
-- Erzeugt mit `prisma migrate dev --create-only` und wie in Phase 2 an einer
|
||||
-- Stelle bereinigt: die DROP INDEX-Zeilen für die Volltext- und Geoindizes
|
||||
-- sind entfernt. Prisma kennt gin_trgm_ops und gist nicht und würde sie bei
|
||||
-- jeder Migration erneut löschen wollen.
|
||||
--
|
||||
-- Mandantentrennung, Koordinaten-Trigger, Unveränderbarkeit freigegebener
|
||||
-- Berichte und die Suchindizes stehen in der nachfolgenden Migration.
|
||||
-- =============================================================================
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EinsatzKategorie" AS ENUM ('BRAND_KLEIN', 'BRAND_MITTEL', 'BRAND_GROSS', 'TECHNISCHE_HILFE_KLEIN', 'TECHNISCHE_HILFE_MITTEL', 'TECHNISCHE_HILFE_GROSS', 'GEFAHRGUT', 'RETTUNGSDIENST', 'BRANDMELDEANLAGE', 'BOESWILLIGER_ALARM', 'SICHERHEITSWACHE', 'UNTERSTUETZUNG', 'SONSTIGES');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EinsatzStatus" AS ENUM ('ENTWURF', 'LAUFEND', 'ABGESCHLOSSEN', 'FREIGEGEBEN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Alarmierungsart" AS ENUM ('SIRENE', 'MELDEEMPFAENGER', 'TELEFON', 'APP', 'PERSOENLICH', 'SONSTIGES');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EinsatzFunktion" AS ENUM ('EINSATZLEITER', 'ZUGFUEHRER', 'GRUPPENFUEHRER', 'STAFFELFUEHRER', 'TRUPPFUEHRER', 'MASCHINIST', 'MELDER', 'TRUPPMANN', 'ATEMSCHUTZTRAEGER', 'SICHERHEITSTRUPP', 'SANITAETER', 'SONSTIGES');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "UebungsArt" AS ENUM ('PRAXIS', 'THEORIE', 'ATEMSCHUTZ', 'MASCHINIST', 'FUNK', 'ERSTE_HILFE', 'GEMEINSCHAFTSUEBUNG', 'SONSTIGES');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "LagemeldungQuelle" AS ENUM ('INTERN', 'PORTAL');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "einsatz" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"einsatznummer" VARCHAR(40) NOT NULL,
|
||||
"stichwort" VARCHAR(160) NOT NULL,
|
||||
"kategorie" "EinsatzKategorie" NOT NULL DEFAULT 'SONSTIGES',
|
||||
"status" "EinsatzStatus" NOT NULL DEFAULT 'ENTWURF',
|
||||
"ilsNummer" VARCHAR(60),
|
||||
"alarmierungsart" "Alarmierungsart",
|
||||
"strasse" VARCHAR(200),
|
||||
"hausnummer" VARCHAR(20),
|
||||
"plz" VARCHAR(10),
|
||||
"ort" VARCHAR(120),
|
||||
"ortsteil" VARCHAR(120),
|
||||
"objekt" VARCHAR(200),
|
||||
"breitengrad" DECIMAL(9,6),
|
||||
"laengengrad" DECIMAL(9,6),
|
||||
"koordinaten" geography(Point, 4326),
|
||||
"koordinatenQuelle" VARCHAR(40),
|
||||
"alarmiertUm" TIMESTAMPTZ(3),
|
||||
"ausgeruecktUm" TIMESTAMPTZ(3),
|
||||
"eingetroffenUm" TIMESTAMPTZ(3),
|
||||
"einsatzendeUm" TIMESTAMPTZ(3),
|
||||
"eingeruecktUm" TIMESTAMPTZ(3),
|
||||
"einsatzbereitUm" TIMESTAMPTZ(3),
|
||||
"einsatzleiterId" UUID,
|
||||
"beschreibung" VARCHAR(4000),
|
||||
"bericht" TEXT,
|
||||
"wetter" VARCHAR(200),
|
||||
"temperatur" INTEGER,
|
||||
"weitereKraefte" VARCHAR(500),
|
||||
"geretteteePersonen" INTEGER NOT NULL DEFAULT 0,
|
||||
"verletztePersonen" INTEGER NOT NULL DEFAULT 0,
|
||||
"totePersonen" INTEGER NOT NULL DEFAULT 0,
|
||||
"sachschaden" DECIMAL(12,2),
|
||||
"kostenpflichtig" BOOLEAN NOT NULL DEFAULT false,
|
||||
"berichtDateiId" UUID,
|
||||
"freigegebenAm" TIMESTAMPTZ(3),
|
||||
"freigegebenVonId" UUID,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
"geloeschtAm" TIMESTAMPTZ(3),
|
||||
|
||||
CONSTRAINT "einsatz_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "einsatzfahrzeug" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"einsatzId" UUID NOT NULL,
|
||||
"fahrzeugId" UUID NOT NULL,
|
||||
"ausgeruecktUm" TIMESTAMPTZ(3),
|
||||
"eingetroffenUm" TIMESTAMPTZ(3),
|
||||
"eingeruecktUm" TIMESTAMPTZ(3),
|
||||
"kilometerAbfahrt" INTEGER,
|
||||
"kilometerRueckkehr" INTEGER,
|
||||
"wasserabgabe" INTEGER,
|
||||
"bemerkung" VARCHAR(500),
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "einsatzfahrzeug_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "einsatzkraft" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"einsatzId" UUID NOT NULL,
|
||||
"personId" UUID NOT NULL,
|
||||
"funktion" "EinsatzFunktion" NOT NULL DEFAULT 'TRUPPMANN',
|
||||
"fahrzeugId" UUID,
|
||||
"vonUm" TIMESTAMPTZ(3),
|
||||
"bisUm" TIMESTAMPTZ(3),
|
||||
"atemschutz" BOOLEAN NOT NULL DEFAULT false,
|
||||
"bemerkung" VARCHAR(500),
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "einsatzkraft_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "materialverbrauch" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"einsatzId" UUID NOT NULL,
|
||||
"geraetId" UUID,
|
||||
"bezeichnung" VARCHAR(200) NOT NULL,
|
||||
"menge" DECIMAL(10,2) NOT NULL,
|
||||
"einheit" VARCHAR(20) NOT NULL,
|
||||
"kosten" DECIMAL(10,2),
|
||||
"beschaedigt" BOOLEAN NOT NULL DEFAULT false,
|
||||
"bemerkung" VARCHAR(500),
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "materialverbrauch_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "lagemeldung" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"einsatzId" UUID NOT NULL,
|
||||
"zeitpunkt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"text" VARCHAR(2000) NOT NULL,
|
||||
"quelle" "LagemeldungQuelle" NOT NULL DEFAULT 'INTERN',
|
||||
"verfasserId" UUID,
|
||||
"verfasserName" VARCHAR(200),
|
||||
"fotoDateiId" UUID,
|
||||
"wichtig" BOOLEAN NOT NULL DEFAULT false,
|
||||
"portalZugangId" UUID,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geloeschtAm" TIMESTAMPTZ(3),
|
||||
|
||||
CONSTRAINT "lagemeldung_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "portal_zugang" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"einsatzId" UUID NOT NULL,
|
||||
"bezeichnung" VARCHAR(160) NOT NULL,
|
||||
"tokenHash" VARCHAR(120) NOT NULL,
|
||||
"gueltigBis" TIMESTAMPTZ(3) NOT NULL,
|
||||
"widerrufenAm" TIMESTAMPTZ(3),
|
||||
"aufrufe" INTEGER NOT NULL DEFAULT 0,
|
||||
"letzterAufruf" TIMESTAMPTZ(3),
|
||||
"letzteIp" INET,
|
||||
"erstelltVonId" UUID,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "portal_zugang_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "uebung" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"nummer" VARCHAR(40) NOT NULL,
|
||||
"thema" VARCHAR(200) NOT NULL,
|
||||
"art" "UebungsArt" NOT NULL DEFAULT 'PRAXIS',
|
||||
"datum" DATE NOT NULL,
|
||||
"beginnUm" TIMESTAMPTZ(3),
|
||||
"endeUm" TIMESTAMPTZ(3),
|
||||
"ort" VARCHAR(200),
|
||||
"leiterId" UUID,
|
||||
"lernziele" VARCHAR(2000),
|
||||
"beschreibung" VARCHAR(4000),
|
||||
"bericht" TEXT,
|
||||
"berichtDateiId" UUID,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
"geloeschtAm" TIMESTAMPTZ(3),
|
||||
|
||||
CONSTRAINT "uebung_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "uebungsteilnahme" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"uebungId" UUID NOT NULL,
|
||||
"personId" UUID NOT NULL,
|
||||
"funktion" "EinsatzFunktion",
|
||||
"vonUm" TIMESTAMPTZ(3),
|
||||
"bisUm" TIMESTAMPTZ(3),
|
||||
"bemerkung" VARCHAR(500),
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "uebungsteilnahme_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "einsatz_organisationId_alarmiertUm_idx" ON "einsatz"("organisationId", "alarmiertUm" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "einsatz_organisationId_status_idx" ON "einsatz"("organisationId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "einsatz_organisationId_kategorie_idx" ON "einsatz"("organisationId", "kategorie");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "einsatz_organisationId_einsatznummer_key" ON "einsatz"("organisationId", "einsatznummer");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "einsatzfahrzeug_organisationId_einsatzId_idx" ON "einsatzfahrzeug"("organisationId", "einsatzId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "einsatzfahrzeug_organisationId_fahrzeugId_idx" ON "einsatzfahrzeug"("organisationId", "fahrzeugId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "einsatzfahrzeug_einsatzId_fahrzeugId_key" ON "einsatzfahrzeug"("einsatzId", "fahrzeugId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "einsatzkraft_organisationId_einsatzId_idx" ON "einsatzkraft"("organisationId", "einsatzId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "einsatzkraft_organisationId_personId_idx" ON "einsatzkraft"("organisationId", "personId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "einsatzkraft_einsatzId_personId_key" ON "einsatzkraft"("einsatzId", "personId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "materialverbrauch_organisationId_einsatzId_idx" ON "materialverbrauch"("organisationId", "einsatzId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "lagemeldung_organisationId_einsatzId_zeitpunkt_idx" ON "lagemeldung"("organisationId", "einsatzId", "zeitpunkt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "portal_zugang_organisationId_einsatzId_idx" ON "portal_zugang"("organisationId", "einsatzId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "portal_zugang_gueltigBis_idx" ON "portal_zugang"("gueltigBis");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "uebung_organisationId_datum_idx" ON "uebung"("organisationId", "datum" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "uebung_organisationId_art_idx" ON "uebung"("organisationId", "art");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "uebung_organisationId_nummer_key" ON "uebung"("organisationId", "nummer");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "uebungsteilnahme_organisationId_personId_idx" ON "uebungsteilnahme"("organisationId", "personId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "uebungsteilnahme_uebungId_personId_key" ON "uebungsteilnahme"("uebungId", "personId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "einsatz" ADD CONSTRAINT "einsatz_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "einsatz" ADD CONSTRAINT "einsatz_einsatzleiterId_fkey" FOREIGN KEY ("einsatzleiterId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "einsatz" ADD CONSTRAINT "einsatz_freigegebenVonId_fkey" FOREIGN KEY ("freigegebenVonId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "einsatz" ADD CONSTRAINT "einsatz_berichtDateiId_fkey" FOREIGN KEY ("berichtDateiId") REFERENCES "datei"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "einsatzfahrzeug" ADD CONSTRAINT "einsatzfahrzeug_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "einsatzfahrzeug" ADD CONSTRAINT "einsatzfahrzeug_einsatzId_fkey" FOREIGN KEY ("einsatzId") REFERENCES "einsatz"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "einsatzfahrzeug" ADD CONSTRAINT "einsatzfahrzeug_fahrzeugId_fkey" FOREIGN KEY ("fahrzeugId") REFERENCES "fahrzeug"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "einsatzkraft" ADD CONSTRAINT "einsatzkraft_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "einsatzkraft" ADD CONSTRAINT "einsatzkraft_einsatzId_fkey" FOREIGN KEY ("einsatzId") REFERENCES "einsatz"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "einsatzkraft" ADD CONSTRAINT "einsatzkraft_personId_fkey" FOREIGN KEY ("personId") REFERENCES "person"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "einsatzkraft" ADD CONSTRAINT "einsatzkraft_fahrzeugId_fkey" FOREIGN KEY ("fahrzeugId") REFERENCES "fahrzeug"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "materialverbrauch" ADD CONSTRAINT "materialverbrauch_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "materialverbrauch" ADD CONSTRAINT "materialverbrauch_einsatzId_fkey" FOREIGN KEY ("einsatzId") REFERENCES "einsatz"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "materialverbrauch" ADD CONSTRAINT "materialverbrauch_geraetId_fkey" FOREIGN KEY ("geraetId") REFERENCES "geraet"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "lagemeldung" ADD CONSTRAINT "lagemeldung_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "lagemeldung" ADD CONSTRAINT "lagemeldung_einsatzId_fkey" FOREIGN KEY ("einsatzId") REFERENCES "einsatz"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "lagemeldung" ADD CONSTRAINT "lagemeldung_verfasserId_fkey" FOREIGN KEY ("verfasserId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "lagemeldung" ADD CONSTRAINT "lagemeldung_fotoDateiId_fkey" FOREIGN KEY ("fotoDateiId") REFERENCES "datei"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "lagemeldung" ADD CONSTRAINT "lagemeldung_portalZugangId_fkey" FOREIGN KEY ("portalZugangId") REFERENCES "portal_zugang"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "portal_zugang" ADD CONSTRAINT "portal_zugang_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "portal_zugang" ADD CONSTRAINT "portal_zugang_einsatzId_fkey" FOREIGN KEY ("einsatzId") REFERENCES "einsatz"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "portal_zugang" ADD CONSTRAINT "portal_zugang_erstelltVonId_fkey" FOREIGN KEY ("erstelltVonId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "uebung" ADD CONSTRAINT "uebung_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "uebung" ADD CONSTRAINT "uebung_leiterId_fkey" FOREIGN KEY ("leiterId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "uebung" ADD CONSTRAINT "uebung_berichtDateiId_fkey" FOREIGN KEY ("berichtDateiId") REFERENCES "datei"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "uebungsteilnahme" ADD CONSTRAINT "uebungsteilnahme_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "uebungsteilnahme" ADD CONSTRAINT "uebungsteilnahme_uebungId_fkey" FOREIGN KEY ("uebungId") REFERENCES "uebung"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "uebungsteilnahme" ADD CONSTRAINT "uebungsteilnahme_personId_fkey" FOREIGN KEY ("personId") REFERENCES "person"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,243 @@
|
||||
-- =============================================================================
|
||||
-- Phase 3: Mandantentrennung, Koordinaten, Berichtsfreigabe, Indizes.
|
||||
--
|
||||
-- Vier Dinge, die in der Datenbank besser aufgehoben sind als im Code:
|
||||
--
|
||||
-- 1. Row Level Security für die acht neuen Tabellen.
|
||||
--
|
||||
-- 2. Der Gleichlauf von Dezimalgrad und PostGIS-Punkt. Die Anwendung
|
||||
-- schreibt Breite und Länge als Zahl; den geography-Punkt setzt ein
|
||||
-- Trigger. Sonst müsste jede Schreiboperation Roh-SQL sein, und die
|
||||
-- Karte ab Phase 6 fände Einsätze, die über einen anderen Weg entstanden
|
||||
-- sind, nicht wieder.
|
||||
--
|
||||
-- 3. Die Unveränderbarkeit freigegebener Einsatzberichte. Der Bericht geht
|
||||
-- an die Gemeinde und im Schadensfall an die Versicherung. Dieselbe Logik
|
||||
-- wie beim Prüfprotokoll aus Phase 2.
|
||||
--
|
||||
-- 4. Suchindizes und ein Teilindex auf die laufenden Einsätze.
|
||||
-- =============================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Row Level Security
|
||||
-- -----------------------------------------------------------------------------
|
||||
DO $$
|
||||
DECLARE
|
||||
tabelle text;
|
||||
BEGIN
|
||||
FOREACH tabelle IN ARRAY ARRAY[
|
||||
'einsatz',
|
||||
'einsatzfahrzeug',
|
||||
'einsatzkraft',
|
||||
'materialverbrauch',
|
||||
'lagemeldung',
|
||||
'portal_zugang',
|
||||
'uebung',
|
||||
'uebungsteilnahme'
|
||||
]
|
||||
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;
|
||||
$$;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Koordinaten: Dezimalgrad und PostGIS-Punkt zusammenhalten
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION einsatz_koordinaten_setzen()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NEW."breitengrad" IS NULL OR NEW."laengengrad" IS NULL THEN
|
||||
NEW."koordinaten" := NULL;
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
-- Reihenfolge beachten: ST_MakePoint nimmt erst die Länge, dann die Breite.
|
||||
NEW."koordinaten" := ST_SetSRID(
|
||||
ST_MakePoint(NEW."laengengrad"::double precision, NEW."breitengrad"::double precision),
|
||||
4326
|
||||
)::geography;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION einsatz_koordinaten_setzen() IS
|
||||
'Haelt den PostGIS-Punkt eines Einsatzes mit den Dezimalgraden im Gleichlauf.';
|
||||
|
||||
CREATE TRIGGER einsatz_koordinaten
|
||||
BEFORE INSERT OR UPDATE OF "breitengrad", "laengengrad" ON "einsatz"
|
||||
FOR EACH ROW EXECUTE FUNCTION einsatz_koordinaten_setzen();
|
||||
|
||||
-- Koordinaten, die es auf der Erde nicht gibt, sind ein Tippfehler.
|
||||
ALTER TABLE "einsatz" ADD CONSTRAINT "einsatz_koordinaten_gueltig" CHECK (
|
||||
("breitengrad" IS NULL OR ("breitengrad" >= -90 AND "breitengrad" <= 90))
|
||||
AND ("laengengrad" IS NULL OR ("laengengrad" >= -180 AND "laengengrad" <= 180))
|
||||
);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Freigegebene Einsatzberichte sind unveränderbar
|
||||
--
|
||||
-- Bis zur Freigabe darf alles nachgetragen werden — ein Einsatz wird tagelang
|
||||
-- ergänzt, und genau dafür ist der Entwurfsstand da. Mit der Freigabe ist
|
||||
-- Schluss: der Bericht ist dann ein Dokument, kein Arbeitsstand.
|
||||
--
|
||||
-- Zwei Ausnahmen bleiben, beide nachtragend statt verändernd: das PDF, solange
|
||||
-- keines hinterlegt ist, und geaendertAm.
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION einsatz_freigabe_schuetzen()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
vergleich "einsatz"%ROWTYPE;
|
||||
BEGIN
|
||||
IF OLD."status" <> 'FREIGEGEBEN' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
vergleich := NEW;
|
||||
vergleich."geaendertAm" := OLD."geaendertAm";
|
||||
|
||||
IF OLD."berichtDateiId" IS NULL THEN
|
||||
vergleich."berichtDateiId" := OLD."berichtDateiId";
|
||||
END IF;
|
||||
|
||||
IF vergleich IS DISTINCT FROM OLD THEN
|
||||
RAISE EXCEPTION
|
||||
'Der Einsatzbericht % ist freigegeben und damit unveraenderlich.',
|
||||
OLD."einsatznummer"
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION einsatz_kein_loeschen()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF OLD."status" <> 'FREIGEGEBEN' THEN
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
|
||||
IF coalesce(current_setting('app.audit_bereinigung', true), '') = 'an' THEN
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
|
||||
RAISE EXCEPTION
|
||||
'Der Einsatzbericht % ist freigegeben und darf nicht geloescht werden '
|
||||
'(nur im Aufbewahrungslauf).',
|
||||
OLD."einsatznummer"
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER einsatz_freigabe
|
||||
BEFORE UPDATE ON "einsatz"
|
||||
FOR EACH ROW EXECUTE FUNCTION einsatz_freigabe_schuetzen();
|
||||
|
||||
CREATE TRIGGER einsatz_loeschschutz
|
||||
BEFORE DELETE ON "einsatz"
|
||||
FOR EACH ROW EXECUTE FUNCTION einsatz_kein_loeschen();
|
||||
|
||||
-- Dasselbe für die Bestandteile: sonst ließe sich ein freigegebener Bericht
|
||||
-- über die Einsatzkräfte oder den Materialverbrauch nachträglich verändern.
|
||||
CREATE OR REPLACE FUNCTION einsatzinhalt_gesperrt()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
betroffener_einsatz uuid;
|
||||
zustand "EinsatzStatus";
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
betroffener_einsatz := OLD."einsatzId";
|
||||
ELSE
|
||||
betroffener_einsatz := NEW."einsatzId";
|
||||
END IF;
|
||||
|
||||
SELECT "status" INTO zustand FROM "einsatz" WHERE "id" = betroffener_einsatz;
|
||||
|
||||
IF zustand IS NULL OR zustand <> 'FREIGEGEBEN' 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
|
||||
'Der Einsatz % ist freigegeben. % auf % ist nicht mehr zulaessig.',
|
||||
betroffener_einsatz, TG_OP, TG_TABLE_NAME
|
||||
USING ERRCODE = 'insufficient_privilege';
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER einsatzfahrzeug_gesperrt
|
||||
BEFORE INSERT OR UPDATE OR DELETE ON "einsatzfahrzeug"
|
||||
FOR EACH ROW EXECUTE FUNCTION einsatzinhalt_gesperrt();
|
||||
|
||||
CREATE TRIGGER einsatzkraft_gesperrt
|
||||
BEFORE INSERT OR UPDATE OR DELETE ON "einsatzkraft"
|
||||
FOR EACH ROW EXECUTE FUNCTION einsatzinhalt_gesperrt();
|
||||
|
||||
CREATE TRIGGER materialverbrauch_gesperrt
|
||||
BEFORE INSERT OR UPDATE OR DELETE ON "materialverbrauch"
|
||||
FOR EACH ROW EXECUTE FUNCTION einsatzinhalt_gesperrt();
|
||||
|
||||
-- Lagemeldungen bleiben ausdrücklich ausgenommen: sie sind ein Protokoll des
|
||||
-- Verlaufs, kein Bestandteil des Berichts, und eine Meldung aus dem Portal
|
||||
-- kann eintreffen, während der Bericht schon freigegeben ist.
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Portalzugänge
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Ein Zugang, der nie gültig war, ist ein Fehler in der Erzeugung.
|
||||
ALTER TABLE "portal_zugang" ADD CONSTRAINT "portal_zugang_gueltigkeit" CHECK (
|
||||
"gueltigBis" > "erstelltAm"
|
||||
);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Indizes
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Der Alarmmonitor fragt immer dasselbe: was läuft gerade.
|
||||
CREATE INDEX "einsatz_laufend" ON "einsatz" ("organisationId", "alarmiertUm" DESC)
|
||||
WHERE "status" = 'LAUFEND' AND "geloeschtAm" IS NULL;
|
||||
|
||||
-- Räumliche Abfragen ab Phase 6.
|
||||
CREATE INDEX "einsatz_koordinaten_idx" ON "einsatz" USING gist ("koordinaten");
|
||||
|
||||
-- Volltextsuche wie in den Phasen davor.
|
||||
CREATE INDEX "einsatz_suche_stichwort" ON "einsatz" USING gin ("stichwort" gin_trgm_ops);
|
||||
CREATE INDEX "einsatz_suche_ort" ON "einsatz" USING gin ("ort" gin_trgm_ops);
|
||||
CREATE INDEX "einsatz_suche_nummer" ON "einsatz" USING gin ("einsatznummer" gin_trgm_ops);
|
||||
CREATE INDEX "uebung_suche_thema" ON "uebung" USING gin ("thema" 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 den Phasen davor gelten unveraendert weiter.
|
||||
EXECUTE 'REVOKE INSERT, UPDATE, DELETE ON "berechtigung" FROM florian_app';
|
||||
EXECUTE 'REVOKE UPDATE, DELETE ON "audit_log" FROM florian_app';
|
||||
ELSE
|
||||
RAISE NOTICE 'Rolle florian_app nicht vorhanden — Rechtevergabe uebersprungen.';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
@@ -180,6 +180,16 @@ model Organisation {
|
||||
rueckrufBetroffenheiten RueckrufBetroffenheit[]
|
||||
erinnerungen Erinnerung[]
|
||||
|
||||
// Phase 3 — Einsätze und Übungen
|
||||
einsaetze Einsatz[]
|
||||
einsatzfahrzeuge Einsatzfahrzeug[]
|
||||
einsatzkraefte Einsatzkraft[]
|
||||
materialverbrauch Materialverbrauch[]
|
||||
lagemeldungen Lagemeldung[]
|
||||
portalzugaenge PortalZugang[]
|
||||
uebungen Uebung[]
|
||||
uebungsteilnahmen Uebungsteilnahme[]
|
||||
|
||||
@@index([aktiv])
|
||||
@@map("organisation")
|
||||
}
|
||||
@@ -253,6 +263,15 @@ model Person {
|
||||
zugewieseneMaengel Mangel[] @relation("MangelZustaendig")
|
||||
behobeneMaengel Mangel[] @relation("MangelBehebung")
|
||||
|
||||
// Phase 3 — Einsätze und Übungen
|
||||
geleiteteEinsaetze Einsatz[] @relation("EinsatzLeitung")
|
||||
freigegebeneBerichte Einsatz[] @relation("EinsatzFreigabe")
|
||||
einsatzteilnahmen Einsatzkraft[] @relation("EinsatzTeilnahme")
|
||||
lagemeldungen Lagemeldung[] @relation("LagemeldungVerfasser")
|
||||
portalzugaenge PortalZugang[] @relation("PortalzugangErsteller")
|
||||
geleiteteUebungen Uebung[] @relation("UebungLeitung")
|
||||
uebungsteilnahmen Uebungsteilnahme[] @relation("UebungTeilnahme")
|
||||
|
||||
/// E-Mail und OIDC-Subject sind je Wehr eindeutig, nicht global — dieselbe
|
||||
/// Person kann in zwei Wehren Mitglied sein.
|
||||
@@unique([organisationId, email])
|
||||
@@ -482,6 +501,11 @@ model Datei {
|
||||
alsSignaturbild Signatur[] @relation("SignaturBild")
|
||||
alsMangelFoto Mangel[] @relation("MangelFoto")
|
||||
|
||||
// Phase 3
|
||||
alsEinsatzbericht Einsatz[] @relation("EinsatzBericht")
|
||||
alsLagemeldungsfoto Lagemeldung[] @relation("LagemeldungFoto")
|
||||
alsUebungsbericht Uebung[] @relation("UebungBericht")
|
||||
|
||||
@@unique([bucket, objektSchluessel])
|
||||
@@index([organisationId, entitaet, entitaetId])
|
||||
@@index([organisationId, kategorie])
|
||||
@@ -648,6 +672,10 @@ model Fahrzeug {
|
||||
geraeteraeume Geraeteraum[]
|
||||
geraete Geraet[]
|
||||
|
||||
// Phase 3
|
||||
einsaetze Einsatzfahrzeug[]
|
||||
einsatzkraefte Einsatzkraft[]
|
||||
|
||||
@@unique([organisationId, funkrufname])
|
||||
@@unique([organisationId, kennzeichen])
|
||||
@@index([organisationId, aktiv])
|
||||
@@ -783,6 +811,7 @@ model Geraet {
|
||||
pruefungen Pruefung[]
|
||||
maengel Mangel[]
|
||||
rueckrufBetroffenheiten RueckrufBetroffenheit[]
|
||||
materialverbrauch Materialverbrauch[]
|
||||
|
||||
@@unique([organisationId, inventarnummer])
|
||||
@@index([organisationId, status])
|
||||
@@ -1403,3 +1432,427 @@ model Erinnerung {
|
||||
@@index([organisationId, versendetAm(sort: Desc)])
|
||||
@@map("erinnerung")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Phase 3 — Einsätze und Übungen
|
||||
//
|
||||
// Ordnungsprinzip:
|
||||
// Einsatz der Vorgang selbst, mit Ort, Zeitstempeln und Bericht
|
||||
// Einsatzfahrzeug welches Fahrzeug wann ausgerückt und eingerückt ist
|
||||
// Einsatzkraft wer mitgefahren ist, in welcher Funktion, wie lange
|
||||
// Materialverbrauch was dabei verbraucht oder beschädigt wurde
|
||||
// Lagemeldung was währenddessen gemeldet wurde, mit Foto
|
||||
// PortalZugang ein zeitlich begrenzter Link für eine Fremdwehr
|
||||
// Uebung dasselbe für Übungen, mit Thema und Lernzielen
|
||||
//
|
||||
// Drei Festlegungen, die den Aufbau erklären:
|
||||
//
|
||||
// 1. Die Zeitstempel sind der Kern. Alarmierung, Ausrücken, Eintreffen,
|
||||
// Einsatzende, Einrücken, Einsatzbereitschaft — daraus rechnet sich die
|
||||
// Ausrückzeit, die Anfahrtszeit und die Einsatzdauer. Sie stehen deshalb
|
||||
// einzeln und nicht als Sammelfeld.
|
||||
//
|
||||
// 2. Koordinaten liegen doppelt: als Dezimalgrad, die Prisma lesen kann, und
|
||||
// als PostGIS-Punkt für die Karte ab Phase 6. Ein Trigger hält beides
|
||||
// zusammen — sonst müsste jede Schreiboperation Roh-SQL sein.
|
||||
//
|
||||
// 3. Ein freigegebener Einsatzbericht ist unveränderbar, dieselbe Logik wie
|
||||
// beim Prüfprotokoll: er geht an die Gemeinde und an die Versicherung.
|
||||
// =============================================================================
|
||||
|
||||
/// Einsatzkategorie nach der bayerischen Feuerwehrstatistik. Danach meldet die
|
||||
/// Wehr ihre Zahlen an die Gemeinde und den Landkreis.
|
||||
enum EinsatzKategorie {
|
||||
BRAND_KLEIN
|
||||
BRAND_MITTEL
|
||||
BRAND_GROSS
|
||||
TECHNISCHE_HILFE_KLEIN
|
||||
TECHNISCHE_HILFE_MITTEL
|
||||
TECHNISCHE_HILFE_GROSS
|
||||
GEFAHRGUT
|
||||
/// Unterstützung des Rettungsdienstes, z. B. Türöffnung, Tragehilfe.
|
||||
RETTUNGSDIENST
|
||||
/// Auslösung einer Brandmeldeanlage ohne Schadenfeuer.
|
||||
BRANDMELDEANLAGE
|
||||
BOESWILLIGER_ALARM
|
||||
SICHERHEITSWACHE
|
||||
/// Amtshilfe, Nachbarschaftshilfe, Sonstiges.
|
||||
UNTERSTUETZUNG
|
||||
SONSTIGES
|
||||
}
|
||||
|
||||
/// Lebenszyklus eines Einsatzes.
|
||||
enum EinsatzStatus {
|
||||
/// Schnellerfassung: Stichwort und Fahrzeuge stehen, der Rest fehlt noch.
|
||||
ENTWURF
|
||||
/// Läuft gerade — der Alarmmonitor und das Portal zeigen ihn an.
|
||||
LAUFEND
|
||||
/// Nachbereitet und abgeschlossen.
|
||||
ABGESCHLOSSEN
|
||||
/// Bericht freigegeben und festgeschrieben.
|
||||
FREIGEGEBEN
|
||||
}
|
||||
|
||||
/// Wie alarmiert wurde. Für die Auswertung, ob die Alarmierungswege greifen.
|
||||
enum Alarmierungsart {
|
||||
SIRENE
|
||||
MELDEEMPFAENGER
|
||||
TELEFON
|
||||
APP
|
||||
PERSOENLICH
|
||||
SONSTIGES
|
||||
}
|
||||
|
||||
/// Funktion einer Einsatzkraft. Aus ihr ergibt sich die Stärke nach FwDV 3.
|
||||
enum EinsatzFunktion {
|
||||
EINSATZLEITER
|
||||
ZUGFUEHRER
|
||||
GRUPPENFUEHRER
|
||||
STAFFELFUEHRER
|
||||
TRUPPFUEHRER
|
||||
MASCHINIST
|
||||
MELDER
|
||||
TRUPPMANN
|
||||
ATEMSCHUTZTRAEGER
|
||||
SICHERHEITSTRUPP
|
||||
SANITAETER
|
||||
SONSTIGES
|
||||
}
|
||||
|
||||
/// Art einer Übung.
|
||||
enum UebungsArt {
|
||||
PRAXIS
|
||||
THEORIE
|
||||
ATEMSCHUTZ
|
||||
MASCHINIST
|
||||
FUNK
|
||||
ERSTE_HILFE
|
||||
/// Gemeinsam mit Nachbarwehren, THW oder Rettungsdienst.
|
||||
GEMEINSCHAFTSUEBUNG
|
||||
SONSTIGES
|
||||
}
|
||||
|
||||
/// Woher eine Lagemeldung kommt.
|
||||
enum LagemeldungQuelle {
|
||||
/// Aus der Anwendung, von einer angemeldeten Person.
|
||||
INTERN
|
||||
/// Über den Portal-Link, von einer Fremdwehr ohne Konto.
|
||||
PORTAL
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/// Ein Einsatz.
|
||||
model Einsatz {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
/// Fortlaufend je Wehr und Jahr, Format "JJJJ-NNNN".
|
||||
einsatznummer String @db.VarChar(40)
|
||||
/// Einsatzstichwort der Leitstelle, z. B. "B 3 Wohnhausbrand".
|
||||
stichwort String @db.VarChar(160)
|
||||
kategorie EinsatzKategorie @default(SONSTIGES)
|
||||
status EinsatzStatus @default(ENTWURF)
|
||||
|
||||
/// Einsatznummer der Integrierten Leitstelle — die Klammer zu deren Akte.
|
||||
ilsNummer String? @db.VarChar(60)
|
||||
alarmierungsart Alarmierungsart?
|
||||
|
||||
// Ort
|
||||
strasse String? @db.VarChar(200)
|
||||
hausnummer String? @db.VarChar(20)
|
||||
plz String? @db.VarChar(10)
|
||||
ort String? @db.VarChar(120)
|
||||
ortsteil String? @db.VarChar(120)
|
||||
/// Objektbezeichnung, z. B. "Grundschule", "Autobahn A9, km 312".
|
||||
objekt String? @db.VarChar(200)
|
||||
|
||||
/// Koordinaten als Dezimalgrad — von Prisma lesbar.
|
||||
breitengrad Decimal? @db.Decimal(9, 6)
|
||||
laengengrad Decimal? @db.Decimal(9, 6)
|
||||
/// Dieselben Koordinaten als PostGIS-Punkt, von einem Trigger gepflegt.
|
||||
/// Ab Phase 6 zeichnet die Karte daraus.
|
||||
koordinaten Unsupported("geography(Point, 4326)")?
|
||||
/// Woher sie stammen: "nominatim", "manuell", "leitstelle".
|
||||
koordinatenQuelle String? @db.VarChar(40)
|
||||
|
||||
// Zeitstempel — der Kern jeder Einsatzauswertung
|
||||
alarmiertUm DateTime? @db.Timestamptz(3)
|
||||
ausgeruecktUm DateTime? @db.Timestamptz(3)
|
||||
eingetroffenUm DateTime? @db.Timestamptz(3)
|
||||
einsatzendeUm DateTime? @db.Timestamptz(3)
|
||||
eingeruecktUm DateTime? @db.Timestamptz(3)
|
||||
einsatzbereitUm DateTime? @db.Timestamptz(3)
|
||||
|
||||
einsatzleiterId String? @db.Uuid
|
||||
|
||||
beschreibung String? @db.VarChar(4000)
|
||||
bericht String?
|
||||
|
||||
wetter String? @db.VarChar(200)
|
||||
temperatur Int?
|
||||
/// Wer sonst vor Ort war: Polizei, Rettungsdienst, THW, Nachbarwehren.
|
||||
weitereKraefte String? @db.VarChar(500)
|
||||
|
||||
/// Zahlen für die Statistik der Gemeinde.
|
||||
geretteteePersonen Int @default(0)
|
||||
verletztePersonen Int @default(0)
|
||||
totePersonen Int @default(0)
|
||||
sachschaden Decimal? @db.Decimal(12, 2)
|
||||
/// Kostenersatzpflichtiger Einsatz (Art. 28 BayFwG).
|
||||
kostenpflichtig Boolean @default(false)
|
||||
|
||||
/// Der erzeugte Einsatzbericht als PDF.
|
||||
berichtDateiId String? @db.Uuid
|
||||
freigegebenAm DateTime? @db.Timestamptz(3)
|
||||
freigegebenVonId String? @db.Uuid
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
geloeschtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
einsatzleiter Person? @relation("EinsatzLeitung", fields: [einsatzleiterId], references: [id], onDelete: SetNull)
|
||||
freigegebenVon Person? @relation("EinsatzFreigabe", fields: [freigegebenVonId], references: [id], onDelete: SetNull)
|
||||
berichtDatei Datei? @relation("EinsatzBericht", fields: [berichtDateiId], references: [id], onDelete: SetNull)
|
||||
fahrzeuge Einsatzfahrzeug[]
|
||||
kraefte Einsatzkraft[]
|
||||
material Materialverbrauch[]
|
||||
lagemeldungen Lagemeldung[]
|
||||
portalzugaenge PortalZugang[]
|
||||
|
||||
@@unique([organisationId, einsatznummer])
|
||||
@@index([organisationId, alarmiertUm(sort: Desc)])
|
||||
@@index([organisationId, status])
|
||||
@@index([organisationId, kategorie])
|
||||
@@map("einsatz")
|
||||
}
|
||||
|
||||
/// Ein Fahrzeug in einem Einsatz, mit eigenen Zeiten: nicht jedes Fahrzeug
|
||||
/// rückt gleichzeitig aus, und die Nachalarmierung soll sichtbar bleiben.
|
||||
model Einsatzfahrzeug {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
einsatzId String @db.Uuid
|
||||
fahrzeugId String @db.Uuid
|
||||
|
||||
ausgeruecktUm DateTime? @db.Timestamptz(3)
|
||||
eingetroffenUm DateTime? @db.Timestamptz(3)
|
||||
eingeruecktUm DateTime? @db.Timestamptz(3)
|
||||
|
||||
kilometerAbfahrt Int?
|
||||
kilometerRueckkehr Int?
|
||||
/// Verbrauchtes Löschwasser in Litern.
|
||||
wasserabgabe Int?
|
||||
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)
|
||||
einsatz Einsatz @relation(fields: [einsatzId], references: [id], onDelete: Cascade)
|
||||
fahrzeug Fahrzeug @relation(fields: [fahrzeugId], references: [id], onDelete: Restrict)
|
||||
|
||||
@@unique([einsatzId, fahrzeugId])
|
||||
@@index([organisationId, einsatzId])
|
||||
@@index([organisationId, fahrzeugId])
|
||||
@@map("einsatzfahrzeug")
|
||||
}
|
||||
|
||||
/// Wer mitgefahren ist. Grundlage für die Stärkemeldung nach FwDV 3 und für
|
||||
/// den Beteiligungsnachweis je Person.
|
||||
model Einsatzkraft {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
einsatzId String @db.Uuid
|
||||
personId String @db.Uuid
|
||||
|
||||
funktion EinsatzFunktion @default(TRUPPMANN)
|
||||
/// Auf welchem Fahrzeug — leer, wenn jemand selbst angefahren ist.
|
||||
fahrzeugId String? @db.Uuid
|
||||
|
||||
vonUm DateTime? @db.Timestamptz(3)
|
||||
bisUm DateTime? @db.Timestamptz(3)
|
||||
|
||||
/// War unter Atemschutz im Einsatz. Zählt auf die Nachweise nach FwDV 7.
|
||||
atemschutz Boolean @default(false)
|
||||
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)
|
||||
einsatz Einsatz @relation(fields: [einsatzId], references: [id], onDelete: Cascade)
|
||||
person Person @relation("EinsatzTeilnahme", fields: [personId], references: [id], onDelete: Cascade)
|
||||
fahrzeug Fahrzeug? @relation(fields: [fahrzeugId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@unique([einsatzId, personId])
|
||||
@@index([organisationId, einsatzId])
|
||||
@@index([organisationId, personId])
|
||||
@@map("einsatzkraft")
|
||||
}
|
||||
|
||||
/// Was verbraucht oder beschädigt wurde — Schaummittel, Bindemittel, ein
|
||||
/// zerschnittener Schlauch. Grundlage für Nachbeschaffung und Kostenersatz.
|
||||
model Materialverbrauch {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
einsatzId String @db.Uuid
|
||||
/// Verweis auf ein erfasstes Gerät, wo es eines gibt.
|
||||
geraetId String? @db.Uuid
|
||||
|
||||
bezeichnung String @db.VarChar(200)
|
||||
menge Decimal @db.Decimal(10, 2)
|
||||
einheit String @db.VarChar(20)
|
||||
kosten Decimal? @db.Decimal(10, 2)
|
||||
/// Nicht verbraucht, sondern kaputtgegangen — dann folgt ein Mangel.
|
||||
beschaedigt Boolean @default(false)
|
||||
bemerkung String? @db.VarChar(500)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
einsatz Einsatz @relation(fields: [einsatzId], references: [id], onDelete: Cascade)
|
||||
geraet Geraet? @relation(fields: [geraetId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([organisationId, einsatzId])
|
||||
@@map("materialverbrauch")
|
||||
}
|
||||
|
||||
/// Eine Meldung während des Einsatzes. Aus der Anwendung oder über den
|
||||
/// Portal-Link einer Fremdwehr.
|
||||
model Lagemeldung {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
einsatzId String @db.Uuid
|
||||
|
||||
zeitpunkt DateTime @default(now()) @db.Timestamptz(3)
|
||||
text String @db.VarChar(2000)
|
||||
quelle LagemeldungQuelle @default(INTERN)
|
||||
|
||||
verfasserId String? @db.Uuid
|
||||
/// Klarname oder Wehrname — bei Portalmeldungen die einzige Angabe.
|
||||
verfasserName String? @db.VarChar(200)
|
||||
|
||||
fotoDateiId String? @db.Uuid
|
||||
/// Hebt die Meldung im Verlauf hervor: Menschenrettung, Nachforderung,
|
||||
/// Lageänderung.
|
||||
wichtig Boolean @default(false)
|
||||
|
||||
/// Über welchen Portalzugang die Meldung kam.
|
||||
portalZugangId String? @db.Uuid
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geloeschtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
einsatz Einsatz @relation(fields: [einsatzId], references: [id], onDelete: Cascade)
|
||||
verfasser Person? @relation("LagemeldungVerfasser", fields: [verfasserId], references: [id], onDelete: SetNull)
|
||||
foto Datei? @relation("LagemeldungFoto", fields: [fotoDateiId], references: [id], onDelete: SetNull)
|
||||
portalZugang PortalZugang? @relation(fields: [portalZugangId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([organisationId, einsatzId, zeitpunkt])
|
||||
@@map("lagemeldung")
|
||||
}
|
||||
|
||||
/// Ein zeitlich begrenzter Zugang zum Einsatzportal für eine Fremdwehr.
|
||||
///
|
||||
/// Der Link enthält die Kennung und ein Geheimnis; gespeichert wird nur dessen
|
||||
/// Hash. Er gilt bis gueltigBis, lässt sich jederzeit widerrufen und zählt
|
||||
/// jeden Aufruf mit — ein Link, der nach dem Einsatz noch benutzt wird, fällt
|
||||
/// so auf.
|
||||
model PortalZugang {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
einsatzId String @db.Uuid
|
||||
|
||||
/// Für wen der Link ist, z. B. "FF Nachbarhausen".
|
||||
bezeichnung String @db.VarChar(160)
|
||||
/// bcrypt-Hash des Geheimnisses aus dem Link.
|
||||
tokenHash String @db.VarChar(120)
|
||||
|
||||
gueltigBis DateTime @db.Timestamptz(3)
|
||||
widerrufenAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
aufrufe Int @default(0)
|
||||
letzterAufruf DateTime? @db.Timestamptz(3)
|
||||
/// Zuletzt sehende IP — für den Fall, dass ein Link weitergegeben wurde.
|
||||
letzteIp String? @db.Inet
|
||||
|
||||
erstelltVonId String? @db.Uuid
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
einsatz Einsatz @relation(fields: [einsatzId], references: [id], onDelete: Cascade)
|
||||
erstelltVon Person? @relation("PortalzugangErsteller", fields: [erstelltVonId], references: [id], onDelete: SetNull)
|
||||
lagemeldungen Lagemeldung[]
|
||||
|
||||
@@index([organisationId, einsatzId])
|
||||
@@index([gueltigBis])
|
||||
@@map("portal_zugang")
|
||||
}
|
||||
|
||||
/// Eine Übung. Aufgebaut wie ein Einsatz, aber mit Ausbildungsthema und
|
||||
/// Lernzielen statt Schadenlage — und ohne Portal.
|
||||
model Uebung {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
/// Fortlaufend je Wehr und Jahr, Format "JJJJ-NNNN".
|
||||
nummer String @db.VarChar(40)
|
||||
thema String @db.VarChar(200)
|
||||
art UebungsArt @default(PRAXIS)
|
||||
|
||||
datum DateTime @db.Date
|
||||
beginnUm DateTime? @db.Timestamptz(3)
|
||||
endeUm DateTime? @db.Timestamptz(3)
|
||||
|
||||
ort String? @db.VarChar(200)
|
||||
leiterId String? @db.Uuid
|
||||
|
||||
/// Was gelernt werden soll — die Lernziele der Ausbildung.
|
||||
lernziele String? @db.VarChar(2000)
|
||||
beschreibung String? @db.VarChar(4000)
|
||||
bericht String?
|
||||
|
||||
berichtDateiId String? @db.Uuid
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
geloeschtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
leiter Person? @relation("UebungLeitung", fields: [leiterId], references: [id], onDelete: SetNull)
|
||||
berichtDatei Datei? @relation("UebungBericht", fields: [berichtDateiId], references: [id], onDelete: SetNull)
|
||||
teilnahmen Uebungsteilnahme[]
|
||||
|
||||
@@unique([organisationId, nummer])
|
||||
@@index([organisationId, datum(sort: Desc)])
|
||||
@@index([organisationId, art])
|
||||
@@map("uebung")
|
||||
}
|
||||
|
||||
/// Wer an einer Übung teilgenommen hat. Der Nachweis, den eine Wehr für die
|
||||
/// Beförderung und für die Statistik braucht.
|
||||
model Uebungsteilnahme {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
uebungId String @db.Uuid
|
||||
personId String @db.Uuid
|
||||
|
||||
funktion EinsatzFunktion?
|
||||
vonUm DateTime? @db.Timestamptz(3)
|
||||
bisUm DateTime? @db.Timestamptz(3)
|
||||
bemerkung String? @db.VarChar(500)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
uebung Uebung @relation(fields: [uebungId], references: [id], onDelete: Cascade)
|
||||
person Person @relation("UebungTeilnahme", fields: [personId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([uebungId, personId])
|
||||
@@index([organisationId, personId])
|
||||
@@map("uebungsteilnahme")
|
||||
}
|
||||
|
||||
+442
-1
@@ -7,7 +7,8 @@
|
||||
* Inventarnummern, 14 Qualifikationstypen und die Qualifikationen der
|
||||
* Mannschaft. Dazu ab Phase 2: 5 Wartungsvorlagen mit Checklisten, die daraus
|
||||
* fälligen Prüftermine, ein abgeschlossenes Protokoll samt Mangel und ein
|
||||
* Beispielrückruf.
|
||||
* Beispielrückruf. Ab Phase 3 dazu: acht Einsätze über das Jahr verteilt,
|
||||
* sechs Übungen und die Beteiligung der Mannschaft.
|
||||
*
|
||||
* 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
|
||||
@@ -20,8 +21,12 @@
|
||||
*/
|
||||
import "dotenv/config";
|
||||
import {
|
||||
Alarmierungsart,
|
||||
AusweisTokenTyp,
|
||||
ChecklistenpunktTyp,
|
||||
EinsatzFunktion,
|
||||
EinsatzKategorie,
|
||||
EinsatzStatus,
|
||||
Dienstgrad,
|
||||
FahrzeugArt,
|
||||
GeraeteKategorie,
|
||||
@@ -38,6 +43,7 @@ import {
|
||||
RolleSchluessel,
|
||||
SignaturRolle,
|
||||
StandortArt,
|
||||
UebungsArt,
|
||||
} from "@prisma/client";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
@@ -1512,6 +1518,437 @@ async function rueckrufAnlegen(organisationId: string) {
|
||||
log("Rückruf: 1 Beispiel");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Phase 3 — Einsätze und Übungen
|
||||
//
|
||||
// Ein Jahr im Leben einer Wehr: Brände, technische Hilfeleistungen, ein
|
||||
// Fehlalarm einer Brandmeldeanlage und die Übungen dazwischen. Die Zahlen sind
|
||||
// so gewählt, dass die Statistik etwas zeigt — auch eine Hilfsfrist, die
|
||||
// gerade noch passt.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
interface EinsatzVorlage {
|
||||
/** Tage vor heute. */
|
||||
vorTagen: number;
|
||||
uhrzeit: [number, number];
|
||||
stichwort: string;
|
||||
kategorie: EinsatzKategorie;
|
||||
strasse: string;
|
||||
hausnummer?: string;
|
||||
objekt?: string;
|
||||
/** Minuten nach der Alarmierung: ausgerückt, eingetroffen, Ende, eingerückt. */
|
||||
ablauf: [number, number, number, number];
|
||||
fahrzeuge: number[];
|
||||
/** Wie viele Kräfte, und wer davon führt. */
|
||||
mannschaft: number;
|
||||
gerettet?: number;
|
||||
verletzt?: number;
|
||||
sachschaden?: number;
|
||||
beschreibung: string;
|
||||
bericht?: string;
|
||||
meldungen?: Array<{ nachMinuten: number; text: string; wichtig?: boolean }>;
|
||||
material?: Array<{ bezeichnung: string; menge: number; einheit: string }>;
|
||||
status?: EinsatzStatus;
|
||||
}
|
||||
|
||||
const EINSAETZE: EinsatzVorlage[] = [
|
||||
{
|
||||
vorTagen: 12,
|
||||
uhrzeit: [3, 42],
|
||||
stichwort: "B 3 Wohnhausbrand",
|
||||
kategorie: EinsatzKategorie.BRAND_GROSS,
|
||||
strasse: "Bahnhofstraße",
|
||||
hausnummer: "14",
|
||||
objekt: "Reihenhaus, zwei Vollgeschosse",
|
||||
ablauf: [5, 9, 143, 168],
|
||||
fahrzeuge: [0, 1],
|
||||
mannschaft: 14,
|
||||
gerettet: 2,
|
||||
verletzt: 1,
|
||||
sachschaden: 85000,
|
||||
beschreibung:
|
||||
"Bei Eintreffen Rauch aus dem Dachgeschoss, zwei Personen im Obergeschoss gemeldet.",
|
||||
bericht:
|
||||
"Menschenrettung über die Steckleiter durch den Angriffstrupp. Zwei Personen unverletzt gerettet, eine Person mit Rauchgasintoxikation an den Rettungsdienst übergeben.\n\nBrandbekämpfung mit einem C-Rohr unter Atemschutz vom Treppenraum aus. Nachlöscharbeiten und Kontrolle mit der Wärmebildkamera bis 06:05 Uhr. Einsatzstelle an die Polizei übergeben.",
|
||||
meldungen: [
|
||||
{ nachMinuten: 10, text: "Rauch aus Dachgeschoss, zwei Personen vermisst.", wichtig: true },
|
||||
{ nachMinuten: 22, text: "Beide Personen gerettet, Menschenrettung abgeschlossen.", wichtig: true },
|
||||
{ nachMinuten: 48, text: "Feuer unter Kontrolle." },
|
||||
{ nachMinuten: 140, text: "Nachlöscharbeiten beendet, Kontrolle mit Wärmebildkamera." },
|
||||
],
|
||||
material: [
|
||||
{ bezeichnung: "Atemluftflaschen 300 bar", menge: 6, einheit: "Stück" },
|
||||
{ bezeichnung: "Löschwasser", menge: 4500, einheit: "Liter" },
|
||||
],
|
||||
},
|
||||
{
|
||||
vorTagen: 26,
|
||||
uhrzeit: [14, 8],
|
||||
stichwort: "THL 1 Ölspur",
|
||||
kategorie: EinsatzKategorie.TECHNISCHE_HILFE_KLEIN,
|
||||
strasse: "Hauptstraße",
|
||||
ablauf: [6, 11, 55, 68],
|
||||
fahrzeuge: [1],
|
||||
mannschaft: 6,
|
||||
beschreibung: "Ölspur über etwa 300 Meter, ausgehend von einem Traktor.",
|
||||
bericht: "Ölspur mit Bindemittel abgestreut und aufgenommen. Straßenmeisterei verständigt.",
|
||||
material: [{ bezeichnung: "Ölbindemittel", menge: 40, einheit: "kg" }],
|
||||
},
|
||||
{
|
||||
vorTagen: 41,
|
||||
uhrzeit: [19, 55],
|
||||
stichwort: "THL 2 Verkehrsunfall",
|
||||
kategorie: EinsatzKategorie.TECHNISCHE_HILFE_MITTEL,
|
||||
strasse: "Kreisstraße MÜ 12",
|
||||
objekt: "zwischen Musterhausen und Nachbarhausen",
|
||||
ablauf: [4, 12, 78, 96],
|
||||
fahrzeuge: [0],
|
||||
mannschaft: 9,
|
||||
verletzt: 2,
|
||||
sachschaden: 24000,
|
||||
beschreibung: "Zwei Fahrzeuge, eine Person eingeklemmt.",
|
||||
bericht:
|
||||
"Person mit dem hydraulischen Rettungssatz befreit und an den Rettungsdienst übergeben. Brandschutz sichergestellt, Fahrzeuge gegen Wegrollen gesichert, Betriebsstoffe aufgenommen.",
|
||||
meldungen: [
|
||||
{ nachMinuten: 14, text: "Eine Person eingeklemmt, Rettungsdienst vor Ort.", wichtig: true },
|
||||
{ nachMinuten: 39, text: "Person befreit und übergeben.", wichtig: true },
|
||||
],
|
||||
material: [{ bezeichnung: "Ölbindemittel", menge: 15, einheit: "kg" }],
|
||||
},
|
||||
{
|
||||
vorTagen: 58,
|
||||
uhrzeit: [9, 12],
|
||||
stichwort: "BMA Auslösung",
|
||||
kategorie: EinsatzKategorie.BRANDMELDEANLAGE,
|
||||
strasse: "Schulstraße",
|
||||
hausnummer: "3",
|
||||
objekt: "Grundschule",
|
||||
ablauf: [7, 13, 32, 44],
|
||||
fahrzeuge: [0],
|
||||
mannschaft: 8,
|
||||
beschreibung: "Auslösung eines Rauchmelders im Werkraum.",
|
||||
bericht: "Kein Schadenfeuer. Auslösung durch Staub bei Reinigungsarbeiten. Anlage zurückgestellt.",
|
||||
},
|
||||
{
|
||||
vorTagen: 73,
|
||||
uhrzeit: [16, 30],
|
||||
stichwort: "B 1 Kleinbrand",
|
||||
kategorie: EinsatzKategorie.BRAND_KLEIN,
|
||||
strasse: "Am Anger",
|
||||
hausnummer: "7",
|
||||
ablauf: [5, 8, 41, 52],
|
||||
fahrzeuge: [1],
|
||||
mannschaft: 6,
|
||||
sachschaden: 1200,
|
||||
beschreibung: "Brennende Mülltonne an der Hauswand.",
|
||||
bericht: "Ablöschen mit Schnellangriff, Kontrolle der Fassade mit der Wärmebildkamera.",
|
||||
},
|
||||
{
|
||||
vorTagen: 95,
|
||||
uhrzeit: [21, 47],
|
||||
stichwort: "THL 1 Sturmschaden",
|
||||
kategorie: EinsatzKategorie.TECHNISCHE_HILFE_KLEIN,
|
||||
strasse: "Lindenweg",
|
||||
ablauf: [8, 16, 62, 74],
|
||||
fahrzeuge: [0],
|
||||
mannschaft: 7,
|
||||
beschreibung: "Baum auf Fahrbahn nach Gewitterfront.",
|
||||
bericht: "Baum mit der Motorkettensäge zerlegt und beiseitegeräumt. Fahrbahn gereinigt.",
|
||||
material: [{ bezeichnung: "Kettensägenbenzin", menge: 2, einheit: "Liter" }],
|
||||
},
|
||||
{
|
||||
vorTagen: 118,
|
||||
uhrzeit: [11, 20],
|
||||
stichwort: "Sicherheitswache Pfarrfest",
|
||||
kategorie: EinsatzKategorie.SICHERHEITSWACHE,
|
||||
strasse: "Kirchplatz",
|
||||
ablauf: [10, 20, 300, 310],
|
||||
fahrzeuge: [2],
|
||||
mannschaft: 4,
|
||||
beschreibung: "Brandsicherheitswache beim Pfarrfest.",
|
||||
bericht: "Ohne besondere Vorkommnisse.",
|
||||
},
|
||||
{
|
||||
vorTagen: 140,
|
||||
uhrzeit: [6, 5],
|
||||
stichwort: "THL 1 Türöffnung",
|
||||
kategorie: EinsatzKategorie.RETTUNGSDIENST,
|
||||
strasse: "Rosenstraße",
|
||||
hausnummer: "22",
|
||||
ablauf: [6, 10, 28, 38],
|
||||
fahrzeuge: [1],
|
||||
mannschaft: 5,
|
||||
gerettet: 1,
|
||||
beschreibung: "Hilflose Person hinter verschlossener Tür, Rettungsdienst vor Ort.",
|
||||
bericht: "Tür schonend geöffnet, Person an den Rettungsdienst übergeben.",
|
||||
},
|
||||
];
|
||||
|
||||
/** Funktionen einer Gruppe in der Reihenfolge, in der sie besetzt werden. */
|
||||
const FUNKTIONSFOLGE: EinsatzFunktion[] = [
|
||||
EinsatzFunktion.GRUPPENFUEHRER,
|
||||
EinsatzFunktion.MASCHINIST,
|
||||
EinsatzFunktion.MELDER,
|
||||
EinsatzFunktion.TRUPPFUEHRER,
|
||||
EinsatzFunktion.TRUPPMANN,
|
||||
EinsatzFunktion.TRUPPFUEHRER,
|
||||
EinsatzFunktion.TRUPPMANN,
|
||||
EinsatzFunktion.TRUPPFUEHRER,
|
||||
EinsatzFunktion.TRUPPMANN,
|
||||
];
|
||||
|
||||
function minutenSpaeter(basis: Date, minuten: number): Date {
|
||||
return new Date(basis.getTime() + minuten * 60_000);
|
||||
}
|
||||
|
||||
async function einsaetzeAnlegen(
|
||||
organisationId: string,
|
||||
fahrzeuge: Array<{ id: string; funkrufname: string }>,
|
||||
) {
|
||||
const vorhanden = await prisma.einsatz.count({ where: { organisationId } });
|
||||
if (vorhanden > 0) {
|
||||
log(`Einsätze: bereits ${vorhanden} vorhanden`);
|
||||
return;
|
||||
}
|
||||
|
||||
const personen = await prisma.person.findMany({
|
||||
where: { organisationId, aktiv: true, geloeschtAm: null },
|
||||
select: { id: true, vorname: true, nachname: true },
|
||||
orderBy: { mitgliedsnummer: "asc" },
|
||||
});
|
||||
|
||||
if (personen.length === 0 || fahrzeuge.length === 0) return;
|
||||
|
||||
let laufend = 0;
|
||||
|
||||
for (const vorlage of EINSAETZE) {
|
||||
laufend += 1;
|
||||
|
||||
const alarmiert = new Date();
|
||||
alarmiert.setUTCDate(alarmiert.getUTCDate() - vorlage.vorTagen);
|
||||
alarmiert.setUTCHours(vorlage.uhrzeit[0], vorlage.uhrzeit[1], 0, 0);
|
||||
|
||||
const jahr = alarmiert.getUTCFullYear();
|
||||
const [ausgerueckt, eingetroffen, ende, eingerueckt] = vorlage.ablauf;
|
||||
|
||||
const einsatz = await prisma.einsatz.create({
|
||||
data: {
|
||||
organisationId,
|
||||
einsatznummer: `${jahr}-${String(laufend).padStart(4, "0")}`,
|
||||
stichwort: vorlage.stichwort,
|
||||
kategorie: vorlage.kategorie,
|
||||
status: vorlage.status ?? EinsatzStatus.ABGESCHLOSSEN,
|
||||
alarmierungsart: Alarmierungsart.MELDEEMPFAENGER,
|
||||
strasse: vorlage.strasse,
|
||||
hausnummer: vorlage.hausnummer ?? null,
|
||||
plz: "84543",
|
||||
ort: "Musterhausen",
|
||||
objekt: vorlage.objekt ?? null,
|
||||
alarmiertUm: alarmiert,
|
||||
ausgeruecktUm: minutenSpaeter(alarmiert, ausgerueckt),
|
||||
eingetroffenUm: minutenSpaeter(alarmiert, eingetroffen),
|
||||
einsatzendeUm: minutenSpaeter(alarmiert, ende),
|
||||
eingeruecktUm: minutenSpaeter(alarmiert, eingerueckt),
|
||||
einsatzbereitUm: minutenSpaeter(alarmiert, eingerueckt + 30),
|
||||
einsatzleiterId: personen[0]?.id ?? null,
|
||||
beschreibung: vorlage.beschreibung,
|
||||
bericht: vorlage.bericht ?? null,
|
||||
geretteteePersonen: vorlage.gerettet ?? 0,
|
||||
verletztePersonen: vorlage.verletzt ?? 0,
|
||||
sachschaden: vorlage.sachschaden ?? null,
|
||||
fahrzeuge: {
|
||||
create: vorlage.fahrzeuge
|
||||
.map((index) => fahrzeuge[index])
|
||||
.filter((fahrzeug): fahrzeug is { id: string; funkrufname: string } => Boolean(fahrzeug))
|
||||
.map((fahrzeug, position) => ({
|
||||
organisationId,
|
||||
fahrzeugId: fahrzeug.id,
|
||||
ausgeruecktUm: minutenSpaeter(alarmiert, ausgerueckt + position),
|
||||
eingetroffenUm: minutenSpaeter(alarmiert, eingetroffen + position),
|
||||
eingeruecktUm: minutenSpaeter(alarmiert, eingerueckt),
|
||||
kilometerAbfahrt: 12000 + laufend * 40,
|
||||
kilometerRueckkehr: 12000 + laufend * 40 + 8 + position * 3,
|
||||
wasserabgabe: vorlage.kategorie.startsWith("BRAND") ? 1500 : null,
|
||||
})),
|
||||
},
|
||||
kraefte: {
|
||||
create: Array.from({ length: Math.min(vorlage.mannschaft, personen.length) }).map(
|
||||
(_, index) => ({
|
||||
organisationId,
|
||||
// Die Mannschaft rotiert, damit die Beteiligung je Person
|
||||
// unterschiedlich ausfällt.
|
||||
personId: personen[(index + laufend * 3) % personen.length]!.id,
|
||||
funktion: FUNKTIONSFOLGE[index % FUNKTIONSFOLGE.length]!,
|
||||
atemschutz: vorlage.kategorie.startsWith("BRAND") && index >= 3 && index <= 6,
|
||||
vonUm: minutenSpaeter(alarmiert, ausgerueckt),
|
||||
bisUm: minutenSpaeter(alarmiert, eingerueckt),
|
||||
}),
|
||||
),
|
||||
},
|
||||
lagemeldungen: {
|
||||
create: (vorlage.meldungen ?? []).map((meldung) => ({
|
||||
organisationId,
|
||||
zeitpunkt: minutenSpaeter(alarmiert, meldung.nachMinuten),
|
||||
text: meldung.text,
|
||||
wichtig: meldung.wichtig ?? false,
|
||||
verfasserId: personen[0]?.id ?? null,
|
||||
verfasserName: personen[0]
|
||||
? `${personen[0].vorname} ${personen[0].nachname}`
|
||||
: "Einsatzleitung",
|
||||
})),
|
||||
},
|
||||
material: {
|
||||
create: (vorlage.material ?? []).map((posten) => ({
|
||||
organisationId,
|
||||
bezeichnung: posten.bezeichnung,
|
||||
menge: posten.menge,
|
||||
einheit: posten.einheit,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Der älteste Einsatz ist bereits freigegeben — damit die Ansicht eines
|
||||
// festgeschriebenen Berichts nicht leer bleibt.
|
||||
if (vorlage.vorTagen >= 140) {
|
||||
await prisma.einsatz.update({
|
||||
where: { id: einsatz.id },
|
||||
data: {
|
||||
status: EinsatzStatus.FREIGEGEBEN,
|
||||
freigegebenAm: minutenSpaeter(alarmiert, 2880),
|
||||
freigegebenVonId: personen[0]?.id ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
log(`Einsätze: ${EINSAETZE.length} mit Fahrzeugen, Mannschaft und Lagemeldungen`);
|
||||
}
|
||||
|
||||
interface UebungVorlage {
|
||||
vorTagen: number;
|
||||
thema: string;
|
||||
art: UebungsArt;
|
||||
ort: string;
|
||||
lernziele: string;
|
||||
teilnehmer: number;
|
||||
dauer: number;
|
||||
}
|
||||
|
||||
const UEBUNGEN: UebungVorlage[] = [
|
||||
{
|
||||
vorTagen: 7,
|
||||
thema: "Innenangriff mit Atemschutz",
|
||||
art: UebungsArt.ATEMSCHUTZ,
|
||||
ort: "Übungscontainer Nachbarhausen",
|
||||
lernziele:
|
||||
"Vorgehen im Trupp unter Nullsicht, Türöffnungsprozedur, Rückweg sichern, Kommunikation mit dem Gruppenführer.",
|
||||
teilnehmer: 9,
|
||||
dauer: 150,
|
||||
},
|
||||
{
|
||||
vorTagen: 21,
|
||||
thema: "Wasserförderung über lange Wegstrecke",
|
||||
art: UebungsArt.PRAXIS,
|
||||
ort: "Löschwasserentnahmestelle Mühlbach",
|
||||
lernziele: "Aufbau einer B-Leitung über 600 m, Verstärkerpumpe setzen, Druckverluste erkennen.",
|
||||
teilnehmer: 12,
|
||||
dauer: 120,
|
||||
},
|
||||
{
|
||||
vorTagen: 35,
|
||||
thema: "Technische Hilfeleistung nach Verkehrsunfall",
|
||||
art: UebungsArt.PRAXIS,
|
||||
ort: "Übungsgelände am Bauhof",
|
||||
lernziele:
|
||||
"Fahrzeug sichern, Glasmanagement, patientengerechte Rettung, Zusammenarbeit mit dem Rettungsdienst.",
|
||||
teilnehmer: 11,
|
||||
dauer: 180,
|
||||
},
|
||||
{
|
||||
vorTagen: 49,
|
||||
thema: "Funkausbildung nach BOS-Richtlinie",
|
||||
art: UebungsArt.FUNK,
|
||||
ort: "Gerätehaus, Schulungsraum",
|
||||
lernziele: "Funkdisziplin, Meldeschema, Umgang mit dem Handsprechfunkgerät.",
|
||||
teilnehmer: 8,
|
||||
dauer: 90,
|
||||
},
|
||||
{
|
||||
vorTagen: 63,
|
||||
thema: "Maschinistenausbildung Tragkraftspritze",
|
||||
art: UebungsArt.MASCHINIST,
|
||||
ort: "Gerätehaus",
|
||||
lernziele: "Inbetriebnahme, Entlüften, Störungssuche, Wartung nach dem Einsatz.",
|
||||
teilnehmer: 5,
|
||||
dauer: 120,
|
||||
},
|
||||
{
|
||||
vorTagen: 84,
|
||||
thema: "Gemeinschaftsübung mit der FF Nachbarhausen",
|
||||
art: UebungsArt.GEMEINSCHAFTSUEBUNG,
|
||||
ort: "Sägewerk Nachbarhausen",
|
||||
lernziele:
|
||||
"Zusammenarbeit zweier Wehren, gemeinsame Einsatzleitung, Wasserversorgung aus offenem Gewässer.",
|
||||
teilnehmer: 14,
|
||||
dauer: 210,
|
||||
},
|
||||
];
|
||||
|
||||
async function uebungenAnlegen(organisationId: string) {
|
||||
const vorhanden = await prisma.uebung.count({ where: { organisationId } });
|
||||
if (vorhanden > 0) {
|
||||
log(`Übungen: bereits ${vorhanden} vorhanden`);
|
||||
return;
|
||||
}
|
||||
|
||||
const personen = await prisma.person.findMany({
|
||||
where: { organisationId, aktiv: true, geloeschtAm: null },
|
||||
select: { id: true },
|
||||
orderBy: { mitgliedsnummer: "asc" },
|
||||
});
|
||||
|
||||
if (personen.length === 0) return;
|
||||
|
||||
let laufend = 0;
|
||||
|
||||
for (const vorlage of UEBUNGEN) {
|
||||
laufend += 1;
|
||||
|
||||
const datum = new Date();
|
||||
datum.setUTCDate(datum.getUTCDate() - vorlage.vorTagen);
|
||||
datum.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
const beginn = new Date(datum);
|
||||
beginn.setUTCHours(19, 0, 0, 0);
|
||||
|
||||
await prisma.uebung.create({
|
||||
data: {
|
||||
organisationId,
|
||||
nummer: `${datum.getUTCFullYear()}-${String(laufend).padStart(4, "0")}`,
|
||||
thema: vorlage.thema,
|
||||
art: vorlage.art,
|
||||
datum,
|
||||
beginnUm: beginn,
|
||||
endeUm: minutenSpaeter(beginn, vorlage.dauer),
|
||||
ort: vorlage.ort,
|
||||
leiterId: personen[0]?.id ?? null,
|
||||
lernziele: vorlage.lernziele,
|
||||
teilnahmen: {
|
||||
create: Array.from({ length: Math.min(vorlage.teilnehmer, personen.length) }).map(
|
||||
(_, index) => ({
|
||||
organisationId,
|
||||
personId: personen[(index + laufend * 2) % personen.length]!.id,
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
log(`Übungen: ${UEBUNGEN.length} mit Teilnahmen`);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Ablauf
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -1539,6 +1976,10 @@ async function main(): Promise<void> {
|
||||
await beispielPruefungAnlegen(organisation.id, wartungsvorlagen);
|
||||
await rueckrufAnlegen(organisation.id);
|
||||
|
||||
// Phase 3 — Einsätze und Übungen
|
||||
await einsaetzeAnlegen(organisation.id, fahrzeuge);
|
||||
await uebungenAnlegen(organisation.id);
|
||||
|
||||
log("Fertig.");
|
||||
log("");
|
||||
log("Anmeldung für den Erststart:");
|
||||
|
||||
Reference in New Issue
Block a user