-- 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 $$;