add features
This commit is contained in:
146
backend/src/database/migrations/002_create_audit_log.sql
Normal file
146
backend/src/database/migrations/002_create_audit_log.sql
Normal file
@@ -0,0 +1,146 @@
|
||||
-- =============================================================================
|
||||
-- Migration 002: Audit Log Table
|
||||
-- GDPR Art. 5(2) Accountability + Art. 30 Records of Processing Activities
|
||||
--
|
||||
-- Design decisions:
|
||||
-- - UUID primary key (consistent with users table)
|
||||
-- - user_id is nullable: pre-auth events (LOGIN failures) have no user yet
|
||||
-- - old_value / new_value are JSONB: flexible, queryable, indexable
|
||||
-- - ip_address stored as TEXT (supports IPv4 + IPv6); anonymised after 90 days
|
||||
-- - Table is immutable: a PostgreSQL RULE blocks UPDATE and DELETE at the
|
||||
-- SQL level, which is simpler than triggers and cannot be bypassed by the
|
||||
-- application role
|
||||
-- - Partial index on ip_address covers only recent rows (90-day window) to
|
||||
-- support efficient anonymisation queries without a full-table scan
|
||||
-- =============================================================================
|
||||
|
||||
-- -------------------------------------------------------
|
||||
-- 1. Enums — define before the table
|
||||
-- -------------------------------------------------------
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'audit_action') THEN
|
||||
CREATE TYPE audit_action AS ENUM (
|
||||
'CREATE',
|
||||
'UPDATE',
|
||||
'DELETE',
|
||||
'LOGIN',
|
||||
'LOGOUT',
|
||||
'EXPORT',
|
||||
'PERMISSION_DENIED',
|
||||
'PASSWORD_CHANGE',
|
||||
'ROLE_CHANGE'
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'audit_resource_type') THEN
|
||||
CREATE TYPE audit_resource_type AS ENUM (
|
||||
'MEMBER',
|
||||
'INCIDENT',
|
||||
'VEHICLE',
|
||||
'EQUIPMENT',
|
||||
'QUALIFICATION',
|
||||
'USER',
|
||||
'SYSTEM'
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- -------------------------------------------------------
|
||||
-- 2. Core audit_log table
|
||||
-- -------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
user_email VARCHAR(255), -- denormalised snapshot; users can be deleted
|
||||
action audit_action NOT NULL,
|
||||
resource_type audit_resource_type NOT NULL,
|
||||
resource_id VARCHAR(255), -- may be UUID, numeric ID, or 'system'
|
||||
old_value JSONB, -- state before the operation
|
||||
new_value JSONB, -- state after the operation
|
||||
ip_address TEXT, -- anonymised to '[anonymized]' after 90 days
|
||||
user_agent TEXT,
|
||||
metadata JSONB DEFAULT '{}', -- any extra context (e.g. export format, reason)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Prevent modification of audit records at the database level.
|
||||
-- Using RULE rather than a trigger because rules are evaluated before
|
||||
-- the operation and cannot be disabled without superuser access.
|
||||
CREATE OR REPLACE RULE audit_log_no_update AS
|
||||
ON UPDATE TO audit_log DO INSTEAD NOTHING;
|
||||
|
||||
CREATE OR REPLACE RULE audit_log_no_delete AS
|
||||
ON DELETE TO audit_log DO INSTEAD NOTHING;
|
||||
|
||||
-- -------------------------------------------------------
|
||||
-- 3. Indexes
|
||||
-- -------------------------------------------------------
|
||||
|
||||
-- Lookup by actor (admin "what did this user do?")
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_user_id
|
||||
ON audit_log (user_id)
|
||||
WHERE user_id IS NOT NULL;
|
||||
|
||||
-- Lookup by subject resource (admin "what happened to member X?")
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_resource
|
||||
ON audit_log (resource_type, resource_id);
|
||||
|
||||
-- Time-range queries and retention scans (most common filter)
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_created_at
|
||||
ON audit_log (created_at DESC);
|
||||
|
||||
-- Action-type filter
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_action
|
||||
ON audit_log (action);
|
||||
|
||||
-- Partial index: only rows where IP address is not yet anonymised.
|
||||
-- The anonymisation job does: WHERE created_at < NOW() - INTERVAL '90 days'
|
||||
-- AND ip_address != '[anonymized]'
|
||||
-- This index makes that query O(matched rows) instead of O(table size).
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_ip_retention
|
||||
ON audit_log (created_at)
|
||||
WHERE ip_address IS NOT NULL
|
||||
AND ip_address != '[anonymized]';
|
||||
|
||||
-- -------------------------------------------------------
|
||||
-- 4. OPTIONAL — Range partitioning by month (recommended
|
||||
-- for high-volume deployments; skip in small setups)
|
||||
--
|
||||
-- To use partitioning, replace the CREATE TABLE above with
|
||||
-- the following DDL *before* the first row is inserted.
|
||||
-- Partitioning cannot be added to an existing unpartitioned
|
||||
-- table without a full rewrite (pg_partman can automate this).
|
||||
--
|
||||
-- CREATE TABLE audit_log (
|
||||
-- id UUID NOT NULL DEFAULT uuid_generate_v4(),
|
||||
-- user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
-- user_email VARCHAR(255),
|
||||
-- action audit_action NOT NULL,
|
||||
-- resource_type audit_resource_type NOT NULL,
|
||||
-- resource_id VARCHAR(255),
|
||||
-- old_value JSONB,
|
||||
-- new_value JSONB,
|
||||
-- ip_address TEXT,
|
||||
-- user_agent TEXT,
|
||||
-- metadata JSONB DEFAULT '{}',
|
||||
-- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
-- ) PARTITION BY RANGE (created_at);
|
||||
--
|
||||
-- -- Create monthly partitions with pg_partman or manually:
|
||||
-- CREATE TABLE audit_log_2026_01 PARTITION OF audit_log
|
||||
-- FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
|
||||
-- CREATE TABLE audit_log_2026_02 PARTITION OF audit_log
|
||||
-- FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
|
||||
-- -- ... and so on. pg_partman automates partition creation.
|
||||
--
|
||||
-- With partitioning, old partitions can be DROPped for
|
||||
-- efficient bulk retention deletion without a full-table scan.
|
||||
-- -------------------------------------------------------
|
||||
@@ -0,0 +1,138 @@
|
||||
-- Migration: 003_create_mitglieder_profile
|
||||
-- Creates the mitglieder_profile and dienstgrad_verlauf tables for fire department member data.
|
||||
-- Rollback:
|
||||
-- DROP TABLE IF EXISTS dienstgrad_verlauf;
|
||||
-- DROP TABLE IF EXISTS mitglieder_profile;
|
||||
|
||||
-- ============================================================
|
||||
-- mitglieder_profile
|
||||
-- One-to-one extension of the users table.
|
||||
-- A user can exist without a profile (profile is created later
|
||||
-- by a Kommandant). The user_id is both FK and UNIQUE.
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS mitglieder_profile (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Internal member identifier assigned by the Kommandant
|
||||
mitglieds_nr VARCHAR(32) UNIQUE,
|
||||
|
||||
-- Rank (Dienstgrad) with allowed values enforced by CHECK
|
||||
dienstgrad VARCHAR(64)
|
||||
CHECK (dienstgrad IS NULL OR dienstgrad IN (
|
||||
'Feuerwehranwärter',
|
||||
'Feuerwehrmann',
|
||||
'Feuerwehrfrau',
|
||||
'Oberfeuerwehrmann',
|
||||
'Oberfeuerwehrfrau',
|
||||
'Hauptfeuerwehrmann',
|
||||
'Hauptfeuerwehrfrau',
|
||||
'Löschmeister',
|
||||
'Oberlöschmeister',
|
||||
'Hauptlöschmeister',
|
||||
'Brandmeister',
|
||||
'Oberbrandmeister',
|
||||
'Hauptbrandmeister',
|
||||
'Brandinspektor',
|
||||
'Oberbrandinspektor',
|
||||
'Brandoberinspektor',
|
||||
'Brandamtmann'
|
||||
)),
|
||||
dienstgrad_seit DATE,
|
||||
|
||||
-- Funktion(en) — a member can hold multiple roles simultaneously
|
||||
-- Stored as a PostgreSQL TEXT array
|
||||
funktion TEXT[] DEFAULT '{}',
|
||||
|
||||
-- Membership status
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'aktiv'
|
||||
CHECK (status IN (
|
||||
'aktiv',
|
||||
'passiv',
|
||||
'ehrenmitglied',
|
||||
'jugendfeuerwehr',
|
||||
'anwärter',
|
||||
'ausgetreten'
|
||||
)),
|
||||
|
||||
-- Important dates
|
||||
eintrittsdatum DATE,
|
||||
austrittsdatum DATE,
|
||||
geburtsdatum DATE, -- sensitive: only shown to Kommandant/Admin
|
||||
|
||||
-- Contact information (stored raw, formatted on display)
|
||||
telefon_mobil VARCHAR(32),
|
||||
telefon_privat VARCHAR(32),
|
||||
|
||||
-- Emergency contact (sensitive: only own record visible to Mitglied)
|
||||
notfallkontakt_name VARCHAR(255),
|
||||
notfallkontakt_telefon VARCHAR(32),
|
||||
|
||||
-- Driving licenses (e.g. ['B', 'C', 'CE'])
|
||||
fuehrerscheinklassen TEXT[] DEFAULT '{}',
|
||||
|
||||
-- Uniform sizing
|
||||
tshirt_groesse VARCHAR(8)
|
||||
CHECK (tshirt_groesse IS NULL OR tshirt_groesse IN (
|
||||
'XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL'
|
||||
)),
|
||||
schuhgroesse VARCHAR(8),
|
||||
|
||||
-- Free-text notes (Kommandant only)
|
||||
bemerkungen TEXT,
|
||||
|
||||
-- Profile photo URL (separate from the Authentik profile_picture_url)
|
||||
bild_url TEXT,
|
||||
|
||||
-- Audit timestamps
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
-- Enforce one-to-one relationship with users
|
||||
CONSTRAINT uq_mitglieder_profile_user_id UNIQUE (user_id)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Indexes for the most common query patterns
|
||||
-- ============================================================
|
||||
CREATE INDEX IF NOT EXISTS idx_mitglieder_profile_user_id
|
||||
ON mitglieder_profile(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mitglieder_profile_status
|
||||
ON mitglieder_profile(status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mitglieder_profile_dienstgrad
|
||||
ON mitglieder_profile(dienstgrad);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mitglieder_profile_mitglieds_nr
|
||||
ON mitglieder_profile(mitglieds_nr);
|
||||
|
||||
-- ============================================================
|
||||
-- Auto-update trigger for updated_at
|
||||
-- Reuses the function already created by migration 001.
|
||||
-- ============================================================
|
||||
CREATE TRIGGER update_mitglieder_profile_updated_at
|
||||
BEFORE UPDATE ON mitglieder_profile
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- ============================================================
|
||||
-- dienstgrad_verlauf
|
||||
-- Append-only audit log of every rank change.
|
||||
-- Never deleted; soft history only.
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS dienstgrad_verlauf (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
dienstgrad_neu VARCHAR(64) NOT NULL,
|
||||
dienstgrad_alt VARCHAR(64), -- NULL on first assignment
|
||||
datum DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
durch_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
bemerkung TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_dienstgrad_verlauf_user_id
|
||||
ON dienstgrad_verlauf(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_dienstgrad_verlauf_datum
|
||||
ON dienstgrad_verlauf(datum DESC);
|
||||
270
backend/src/database/migrations/004_create_einsaetze.sql
Normal file
270
backend/src/database/migrations/004_create_einsaetze.sql
Normal file
@@ -0,0 +1,270 @@
|
||||
-- Migration: 004_create_einsaetze.sql
|
||||
-- Feature: Einsatzprotokoll (Incident Management)
|
||||
-- Depends on: 001_create_users_table.sql
|
||||
-- Rollback instructions at bottom of file
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- ENUM-LIKE CHECK CONSTRAINTS (as VARCHAR + CHECK for easy extension)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- SEQUENCE SUPPORT TABLE for year-based Einsatz-Nr (YYYY-NNN)
|
||||
-- One row per year; nextval equivalent done via UPDATE...RETURNING
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS einsatz_nr_sequence (
|
||||
year INTEGER PRIMARY KEY,
|
||||
last_nr INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- FAHRZEUGE TABLE (vehicles — stub so junction table has a valid FK target)
|
||||
-- A full Fahrzeuge feature is covered by 005_create_fahrzeuge.sql.
|
||||
-- We do NOT create the table here; migration 005 owns it.
|
||||
-- The FK in einsatz_fahrzeuge will resolve correctly when 005 runs first
|
||||
-- in the standard sort order (004 runs before 005, so we use DEFERRABLE or
|
||||
-- we simply add the FK as a separate ALTER TABLE at the end of this file,
|
||||
-- after we know 005 may not have run yet).
|
||||
--
|
||||
-- Resolution strategy: einsatz_fahrzeuge.fahrzeug_id FK is defined as
|
||||
-- DEFERRABLE INITIALLY DEFERRED so it is checked at transaction commit time
|
||||
-- rather than at statement time, allowing this migration to run without
|
||||
-- requiring fahrzeuge to exist yet.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- MAIN EINSAETZE TABLE
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS einsaetze (
|
||||
-- Identity
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
einsatz_nr VARCHAR(12) NOT NULL, -- Format: 2026-001
|
||||
|
||||
-- Core timestamps (all TIMESTAMPTZ — never null for alarm_time)
|
||||
alarm_time TIMESTAMPTZ NOT NULL,
|
||||
ausrueck_time TIMESTAMPTZ,
|
||||
ankunft_time TIMESTAMPTZ,
|
||||
einrueck_time TIMESTAMPTZ,
|
||||
|
||||
-- Classification
|
||||
einsatz_art VARCHAR(30) NOT NULL
|
||||
CONSTRAINT chk_einsatz_art CHECK (einsatz_art IN (
|
||||
'Brand',
|
||||
'THL',
|
||||
'ABC',
|
||||
'BMA',
|
||||
'Hilfeleistung',
|
||||
'Fehlalarm',
|
||||
'Brandsicherheitswache'
|
||||
)),
|
||||
einsatz_stichwort VARCHAR(30), -- B1/B2/B3/B4, THL 1/2/3, etc.
|
||||
|
||||
-- Location
|
||||
strasse VARCHAR(150),
|
||||
hausnummer VARCHAR(20),
|
||||
ort VARCHAR(100),
|
||||
koordinaten POINT, -- (longitude, latitude)
|
||||
|
||||
-- Narrative
|
||||
bericht_kurz VARCHAR(255), -- short description, visible to all
|
||||
bericht_text TEXT, -- full narrative, restricted to Kommandant+
|
||||
|
||||
-- References
|
||||
einsatzleiter_id UUID
|
||||
CONSTRAINT fk_einsaetze_einsatzleiter REFERENCES users(id) ON DELETE SET NULL,
|
||||
|
||||
-- Operational metadata
|
||||
alarmierung_art VARCHAR(30) NOT NULL DEFAULT 'ILS'
|
||||
CONSTRAINT chk_alarmierung_art CHECK (alarmierung_art IN (
|
||||
'ILS',
|
||||
'DME',
|
||||
'Telefon',
|
||||
'Vor_Ort',
|
||||
'Sonstiges'
|
||||
)),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'aktiv'
|
||||
CONSTRAINT chk_einsatz_status CHECK (status IN (
|
||||
'aktiv',
|
||||
'abgeschlossen',
|
||||
'archiviert'
|
||||
)),
|
||||
|
||||
-- Audit
|
||||
created_by UUID
|
||||
CONSTRAINT fk_einsaetze_created_by REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Uniqueness: einsatz_nr is globally unique (year+seq guarantees it)
|
||||
CONSTRAINT uq_einsatz_nr UNIQUE (einsatz_nr)
|
||||
);
|
||||
|
||||
-- Performance indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_einsaetze_alarm_time ON einsaetze(alarm_time DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_einsaetze_einsatz_art ON einsaetze(einsatz_art);
|
||||
CREATE INDEX IF NOT EXISTS idx_einsaetze_status ON einsaetze(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_einsaetze_einsatzleiter ON einsaetze(einsatzleiter_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_einsaetze_alarm_year ON einsaetze(EXTRACT(YEAR FROM alarm_time));
|
||||
CREATE INDEX IF NOT EXISTS idx_einsaetze_alarm_art_year ON einsaetze(einsatz_art, EXTRACT(YEAR FROM alarm_time));
|
||||
|
||||
-- Auto-update updated_at
|
||||
CREATE TRIGGER update_einsaetze_updated_at
|
||||
BEFORE UPDATE ON einsaetze
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- JUNCTION TABLE: einsatz_fahrzeuge
|
||||
-- NOTE: fahrzeug_id FK references fahrzeuge which is created in 005.
|
||||
-- We define the FK as DEFERRABLE INITIALLY DEFERRED so the constraint is
|
||||
-- validated at transaction commit, not statement time. This means the
|
||||
-- migration can run before 005 as long as both run in the same session.
|
||||
-- In production where 005 was already run, the FK resolves immediately.
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS einsatz_fahrzeuge (
|
||||
einsatz_id UUID NOT NULL
|
||||
CONSTRAINT fk_ef_einsatz REFERENCES einsaetze(id) ON DELETE CASCADE,
|
||||
fahrzeug_id UUID NOT NULL,
|
||||
-- FK added separately below via ALTER TABLE to handle cross-migration dependency
|
||||
|
||||
-- Vehicle-level timestamps (may differ from main einsatz times)
|
||||
ausrueck_time TIMESTAMPTZ,
|
||||
einrueck_time TIMESTAMPTZ,
|
||||
|
||||
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT pk_einsatz_fahrzeuge PRIMARY KEY (einsatz_id, fahrzeug_id)
|
||||
);
|
||||
|
||||
-- Add the FK to fahrzeuge only if the fahrzeuge table already exists
|
||||
-- (handles the case where 004 runs after 005 in a fresh deployment).
|
||||
-- If fahrzeuge does not exist yet, the FK will be added by migration 005
|
||||
-- via an ALTER TABLE at the end of that migration.
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'fahrzeuge'
|
||||
) THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_name = 'einsatz_fahrzeuge'
|
||||
AND constraint_name = 'fk_ef_fahrzeug'
|
||||
) THEN
|
||||
ALTER TABLE einsatz_fahrzeuge
|
||||
ADD CONSTRAINT fk_ef_fahrzeug
|
||||
FOREIGN KEY (fahrzeug_id) REFERENCES fahrzeuge(id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ef_fahrzeug_id ON einsatz_fahrzeuge(fahrzeug_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- JUNCTION TABLE: einsatz_personal
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS einsatz_personal (
|
||||
einsatz_id UUID NOT NULL
|
||||
CONSTRAINT fk_ep_einsatz REFERENCES einsaetze(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL
|
||||
CONSTRAINT fk_ep_user REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
funktion VARCHAR(50) NOT NULL DEFAULT 'Mannschaft'
|
||||
CONSTRAINT chk_ep_funktion CHECK (funktion IN (
|
||||
'Einsatzleiter',
|
||||
'Gruppenführer',
|
||||
'Maschinist',
|
||||
'Atemschutz',
|
||||
'Sicherheitstrupp',
|
||||
'Melder',
|
||||
'Wassertrupp',
|
||||
'Angriffstrupp',
|
||||
'Mannschaft',
|
||||
'Sonstiges'
|
||||
)),
|
||||
|
||||
-- Personal-level timestamps
|
||||
alarm_time TIMESTAMPTZ,
|
||||
ankunft_time TIMESTAMPTZ,
|
||||
|
||||
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT pk_einsatz_personal PRIMARY KEY (einsatz_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ep_user_id ON einsatz_personal(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ep_funktion ON einsatz_personal(funktion);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- FUNCTION: generate_einsatz_nr(alarm_time)
|
||||
-- Atomically generates the next Einsatz-Nr for a given year.
|
||||
-- Uses an advisory lock to serialise concurrent inserts for the same year.
|
||||
-- Returns format: '2026-001'
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION generate_einsatz_nr(p_alarm_time TIMESTAMPTZ)
|
||||
RETURNS VARCHAR AS $$
|
||||
DECLARE
|
||||
v_year INTEGER;
|
||||
v_nr INTEGER;
|
||||
BEGIN
|
||||
v_year := EXTRACT(YEAR FROM p_alarm_time)::INTEGER;
|
||||
|
||||
-- Upsert the sequence row, then atomically increment and return
|
||||
INSERT INTO einsatz_nr_sequence (year, last_nr)
|
||||
VALUES (v_year, 1)
|
||||
ON CONFLICT (year) DO UPDATE
|
||||
SET last_nr = einsatz_nr_sequence.last_nr + 1
|
||||
RETURNING last_nr INTO v_nr;
|
||||
|
||||
RETURN v_year::TEXT || '-' || LPAD(v_nr::TEXT, 3, '0');
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- MATERIALIZED VIEW: einsatz_statistik
|
||||
-- Pre-aggregated stats used by the dashboard and annual KBI reports.
|
||||
-- Refresh manually after inserts/updates via: REFRESH MATERIALIZED VIEW einsatz_statistik;
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS einsatz_statistik AS
|
||||
SELECT
|
||||
EXTRACT(YEAR FROM alarm_time)::INTEGER AS jahr,
|
||||
EXTRACT(MONTH FROM alarm_time)::INTEGER AS monat,
|
||||
einsatz_art,
|
||||
COUNT(*) AS anzahl,
|
||||
-- Hilfsfrist: median minutes from alarm to arrival (only where ankunft_time is set)
|
||||
ROUND(
|
||||
AVG(
|
||||
EXTRACT(EPOCH FROM (ankunft_time - alarm_time)) / 60.0
|
||||
) FILTER (WHERE ankunft_time IS NOT NULL)
|
||||
)::INTEGER AS avg_hilfsfrist_min,
|
||||
-- Total duration (alarm to einrueck)
|
||||
ROUND(
|
||||
AVG(
|
||||
EXTRACT(EPOCH FROM (einrueck_time - alarm_time)) / 60.0
|
||||
) FILTER (WHERE einrueck_time IS NOT NULL)
|
||||
)::INTEGER AS avg_dauer_min,
|
||||
COUNT(*) FILTER (WHERE status = 'abgeschlossen') AS anzahl_abgeschlossen,
|
||||
COUNT(*) FILTER (WHERE status = 'aktiv') AS anzahl_aktiv
|
||||
FROM einsaetze
|
||||
WHERE status != 'archiviert'
|
||||
GROUP BY
|
||||
EXTRACT(YEAR FROM alarm_time),
|
||||
EXTRACT(MONTH FROM alarm_time),
|
||||
einsatz_art
|
||||
WITH DATA;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_einsatz_statistik_pk
|
||||
ON einsatz_statistik(jahr, monat, einsatz_art);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_einsatz_statistik_jahr
|
||||
ON einsatz_statistik(jahr);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- ROLLBACK INSTRUCTIONS (run in reverse order to undo):
|
||||
--
|
||||
-- DROP MATERIALIZED VIEW IF EXISTS einsatz_statistik;
|
||||
-- DROP FUNCTION IF EXISTS generate_einsatz_nr(TIMESTAMPTZ);
|
||||
-- DROP TABLE IF EXISTS einsatz_personal;
|
||||
-- DROP TABLE IF EXISTS einsatz_fahrzeuge;
|
||||
-- DROP TABLE IF EXISTS einsaetze;
|
||||
-- DROP TABLE IF EXISTS fahrzeuge;
|
||||
-- DROP TABLE IF EXISTS einsatz_nr_sequence;
|
||||
-- ---------------------------------------------------------------------------
|
||||
331
backend/src/database/migrations/005_create_fahrzeuge.sql
Normal file
331
backend/src/database/migrations/005_create_fahrzeuge.sql
Normal file
@@ -0,0 +1,331 @@
|
||||
-- Migration 005: Fahrzeugverwaltung (Vehicle Fleet Management)
|
||||
-- Depends on: 001_create_users_table.sql (uuid-ossp extension, users table)
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: fahrzeuge
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS fahrzeuge (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
bezeichnung VARCHAR(100) NOT NULL, -- e.g. "LF 20/16", "HLF 10"
|
||||
kurzname VARCHAR(20), -- e.g. "LF1", "HLF2"
|
||||
amtliches_kennzeichen VARCHAR(20) UNIQUE, -- e.g. "WN-FW 1"
|
||||
fahrgestellnummer VARCHAR(50), -- VIN
|
||||
baujahr INTEGER CHECK (baujahr >= 1950 AND baujahr <= 2100),
|
||||
hersteller VARCHAR(100), -- e.g. "MAN", "Mercedes-Benz", "Rosenbauer"
|
||||
typ_schluessel VARCHAR(30), -- DIN 14502 code, e.g. "LF 20/16"
|
||||
besatzung_soll VARCHAR(10), -- crew config e.g. "1/8", "1/5"
|
||||
status VARCHAR(40) NOT NULL DEFAULT 'einsatzbereit'
|
||||
CHECK (status IN (
|
||||
'einsatzbereit',
|
||||
'ausser_dienst_wartung',
|
||||
'ausser_dienst_schaden',
|
||||
'in_lehrgang'
|
||||
)),
|
||||
status_bemerkung TEXT,
|
||||
standort VARCHAR(100) NOT NULL DEFAULT 'Feuerwehrhaus',
|
||||
bild_url VARCHAR(500),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_fahrzeuge_status ON fahrzeuge(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_fahrzeuge_kennzeichen ON fahrzeuge(amtliches_kennzeichen);
|
||||
|
||||
-- Auto-update updated_at (reuses function from migration 001)
|
||||
CREATE TRIGGER update_fahrzeuge_updated_at
|
||||
BEFORE UPDATE ON fahrzeuge
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: fahrzeug_pruefungen
|
||||
-- Stores both upcoming scheduled inspections AND completed ones.
|
||||
-- A row with durchgefuehrt_am = NULL is an open/scheduled inspection.
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS fahrzeug_pruefungen (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
fahrzeug_id UUID NOT NULL REFERENCES fahrzeuge(id) ON DELETE CASCADE,
|
||||
pruefung_art VARCHAR(30) NOT NULL
|
||||
CHECK (pruefung_art IN (
|
||||
'HU', -- Hauptuntersuchung (TÜV), 24-month interval
|
||||
'AU', -- Abgasuntersuchung, 12-month interval
|
||||
'UVV', -- Unfallverhütungsvorschrift BGV D29, 12-month
|
||||
'Leiter', -- Leiternprüfung (DLK), 12-month
|
||||
'Kran', -- Kranprüfung, 12-month
|
||||
'Seilwinde', -- Seilwindenprüfung, 12-month
|
||||
'Sonstiges'
|
||||
)),
|
||||
-- faellig_am: the deadline by which this inspection must be completed
|
||||
faellig_am DATE NOT NULL,
|
||||
-- durchgefuehrt_am: NULL = not yet done; set when inspection is completed
|
||||
durchgefuehrt_am DATE,
|
||||
ergebnis VARCHAR(30)
|
||||
CHECK (ergebnis IS NULL OR ergebnis IN (
|
||||
'bestanden',
|
||||
'bestanden_mit_maengeln',
|
||||
'nicht_bestanden',
|
||||
'ausstehend'
|
||||
)),
|
||||
-- naechste_faelligkeit: auto-calculated next due date after completion
|
||||
naechste_faelligkeit DATE,
|
||||
pruefende_stelle VARCHAR(150), -- e.g. "TÜV Süd Stuttgart", "DEKRA"
|
||||
kosten DECIMAL(8,2),
|
||||
dokument_url VARCHAR(500),
|
||||
bemerkung TEXT,
|
||||
erfasst_von UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pruefungen_fahrzeug_id ON fahrzeug_pruefungen(fahrzeug_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pruefungen_faellig_am ON fahrzeug_pruefungen(faellig_am);
|
||||
CREATE INDEX IF NOT EXISTS idx_pruefungen_art ON fahrzeug_pruefungen(pruefung_art);
|
||||
-- Composite index for the "latest per type" query pattern
|
||||
CREATE INDEX IF NOT EXISTS idx_pruefungen_fahrzeug_art_faellig
|
||||
ON fahrzeug_pruefungen(fahrzeug_id, pruefung_art, faellig_am DESC);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: fahrzeug_wartungslog
|
||||
-- Service/maintenance log entries (fuel, repairs, tyres, etc.)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS fahrzeug_wartungslog (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
fahrzeug_id UUID NOT NULL REFERENCES fahrzeuge(id) ON DELETE CASCADE,
|
||||
datum DATE NOT NULL,
|
||||
art VARCHAR(30)
|
||||
CHECK (art IS NULL OR art IN (
|
||||
'Inspektion',
|
||||
'Reparatur',
|
||||
'Kraftstoff',
|
||||
'Reifenwechsel',
|
||||
'Hauptuntersuchung',
|
||||
'Reinigung',
|
||||
'Sonstiges'
|
||||
)),
|
||||
beschreibung TEXT NOT NULL,
|
||||
km_stand INTEGER CHECK (km_stand >= 0),
|
||||
kraftstoff_liter DECIMAL(6,2) CHECK (kraftstoff_liter >= 0),
|
||||
kosten DECIMAL(8,2) CHECK (kosten >= 0),
|
||||
externe_werkstatt VARCHAR(150),
|
||||
erfasst_von UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_wartungslog_fahrzeug_id ON fahrzeug_wartungslog(fahrzeug_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_wartungslog_datum ON fahrzeug_wartungslog(datum DESC);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- VIEW: fahrzeuge_mit_pruefstatus
|
||||
-- For each vehicle, joins its LATEST scheduled pruefung per art
|
||||
-- and computes tage_bis_faelligkeit (negative = overdue).
|
||||
-- The dashboard alert panel and fleet overview query this view.
|
||||
-- ============================================================
|
||||
CREATE OR REPLACE VIEW fahrzeuge_mit_pruefstatus AS
|
||||
WITH latest_pruefungen AS (
|
||||
-- For each (fahrzeug, pruefung_art) pair, pick the row with the
|
||||
-- latest faellig_am that is NOT yet completed (durchgefuehrt_am IS NULL),
|
||||
-- OR if all are completed, the one with the highest naechste_faelligkeit.
|
||||
SELECT DISTINCT ON (fahrzeug_id, pruefung_art)
|
||||
fahrzeug_id,
|
||||
pruefung_art,
|
||||
id AS pruefung_id,
|
||||
faellig_am,
|
||||
durchgefuehrt_am,
|
||||
ergebnis,
|
||||
naechste_faelligkeit,
|
||||
pruefende_stelle,
|
||||
CURRENT_DATE - faellig_am::date AS tage_ueberfaellig,
|
||||
faellig_am::date - CURRENT_DATE AS tage_bis_faelligkeit
|
||||
FROM fahrzeug_pruefungen
|
||||
ORDER BY
|
||||
fahrzeug_id,
|
||||
pruefung_art,
|
||||
-- Open inspections (nicht durchgeführt) first, then most recent
|
||||
(durchgefuehrt_am IS NULL) DESC,
|
||||
faellig_am DESC
|
||||
)
|
||||
SELECT
|
||||
f.id,
|
||||
f.bezeichnung,
|
||||
f.kurzname,
|
||||
f.amtliches_kennzeichen,
|
||||
f.fahrgestellnummer,
|
||||
f.baujahr,
|
||||
f.hersteller,
|
||||
f.typ_schluessel,
|
||||
f.besatzung_soll,
|
||||
f.status,
|
||||
f.status_bemerkung,
|
||||
f.standort,
|
||||
f.bild_url,
|
||||
f.created_at,
|
||||
f.updated_at,
|
||||
-- HU
|
||||
hu.pruefung_id AS hu_pruefung_id,
|
||||
hu.faellig_am AS hu_faellig_am,
|
||||
hu.tage_bis_faelligkeit AS hu_tage_bis_faelligkeit,
|
||||
hu.ergebnis AS hu_ergebnis,
|
||||
-- AU
|
||||
au.pruefung_id AS au_pruefung_id,
|
||||
au.faellig_am AS au_faellig_am,
|
||||
au.tage_bis_faelligkeit AS au_tage_bis_faelligkeit,
|
||||
au.ergebnis AS au_ergebnis,
|
||||
-- UVV
|
||||
uvv.pruefung_id AS uvv_pruefung_id,
|
||||
uvv.faellig_am AS uvv_faellig_am,
|
||||
uvv.tage_bis_faelligkeit AS uvv_tage_bis_faelligkeit,
|
||||
uvv.ergebnis AS uvv_ergebnis,
|
||||
-- Leiter (DLK only)
|
||||
leiter.pruefung_id AS leiter_pruefung_id,
|
||||
leiter.faellig_am AS leiter_faellig_am,
|
||||
leiter.tage_bis_faelligkeit AS leiter_tage_bis_faelligkeit,
|
||||
leiter.ergebnis AS leiter_ergebnis,
|
||||
-- Overall worst tage_bis_faelligkeit across all active inspections
|
||||
LEAST(
|
||||
hu.tage_bis_faelligkeit,
|
||||
au.tage_bis_faelligkeit,
|
||||
uvv.tage_bis_faelligkeit,
|
||||
leiter.tage_bis_faelligkeit
|
||||
) AS naechste_pruefung_tage
|
||||
FROM
|
||||
fahrzeuge f
|
||||
LEFT JOIN latest_pruefungen hu ON hu.fahrzeug_id = f.id AND hu.pruefung_art = 'HU'
|
||||
LEFT JOIN latest_pruefungen au ON au.fahrzeug_id = f.id AND au.pruefung_art = 'AU'
|
||||
LEFT JOIN latest_pruefungen uvv ON uvv.fahrzeug_id = f.id AND uvv.pruefung_art = 'UVV'
|
||||
LEFT JOIN latest_pruefungen leiter ON leiter.fahrzeug_id = f.id AND leiter.pruefung_art = 'Leiter';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- SEED DATA: 3 typical German Feuerwehr vehicles
|
||||
-- ============================================================
|
||||
DO $$
|
||||
DECLARE
|
||||
v_lf10_id UUID := uuid_generate_v4();
|
||||
v_hlf20_id UUID := uuid_generate_v4();
|
||||
v_mtf_id UUID := uuid_generate_v4();
|
||||
BEGIN
|
||||
-- Only insert if no vehicles exist yet (idempotent seed)
|
||||
IF (SELECT COUNT(*) FROM fahrzeuge) = 0 THEN
|
||||
|
||||
-- 1) LF 10 – Standard Löschgruppenfahrzeug
|
||||
INSERT INTO fahrzeuge (
|
||||
id, bezeichnung, kurzname, amtliches_kennzeichen,
|
||||
fahrgestellnummer, baujahr, hersteller, typ_schluessel,
|
||||
besatzung_soll, status, standort
|
||||
) VALUES (
|
||||
v_lf10_id,
|
||||
'LF 10',
|
||||
'LF 1',
|
||||
'WN-FW 1',
|
||||
'WDB9634031L123456',
|
||||
2018,
|
||||
'Mercedes-Benz Atego',
|
||||
'LF 10',
|
||||
'1/8',
|
||||
'einsatzbereit',
|
||||
'Feuerwehrhaus'
|
||||
);
|
||||
|
||||
-- LF 10 inspections
|
||||
INSERT INTO fahrzeug_pruefungen (fahrzeug_id, pruefung_art, faellig_am, durchgefuehrt_am, ergebnis, naechste_faelligkeit, pruefende_stelle) VALUES
|
||||
(v_lf10_id, 'HU', '2026-03-15', NULL, 'ausstehend', NULL, 'TÜV Süd Stuttgart'),
|
||||
(v_lf10_id, 'AU', '2026-04-01', NULL, 'ausstehend', NULL, 'TÜV Süd Stuttgart'),
|
||||
(v_lf10_id, 'UVV', '2025-11-30', '2025-11-28', 'bestanden', '2026-11-28', 'DGUV Prüfer Rems');
|
||||
|
||||
-- LF 10 maintenance log
|
||||
INSERT INTO fahrzeug_wartungslog (fahrzeug_id, datum, art, beschreibung, km_stand, kraftstoff_liter, kosten) VALUES
|
||||
(v_lf10_id, '2025-11-28', 'Inspektion', 'Jahresinspektion nach Herstellervorgabe, Öl- und Filterwechsel, Bremsenprüfung', 48320, NULL, 420.00),
|
||||
(v_lf10_id, '2025-10-15', 'Kraftstoff', 'Betankung nach Einsatz Feuerwehr Rems', 48150, 85.4, 145.18),
|
||||
(v_lf10_id, '2025-09-01', 'Reifenwechsel','Sommerreifen auf Winterreifen gewechselt, alle 4 Reifen erneuert (Continental)', 47800, NULL, 980.00);
|
||||
|
||||
-- 2) HLF 20/16 – Hilfeleistungslöschgruppenfahrzeug (flagship)
|
||||
INSERT INTO fahrzeuge (
|
||||
id, bezeichnung, kurzname, amtliches_kennzeichen,
|
||||
fahrgestellnummer, baujahr, hersteller, typ_schluessel,
|
||||
besatzung_soll, status, standort
|
||||
) VALUES (
|
||||
v_hlf20_id,
|
||||
'HLF 20/16',
|
||||
'HLF 1',
|
||||
'WN-FW 2',
|
||||
'WMAN29ZZ3LM654321',
|
||||
2020,
|
||||
'MAN TGM / Rosenbauer',
|
||||
'HLF 20/16',
|
||||
'1/8',
|
||||
'einsatzbereit',
|
||||
'Feuerwehrhaus'
|
||||
);
|
||||
|
||||
-- HLF 20 inspections — all current
|
||||
INSERT INTO fahrzeug_pruefungen (fahrzeug_id, pruefung_art, faellig_am, durchgefuehrt_am, ergebnis, naechste_faelligkeit, pruefende_stelle) VALUES
|
||||
(v_hlf20_id, 'HU', '2026-08-20', NULL, 'ausstehend', NULL, 'DEKRA Esslingen'),
|
||||
(v_hlf20_id, 'AU', '2026-02-01', '2026-01-28', 'bestanden', '2027-01-28', 'DEKRA Esslingen'),
|
||||
(v_hlf20_id, 'UVV', '2026-01-15', '2026-01-14', 'bestanden', '2027-01-14', 'DGUV Prüfer Rems');
|
||||
|
||||
-- HLF 20 maintenance log
|
||||
INSERT INTO fahrzeug_wartungslog (fahrzeug_id, datum, art, beschreibung, km_stand, kraftstoff_liter, kosten) VALUES
|
||||
(v_hlf20_id, '2026-01-28', 'Hauptuntersuchung', 'AU bestanden ohne Mängel bei DEKRA Esslingen', 22450, NULL, 185.00),
|
||||
(v_hlf20_id, '2026-01-14', 'Inspektion', 'UVV-Prüfung bestanden, Licht und Bremsen geprüft', 22430, NULL, 0.00),
|
||||
(v_hlf20_id, '2025-12-10', 'Kraftstoff', 'Betankung nach Übung', 22300, 120.0, 204.00),
|
||||
(v_hlf20_id, '2025-10-05', 'Reparatur', 'Hydraulikpumpe für Rettungssatz getauscht', 21980, NULL, 2340.00);
|
||||
|
||||
-- 3) MTF – Mannschaftstransportfahrzeug
|
||||
INSERT INTO fahrzeuge (
|
||||
id, bezeichnung, kurzname, amtliches_kennzeichen,
|
||||
fahrgestellnummer, baujahr, hersteller, typ_schluessel,
|
||||
besatzung_soll, status, status_bemerkung, standort
|
||||
) VALUES (
|
||||
v_mtf_id,
|
||||
'MTF',
|
||||
'MTF 1',
|
||||
'WN-FW 5',
|
||||
'WDB9066371S789012',
|
||||
2015,
|
||||
'Mercedes-Benz Sprinter',
|
||||
'MTF',
|
||||
'1/8',
|
||||
'ausser_dienst_wartung',
|
||||
'Geplante Inspektion: Zahnriemenwechsel 60.000 km fällig',
|
||||
'Feuerwehrhaus'
|
||||
);
|
||||
|
||||
-- MTF inspections — HU overdue (safety-critical test data)
|
||||
INSERT INTO fahrzeug_pruefungen (fahrzeug_id, pruefung_art, faellig_am, durchgefuehrt_am, ergebnis, naechste_faelligkeit, pruefende_stelle) VALUES
|
||||
(v_mtf_id, 'HU', '2026-01-31', NULL, 'ausstehend', NULL, 'TÜV Süd Stuttgart'),
|
||||
(v_mtf_id, 'AU', '2025-06-15', '2025-06-12', 'bestanden', '2026-06-12', 'TÜV Süd Stuttgart'),
|
||||
(v_mtf_id, 'UVV', '2025-12-01', '2025-11-30', 'bestanden_mit_maengeln', '2026-11-30', 'DGUV Prüfer Rems');
|
||||
|
||||
-- MTF maintenance log
|
||||
INSERT INTO fahrzeug_wartungslog (fahrzeug_id, datum, art, beschreibung, km_stand, kraftstoff_liter, kosten) VALUES
|
||||
(v_mtf_id, '2025-11-30', 'Inspektion', 'UVV-Prüfung: kleiner Mangel Innenbeleuchtung, nachgebessert', 58920, NULL, 0.00),
|
||||
(v_mtf_id, '2025-11-01', 'Kraftstoff', 'Betankung regulär', 58700, 65.0, 110.50),
|
||||
(v_mtf_id, '2025-09-20', 'Reparatur', 'Heckleuchte links defekt, Glühbirne getauscht', 58400, NULL, 12.50);
|
||||
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Cross-migration FK: add fahrzeug_id FK to einsatz_fahrzeuge (created in 004)
|
||||
-- This runs only if einsatz_fahrzeuge exists but the FK is not yet present.
|
||||
-- Handles the case where 004 ran before 005 and deferred the FK creation.
|
||||
-- ---------------------------------------------------------------------------
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'einsatz_fahrzeuge'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_name = 'einsatz_fahrzeuge'
|
||||
AND constraint_name = 'fk_ef_fahrzeug'
|
||||
) THEN
|
||||
ALTER TABLE einsatz_fahrzeuge
|
||||
ADD CONSTRAINT fk_ef_fahrzeug
|
||||
FOREIGN KEY (fahrzeug_id) REFERENCES fahrzeuge(id) ON DELETE CASCADE;
|
||||
RAISE NOTICE 'Added fk_ef_fahrzeug FK on einsatz_fahrzeuge';
|
||||
END IF;
|
||||
END $$;
|
||||
201
backend/src/database/migrations/006_create_uebungen.sql
Normal file
201
backend/src/database/migrations/006_create_uebungen.sql
Normal file
@@ -0,0 +1,201 @@
|
||||
-- =============================================================================
|
||||
-- Migration 006: Übungsplanung & Dienstkalender
|
||||
-- Training Schedule & Service Calendar for Feuerwehr Rems Dashboard
|
||||
-- =============================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1. Calendar token table for iCal subscribe URLs (no auth required per-request)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS calendar_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token VARCHAR(64) UNIQUE NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_used_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX idx_calendar_tokens_token ON calendar_tokens(token);
|
||||
CREATE INDEX idx_calendar_tokens_user_id ON calendar_tokens(user_id);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2. Main events table
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TYPE uebung_typ AS ENUM (
|
||||
'Übungsabend',
|
||||
'Lehrgang',
|
||||
'Sonderdienst',
|
||||
'Versammlung',
|
||||
'Gemeinschaftsübung',
|
||||
'Sonstiges'
|
||||
);
|
||||
|
||||
CREATE TABLE uebungen (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
titel VARCHAR(255) NOT NULL,
|
||||
beschreibung TEXT,
|
||||
typ uebung_typ NOT NULL,
|
||||
datum_von TIMESTAMPTZ NOT NULL,
|
||||
datum_bis TIMESTAMPTZ NOT NULL,
|
||||
ort VARCHAR(255),
|
||||
treffpunkt VARCHAR(255),
|
||||
pflichtveranstaltung BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
mindest_teilnehmer INT CHECK (mindest_teilnehmer > 0),
|
||||
max_teilnehmer INT CHECK (max_teilnehmer > 0),
|
||||
angelegt_von UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
aktualisiert_am TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
abgesagt BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
absage_grund TEXT,
|
||||
|
||||
CONSTRAINT datum_reihenfolge CHECK (datum_bis >= datum_von),
|
||||
CONSTRAINT max_groesser_min CHECK (
|
||||
max_teilnehmer IS NULL OR
|
||||
mindest_teilnehmer IS NULL OR
|
||||
max_teilnehmer >= mindest_teilnehmer
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_uebungen_datum_von ON uebungen(datum_von);
|
||||
CREATE INDEX idx_uebungen_typ ON uebungen(typ);
|
||||
CREATE INDEX idx_uebungen_pflichtveranstaltung ON uebungen(pflichtveranstaltung) WHERE pflichtveranstaltung = TRUE;
|
||||
CREATE INDEX idx_uebungen_abgesagt ON uebungen(abgesagt) WHERE abgesagt = FALSE;
|
||||
-- Compound index for the most common calendar-range query
|
||||
CREATE INDEX idx_uebungen_datum_von_bis ON uebungen(datum_von, datum_bis);
|
||||
|
||||
-- Keep aktualisiert_am in sync via trigger (reuse function from migration 001)
|
||||
CREATE TRIGGER update_uebungen_aktualisiert_am
|
||||
BEFORE UPDATE ON uebungen
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 3. Attendance / RSVP table
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TYPE teilnahme_status AS ENUM (
|
||||
'zugesagt',
|
||||
'abgesagt',
|
||||
'erschienen',
|
||||
'entschuldigt',
|
||||
'unbekannt'
|
||||
);
|
||||
|
||||
CREATE TABLE uebung_teilnahmen (
|
||||
uebung_id UUID NOT NULL REFERENCES uebungen(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
status teilnahme_status NOT NULL DEFAULT 'unbekannt',
|
||||
antwort_am TIMESTAMPTZ,
|
||||
erschienen_erfasst_am TIMESTAMPTZ,
|
||||
erschienen_erfasst_von UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
bemerkung VARCHAR(500),
|
||||
|
||||
PRIMARY KEY (uebung_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_teilnahmen_uebung_id ON uebung_teilnahmen(uebung_id);
|
||||
CREATE INDEX idx_teilnahmen_user_id ON uebung_teilnahmen(user_id);
|
||||
CREATE INDEX idx_teilnahmen_status ON uebung_teilnahmen(status);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4. Trigger: auto-create 'unbekannt' rows for all active members on new event
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION fn_create_teilnahmen_for_all_active_members()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
INSERT INTO uebung_teilnahmen (uebung_id, user_id, status)
|
||||
SELECT NEW.id, u.id, 'unbekannt'
|
||||
FROM users u
|
||||
WHERE u.is_active = TRUE
|
||||
ON CONFLICT (uebung_id, user_id) DO NOTHING;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_auto_teilnahmen_after_insert
|
||||
AFTER INSERT ON uebungen
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION fn_create_teilnahmen_for_all_active_members();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 5. Trigger: when a new member becomes active, add them to all future events
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION fn_add_member_to_future_events()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Only run when is_active transitions FALSE -> TRUE
|
||||
IF (OLD.is_active = FALSE OR OLD.is_active IS NULL) AND NEW.is_active = TRUE THEN
|
||||
INSERT INTO uebung_teilnahmen (uebung_id, user_id, status)
|
||||
SELECT u.id, NEW.id, 'unbekannt'
|
||||
FROM uebungen u
|
||||
WHERE u.datum_von > NOW()
|
||||
AND u.abgesagt = FALSE
|
||||
ON CONFLICT (uebung_id, user_id) DO NOTHING;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_add_member_to_future_events
|
||||
AFTER UPDATE OF is_active ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION fn_add_member_to_future_events();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 6. Convenience view: event overview with attendance counts
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE VIEW uebung_uebersicht AS
|
||||
SELECT
|
||||
u.id,
|
||||
u.titel,
|
||||
u.typ,
|
||||
u.datum_von,
|
||||
u.datum_bis,
|
||||
u.ort,
|
||||
u.treffpunkt,
|
||||
u.pflichtveranstaltung,
|
||||
u.mindest_teilnehmer,
|
||||
u.max_teilnehmer,
|
||||
u.abgesagt,
|
||||
u.absage_grund,
|
||||
u.angelegt_von,
|
||||
u.erstellt_am,
|
||||
u.aktualisiert_am,
|
||||
-- Attendance aggregates
|
||||
COUNT(t.user_id) AS gesamt_eingeladen,
|
||||
COUNT(t.user_id) FILTER (WHERE t.status = 'zugesagt') AS anzahl_zugesagt,
|
||||
COUNT(t.user_id) FILTER (WHERE t.status = 'abgesagt') AS anzahl_abgesagt,
|
||||
COUNT(t.user_id) FILTER (WHERE t.status = 'erschienen') AS anzahl_erschienen,
|
||||
COUNT(t.user_id) FILTER (WHERE t.status = 'entschuldigt') AS anzahl_entschuldigt,
|
||||
COUNT(t.user_id) FILTER (WHERE t.status = 'unbekannt') AS anzahl_unbekannt
|
||||
FROM uebungen u
|
||||
LEFT JOIN uebung_teilnahmen t ON t.uebung_id = u.id
|
||||
GROUP BY u.id;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 7. View: per-member participation statistics (feeds Tier 3 reporting)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE VIEW member_participation_stats AS
|
||||
SELECT
|
||||
usr.id AS user_id,
|
||||
COALESCE(usr.name, usr.preferred_username, usr.email) AS name,
|
||||
COUNT(t.uebung_id) AS total_eingeladen,
|
||||
COUNT(t.uebung_id) FILTER (WHERE t.status = 'erschienen') AS total_erschienen,
|
||||
COUNT(t.uebung_id) FILTER (
|
||||
WHERE u.pflichtveranstaltung = TRUE AND t.status = 'erschienen'
|
||||
) AS pflicht_erschienen,
|
||||
COUNT(t.uebung_id) FILTER (WHERE u.pflichtveranstaltung = TRUE) AS pflicht_gesamt,
|
||||
ROUND(
|
||||
CASE
|
||||
WHEN COUNT(t.uebung_id) FILTER (WHERE u.typ = 'Übungsabend') = 0 THEN 0
|
||||
ELSE
|
||||
COUNT(t.uebung_id) FILTER (
|
||||
WHERE u.typ = 'Übungsabend' AND t.status = 'erschienen'
|
||||
)::NUMERIC /
|
||||
COUNT(t.uebung_id) FILTER (WHERE u.typ = 'Übungsabend') * 100
|
||||
END, 1
|
||||
) AS uebungsabend_quote_pct
|
||||
FROM users usr
|
||||
JOIN uebung_teilnahmen t ON t.user_id = usr.id
|
||||
JOIN uebungen u ON u.id = t.uebung_id
|
||||
WHERE usr.is_active = TRUE
|
||||
AND u.abgesagt = FALSE
|
||||
GROUP BY usr.id, usr.name, usr.preferred_username, usr.email;
|
||||
197
backend/src/database/seeds/einsaetze_test_data.sql
Normal file
197
backend/src/database/seeds/einsaetze_test_data.sql
Normal file
@@ -0,0 +1,197 @@
|
||||
-- =============================================================================
|
||||
-- TEST DATA: 5 sample incidents for development and manual testing
|
||||
-- Run AFTER all migrations have been applied.
|
||||
-- Assumes at least one user exists in the users table.
|
||||
-- Replace the user UUIDs with real values from your dev database.
|
||||
-- =============================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Step 1: Insert two sample vehicles (if not already present)
|
||||
-- Uses column names from 005_create_fahrzeuge.sql
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO fahrzeuge (id, bezeichnung, kurzname, amtliches_kennzeichen, baujahr, hersteller, typ_schluessel, besatzung_soll, status)
|
||||
VALUES
|
||||
('a1000000-0000-0000-0000-000000000001', 'HLF 20 - Hilfeleistungslöschgruppenfahrzeug', 'HLF TEST', 'WN-FW 1234', 2020, 'MAN / Rosenbauer', 'HLF 20/16', '1/8', 'einsatzbereit'),
|
||||
('a1000000-0000-0000-0000-000000000002', 'TLF 3000 - Tanklöschfahrzeug', 'TLF TEST', 'WN-FW 5678', 2018, 'Mercedes-Benz Atego', 'TLF 3000', '1/5', 'einsatzbereit')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Step 2: Helper — get the first active user ID (replace with real UUID in prod)
|
||||
-- We use a DO block to make the script re-runnable and dev-environment agnostic.
|
||||
-- ---------------------------------------------------------------------------
|
||||
DO $$
|
||||
DECLARE
|
||||
v_user_id UUID;
|
||||
v_einsatz_id1 UUID := 'b1000000-0000-0000-0000-000000000001';
|
||||
v_einsatz_id2 UUID := 'b1000000-0000-0000-0000-000000000002';
|
||||
v_einsatz_id3 UUID := 'b1000000-0000-0000-0000-000000000003';
|
||||
v_einsatz_id4 UUID := 'b1000000-0000-0000-0000-000000000004';
|
||||
v_einsatz_id5 UUID := 'b1000000-0000-0000-0000-000000000005';
|
||||
v_fahrzeug1 UUID := 'a1000000-0000-0000-0000-000000000001';
|
||||
v_fahrzeug2 UUID := 'a1000000-0000-0000-0000-000000000002';
|
||||
BEGIN
|
||||
SELECT id INTO v_user_id FROM users WHERE is_active = TRUE LIMIT 1;
|
||||
IF v_user_id IS NULL THEN
|
||||
RAISE NOTICE 'No active user found — skipping test data insert.';
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- EINSATZ 1: Wohnungsbrand (Brand B3) — January — abgeschlossen
|
||||
-- -------------------------------------------------------------------------
|
||||
INSERT INTO einsaetze (
|
||||
id, einsatz_nr, alarm_time, ausrueck_time, ankunft_time, einrueck_time,
|
||||
einsatz_art, einsatz_stichwort, strasse, hausnummer, ort,
|
||||
bericht_kurz, bericht_text,
|
||||
einsatzleiter_id, alarmierung_art, status, created_by
|
||||
) VALUES (
|
||||
v_einsatz_id1,
|
||||
generate_einsatz_nr('2026-01-14 14:32:00+01'::TIMESTAMPTZ),
|
||||
'2026-01-14 14:32:00+01',
|
||||
'2026-01-14 14:35:00+01',
|
||||
'2026-01-14 14:41:00+01',
|
||||
'2026-01-14 17:15:00+01',
|
||||
'Brand', 'B3',
|
||||
'Hauptstraße', '42', 'Berglen',
|
||||
'Wohnungsbrand im 2. OG, 1 Person gerettet',
|
||||
'Alarm um 14:32 Uhr durch ILS. Bei Ankunft stand das zweite Obergeschoss in Flammen. ' ||
|
||||
'Eine Person wurde über die Drehleiter gerettet und dem Rettungsdienst übergeben. ' ||
|
||||
'Brandbekämpfung mit zwei C-Rohren. Nachlöscharbeiten bis 17:00 Uhr.',
|
||||
v_user_id, 'ILS', 'abgeschlossen', v_user_id
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- EINSATZ 2: Verkehrsunfall (THL 2) — Februar — abgeschlossen
|
||||
-- -------------------------------------------------------------------------
|
||||
INSERT INTO einsaetze (
|
||||
id, einsatz_nr, alarm_time, ausrueck_time, ankunft_time, einrueck_time,
|
||||
einsatz_art, einsatz_stichwort, strasse, hausnummer, ort,
|
||||
bericht_kurz, bericht_text,
|
||||
einsatzleiter_id, alarmierung_art, status, created_by
|
||||
) VALUES (
|
||||
v_einsatz_id2,
|
||||
generate_einsatz_nr('2026-02-03 08:17:00+01'::TIMESTAMPTZ),
|
||||
'2026-02-03 08:17:00+01',
|
||||
'2026-02-03 08:20:00+01',
|
||||
'2026-02-03 08:26:00+01',
|
||||
'2026-02-03 10:45:00+01',
|
||||
'THL', 'THL 2',
|
||||
'Landesstraße 1115', NULL, 'Rudersberg',
|
||||
'Verkehrsunfall mit eingeklemmter Person, PKW gegen Baum',
|
||||
'PKW frontal gegen Baum. Fahrer eingeklemmt, Beifahrerin konnte selbstständig aussteigen. ' ||
|
||||
'Technische Rettung mit Spreizer und Schere. Person nach ca. 25 Minuten befreit. ' ||
|
||||
'Übergabe an den Rettungsdienst und Hubschrauber.',
|
||||
v_user_id, 'ILS', 'abgeschlossen', v_user_id
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- EINSATZ 3: Brandmeldeanlage (BMA) — März — abgeschlossen (Fehlalarm)
|
||||
-- -------------------------------------------------------------------------
|
||||
INSERT INTO einsaetze (
|
||||
id, einsatz_nr, alarm_time, ausrueck_time, ankunft_time, einrueck_time,
|
||||
einsatz_art, einsatz_stichwort, strasse, hausnummer, ort,
|
||||
bericht_kurz,
|
||||
einsatzleiter_id, alarmierung_art, status, created_by
|
||||
) VALUES (
|
||||
v_einsatz_id3,
|
||||
generate_einsatz_nr('2026-03-21 11:05:00+01'::TIMESTAMPTZ),
|
||||
'2026-03-21 11:05:00+01',
|
||||
'2026-03-21 11:08:00+01',
|
||||
'2026-03-21 11:12:00+01',
|
||||
'2026-03-21 11:35:00+01',
|
||||
'BMA', NULL,
|
||||
'Gewerbepark Rems', '7', 'Weinstadt',
|
||||
'BMA ausgelöst durch Staubentwicklung bei Bauarbeiten, kein Feuer',
|
||||
v_user_id, 'ILS', 'abgeschlossen', v_user_id
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- EINSATZ 4: Ölspur (Hilfeleistung) — März — abgeschlossen
|
||||
-- -------------------------------------------------------------------------
|
||||
INSERT INTO einsaetze (
|
||||
id, einsatz_nr, alarm_time, ausrueck_time, ankunft_time, einrueck_time,
|
||||
einsatz_art, einsatz_stichwort, strasse, hausnummer, ort,
|
||||
bericht_kurz,
|
||||
einsatzleiter_id, alarmierung_art, status, created_by
|
||||
) VALUES (
|
||||
v_einsatz_id4,
|
||||
generate_einsatz_nr('2026-03-28 16:45:00+01'::TIMESTAMPTZ),
|
||||
'2026-03-28 16:45:00+01',
|
||||
'2026-03-28 16:49:00+01',
|
||||
'2026-03-28 16:54:00+01',
|
||||
'2026-03-28 18:20:00+01',
|
||||
'Hilfeleistung', NULL,
|
||||
'Ortsdurchfahrt B29', NULL, 'Schorndorf',
|
||||
'Ölspur ca. 600m, LKW verlor Hydraulikflüssigkeit, Reinigung mit Ölbindemittel',
|
||||
v_user_id, 'ILS', 'abgeschlossen', v_user_id
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- EINSATZ 5: Flächenbrand (Brand B2) — aktiv (noch laufend heute)
|
||||
-- -------------------------------------------------------------------------
|
||||
INSERT INTO einsaetze (
|
||||
id, einsatz_nr, alarm_time, ausrueck_time, ankunft_time,
|
||||
einsatz_art, einsatz_stichwort, strasse, hausnummer, ort,
|
||||
bericht_kurz,
|
||||
einsatzleiter_id, alarmierung_art, status, created_by
|
||||
) VALUES (
|
||||
v_einsatz_id5,
|
||||
generate_einsatz_nr(NOW()),
|
||||
NOW() - INTERVAL '45 minutes',
|
||||
NOW() - INTERVAL '42 minutes',
|
||||
NOW() - INTERVAL '37 minutes',
|
||||
'Brand', 'B2',
|
||||
'Waldweg', NULL, 'Alfdorf',
|
||||
'Flächenbrand ca. 0,5 ha Unterholz, Wasserversorgung über Pendelverkehr',
|
||||
v_user_id, 'ILS', 'aktiv', v_user_id
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- VEHICLES — assign to incidents
|
||||
-- -------------------------------------------------------------------------
|
||||
INSERT INTO einsatz_fahrzeuge (einsatz_id, fahrzeug_id, ausrueck_time, einrueck_time)
|
||||
VALUES
|
||||
(v_einsatz_id1, v_fahrzeug1, '2026-01-14 14:35:00+01', '2026-01-14 17:15:00+01'),
|
||||
(v_einsatz_id1, v_fahrzeug2, '2026-01-14 14:36:00+01', '2026-01-14 17:10:00+01'),
|
||||
(v_einsatz_id2, v_fahrzeug1, '2026-02-03 08:20:00+01', '2026-02-03 10:45:00+01'),
|
||||
(v_einsatz_id4, v_fahrzeug1, '2026-03-28 16:49:00+01', '2026-03-28 18:20:00+01'),
|
||||
(v_einsatz_id5, v_fahrzeug1, NOW() - INTERVAL '42 minutes', NULL),
|
||||
(v_einsatz_id5, v_fahrzeug2, NOW() - INTERVAL '40 minutes', NULL)
|
||||
ON CONFLICT (einsatz_id, fahrzeug_id) DO NOTHING;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- PERSONNEL — assign the single test user to each incident
|
||||
-- -------------------------------------------------------------------------
|
||||
INSERT INTO einsatz_personal (einsatz_id, user_id, funktion, alarm_time, ankunft_time)
|
||||
VALUES
|
||||
(v_einsatz_id1, v_user_id, 'Einsatzleiter', '2026-01-14 14:32:00+01', '2026-01-14 14:41:00+01'),
|
||||
(v_einsatz_id2, v_user_id, 'Gruppenführer', '2026-02-03 08:17:00+01', '2026-02-03 08:26:00+01'),
|
||||
(v_einsatz_id3, v_user_id, 'Einsatzleiter', '2026-03-21 11:05:00+01', '2026-03-21 11:12:00+01'),
|
||||
(v_einsatz_id4, v_user_id, 'Maschinist', '2026-03-28 16:45:00+01', '2026-03-28 16:54:00+01'),
|
||||
(v_einsatz_id5, v_user_id, 'Einsatzleiter', NOW() - INTERVAL '45 minutes', NOW() - INTERVAL '37 minutes')
|
||||
ON CONFLICT (einsatz_id, user_id) DO NOTHING;
|
||||
|
||||
RAISE NOTICE 'Test data inserted successfully for user %', v_user_id;
|
||||
END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Refresh the materialized view after data load
|
||||
-- ---------------------------------------------------------------------------
|
||||
REFRESH MATERIALIZED VIEW einsatz_statistik;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Verify: quick sanity check
|
||||
-- ---------------------------------------------------------------------------
|
||||
SELECT
|
||||
e.einsatz_nr,
|
||||
e.einsatz_art,
|
||||
TO_CHAR(e.alarm_time AT TIME ZONE 'Europe/Berlin', 'DD.MM.YYYY HH24:MI') AS alarm_de,
|
||||
e.ort,
|
||||
e.status,
|
||||
COUNT(ep.user_id) AS kräfte,
|
||||
COUNT(ef.fahrzeug_id) AS fahrzeuge
|
||||
FROM einsaetze e
|
||||
LEFT JOIN einsatz_personal ep ON ep.einsatz_id = e.id
|
||||
LEFT JOIN einsatz_fahrzeuge ef ON ef.einsatz_id = e.id
|
||||
GROUP BY e.id, e.einsatz_nr, e.einsatz_art, e.alarm_time, e.ort, e.status
|
||||
ORDER BY e.alarm_time DESC;
|
||||
Reference in New Issue
Block a user