add features

This commit is contained in:
Matthias Hochmeister
2026-02-27 19:50:14 +01:00
parent c5e8337a69
commit 620bacc6b5
46 changed files with 14095 additions and 1 deletions

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