diff --git a/db/migrations/003_qualifications_fixed.sql b/db/migrations/003_qualifications_fixed.sql new file mode 100644 index 0000000..7c14b70 --- /dev/null +++ b/db/migrations/003_qualifications_fixed.sql @@ -0,0 +1,110 @@ +-- Migration: 003_qualifications_fixed.sql +-- Qualifikationen & Zertifikate Modul (korrigiert für INTEGER IDs) + +-- Verfügbare Qualifikationstypen (System-weit) +CREATE TABLE IF NOT EXISTS qualification_types ( + id SERIAL PRIMARY KEY, + key VARCHAR(50) UNIQUE NOT NULL, + name VARCHAR(100) NOT NULL, + description TEXT, + category VARCHAR(50) NOT NULL DEFAULT 'general', + has_expiry BOOLEAN DEFAULT true, + validity_months INTEGER, + is_required BOOLEAN DEFAULT false, + icon VARCHAR(10) DEFAULT '📜', + sort_order INTEGER DEFAULT 0, + is_system BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT NOW() +); + +-- Standard-Qualifikationstypen einfügen (nur wenn Tabelle leer) +INSERT INTO qualification_types (key, name, description, category, has_expiry, validity_months, is_required, icon, sort_order, is_system) +SELECT * FROM (VALUES + ('sachkunde_34a', '§34a Sachkundenachweis', 'Sachkundeprüfung nach §34a GewO', 'legal', false, NULL::int, true, '📋', 1, true), + ('unterrichtung_34a', '§34a Unterrichtungsnachweis', '40-Stunden Unterrichtung nach §34a GewO', 'legal', false, NULL::int, false, '📋', 2, true), + ('waffensachkunde', 'Waffensachkunde', 'Sachkundenachweis für Waffenbesitz', 'weapons', false, NULL::int, false, '🔫', 10, true), + ('waffenschein_gruen', 'Waffenschein (grün)', 'Waffenschein für Schusswaffen', 'weapons', true, 36, false, '🟢', 11, true), + ('waffenschein_gelb', 'Waffenschein (gelb)', 'Kleiner Waffenschein', 'weapons', true, 36, false, '🟡', 12, true), + ('erste_hilfe', 'Erste-Hilfe-Kurs', 'Erste-Hilfe-Ausbildung (9 UE)', 'safety', true, 24, false, '🏥', 20, true), + ('brandschutzhelfer', 'Brandschutzhelfer', 'Ausbildung zum Brandschutzhelfer', 'safety', true, 36, false, '🔥', 21, true), + ('evakuierungshelfer', 'Evakuierungshelfer', 'Ausbildung zum Evakuierungshelfer', 'safety', true, 24, false, '🚪', 22, true), + ('defibrillator', 'AED/Defibrillator', 'Einweisung Automatisierter Externer Defibrillator', 'safety', true, 24, false, '💓', 23, true), + ('fuehrerschein_b', 'Führerschein Klasse B', 'PKW Führerschein', 'driving', true, NULL::int, false, '🚗', 30, true), + ('fuehrerschein_c', 'Führerschein Klasse C', 'LKW Führerschein', 'driving', true, 60, false, '🚛', 31, true), + ('fuehrerschein_d', 'Führerschein Klasse D', 'Bus Führerschein', 'driving', true, 60, false, '🚌', 32, true), + ('personenschutz', 'Personenschutz', 'Ausbildung zum Personenschützer', 'special', false, NULL::int, false, '🛡️', 40, true), + ('hundefuehrer', 'Diensthundeführer', 'Ausbildung zum Diensthundeführer', 'special', true, 12, false, '🐕', 41, true), + ('intervention', 'Interventionskraft', 'Ausbildung zur Interventionskraft (IK)', 'special', false, NULL::int, false, '⚡', 42, true), + ('nsk', 'NSK (Notruf-Service-Kraft)', 'Notruf- und Service-Leitstelle', 'special', false, NULL::int, false, '📞', 43, true), + ('sprache_englisch', 'Englisch', 'Englischkenntnisse', 'language', false, NULL::int, false, '🇬🇧', 50, true), + ('sprache_franzoesisch', 'Französisch', 'Französischkenntnisse', 'language', false, NULL::int, false, '🇫🇷', 51, true), + ('sprache_russisch', 'Russisch', 'Russischkenntnisse', 'language', false, NULL::int, false, '🇷🇺', 52, true), + ('sprache_tuerkisch', 'Türkisch', 'Türkischkenntnisse', 'language', false, NULL::int, false, '🇹🇷', 53, true), + ('sprache_arabisch', 'Arabisch', 'Arabischkenntnisse', 'language', false, NULL::int, false, '🇸🇦', 54, true) +) AS v(key, name, description, category, has_expiry, validity_months, is_required, icon, sort_order, is_system) +WHERE NOT EXISTS (SELECT 1 FROM qualification_types LIMIT 1); + +-- Custom Qualifikationstypen pro Organisation +CREATE TABLE IF NOT EXISTS org_qualification_types ( + id SERIAL PRIMARY KEY, + org_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + key VARCHAR(50) NOT NULL, + name VARCHAR(100) NOT NULL, + description TEXT, + category VARCHAR(50) DEFAULT 'custom', + has_expiry BOOLEAN DEFAULT true, + validity_months INTEGER, + icon VARCHAR(10) DEFAULT '📜', + sort_order INTEGER DEFAULT 100, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(org_id, key) +); + +-- Mitarbeiter-Qualifikationen +CREATE TABLE IF NOT EXISTS employee_qualifications ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + org_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + + qualification_type_id INTEGER REFERENCES qualification_types(id), + org_qualification_type_id INTEGER REFERENCES org_qualification_types(id), + + issued_date DATE, + expiry_date DATE, + issuer VARCHAR(255), + certificate_number VARCHAR(100), + level VARCHAR(50), + document_url TEXT, + document_name VARCHAR(255), + status VARCHAR(20) DEFAULT 'active', + verified_by INTEGER REFERENCES users(id), + verified_at TIMESTAMP, + notes TEXT, + + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + + CONSTRAINT chk_qualification_type CHECK ( + (qualification_type_id IS NOT NULL AND org_qualification_type_id IS NULL) OR + (qualification_type_id IS NULL AND org_qualification_type_id IS NOT NULL) + ) +); + +-- Indices +CREATE INDEX IF NOT EXISTS idx_emp_qual_user ON employee_qualifications(user_id); +CREATE INDEX IF NOT EXISTS idx_emp_qual_org ON employee_qualifications(org_id); +CREATE INDEX IF NOT EXISTS idx_emp_qual_expiry ON employee_qualifications(expiry_date); +CREATE INDEX IF NOT EXISTS idx_emp_qual_status ON employee_qualifications(status); + +-- Erinnerungen +CREATE TABLE IF NOT EXISTS qualification_reminders ( + id SERIAL PRIMARY KEY, + employee_qualification_id INTEGER NOT NULL REFERENCES employee_qualifications(id) ON DELETE CASCADE, + reminder_date DATE NOT NULL, + days_before INTEGER NOT NULL, + sent BOOLEAN DEFAULT false, + sent_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_qual_reminder_date ON qualification_reminders(reminder_date, sent); diff --git a/db/migrations/004_objects.sql b/db/migrations/004_objects.sql new file mode 100644 index 0000000..3cc963c --- /dev/null +++ b/db/migrations/004_objects.sql @@ -0,0 +1,179 @@ +-- Migration: 004_objects.sql +-- Objektverwaltung / Wachobjekte + +-- Wachobjekte/Standorte +CREATE TABLE IF NOT EXISTS objects ( + id SERIAL PRIMARY KEY, + org_id INTEGER NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + + -- Basisdaten + name VARCHAR(200) NOT NULL, + short_name VARCHAR(50), + object_number VARCHAR(50), -- Interne Objektnummer + + -- Typ + object_type VARCHAR(50) DEFAULT 'other', -- building, event, construction, retail, industrial, residential, other + + -- Adresse + street VARCHAR(200), + house_number VARCHAR(20), + postal_code VARCHAR(10), + city VARCHAR(100), + country VARCHAR(50) DEFAULT 'Deutschland', + + -- Koordinaten (für Karte/GPS) + latitude DECIMAL(10, 8), + longitude DECIMAL(11, 8), + + -- Kontaktdaten Objekt + phone VARCHAR(50), + email VARCHAR(255), + + -- Details + description TEXT, + size_sqm INTEGER, -- Größe in m² + floors INTEGER, -- Anzahl Etagen + + -- Zugang + access_info TEXT, -- Zugangsinfos (Schlüssel, Codes, etc.) + parking_info TEXT, -- Parkhinweise + + -- Kundenreferenz + customer_id INTEGER, -- Später für CRM-Modul + customer_name VARCHAR(200), + + -- Status + status VARCHAR(20) DEFAULT 'active', -- active, inactive, archived + + -- Bilder + image_url TEXT, + + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + created_by INTEGER REFERENCES users(id) +); + +CREATE INDEX IF NOT EXISTS idx_objects_org ON objects(org_id); +CREATE INDEX IF NOT EXISTS idx_objects_status ON objects(status); +CREATE INDEX IF NOT EXISTS idx_objects_type ON objects(object_type); + +-- Ansprechpartner pro Objekt +CREATE TABLE IF NOT EXISTS object_contacts ( + id SERIAL PRIMARY KEY, + object_id INTEGER NOT NULL REFERENCES objects(id) ON DELETE CASCADE, + + -- Kontaktdaten + name VARCHAR(200) NOT NULL, + role VARCHAR(100), -- z.B. "Hausmeister", "Objektleiter", "Notfall" + company VARCHAR(200), + phone VARCHAR(50), + mobile VARCHAR(50), + email VARCHAR(255), + + -- Verfügbarkeit + availability TEXT, -- z.B. "Mo-Fr 8-17 Uhr" + + -- Priorität + is_primary BOOLEAN DEFAULT false, + is_emergency BOOLEAN DEFAULT false, -- Notfallkontakt + + notes TEXT, + + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_object_contacts_object ON object_contacts(object_id); + +-- Dienstanweisungen pro Objekt +CREATE TABLE IF NOT EXISTS object_instructions ( + id SERIAL PRIMARY KEY, + object_id INTEGER NOT NULL REFERENCES objects(id) ON DELETE CASCADE, + + title VARCHAR(200) NOT NULL, + category VARCHAR(50) DEFAULT 'general', -- general, patrol, emergency, access, reporting + content TEXT NOT NULL, + + -- Sortierung/Priorität + sort_order INTEGER DEFAULT 0, + is_critical BOOLEAN DEFAULT false, -- Wichtig hervorheben + + -- Version + version INTEGER DEFAULT 1, + + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + created_by INTEGER REFERENCES users(id) +); + +CREATE INDEX IF NOT EXISTS idx_object_instructions_object ON object_instructions(object_id); + +-- Dokumente pro Objekt (Lagepläne, Schlüssellisten, etc.) +CREATE TABLE IF NOT EXISTS object_documents ( + id SERIAL PRIMARY KEY, + object_id INTEGER NOT NULL REFERENCES objects(id) ON DELETE CASCADE, + + name VARCHAR(200) NOT NULL, + document_type VARCHAR(50), -- floorplan, keylist, contract, photo, other + file_url TEXT NOT NULL, + file_size INTEGER, + mime_type VARCHAR(100), + + description TEXT, + + uploaded_at TIMESTAMP DEFAULT NOW(), + uploaded_by INTEGER REFERENCES users(id) +); + +CREATE INDEX IF NOT EXISTS idx_object_documents_object ON object_documents(object_id); + +-- Checkpoints/Kontrollpunkte pro Objekt (für Wächterkontrolle) +CREATE TABLE IF NOT EXISTS object_checkpoints ( + id SERIAL PRIMARY KEY, + object_id INTEGER NOT NULL REFERENCES objects(id) ON DELETE CASCADE, + + name VARCHAR(100) NOT NULL, + location_description TEXT, -- Wo genau ist der Punkt + + -- QR/NFC Code + code VARCHAR(100) UNIQUE, -- Der zu scannende Code + code_type VARCHAR(20) DEFAULT 'qr', -- qr, nfc, manual + + -- Position in Rundgang + sort_order INTEGER DEFAULT 0, + + -- GPS (optional) + latitude DECIMAL(10, 8), + longitude DECIMAL(11, 8), + + -- Status + active BOOLEAN DEFAULT true, + + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_object_checkpoints_object ON object_checkpoints(object_id); +CREATE INDEX IF NOT EXISTS idx_object_checkpoints_code ON object_checkpoints(code); + +-- Anforderungen pro Objekt (welche Qualifikationen werden benötigt) +CREATE TABLE IF NOT EXISTS object_requirements ( + id SERIAL PRIMARY KEY, + object_id INTEGER NOT NULL REFERENCES objects(id) ON DELETE CASCADE, + qualification_type_id INTEGER REFERENCES qualification_types(id), + org_qualification_type_id INTEGER REFERENCES org_qualification_types(id), + is_mandatory BOOLEAN DEFAULT true, + notes TEXT, + + CONSTRAINT chk_object_req_type CHECK ( + (qualification_type_id IS NOT NULL AND org_qualification_type_id IS NULL) OR + (qualification_type_id IS NULL AND org_qualification_type_id IS NOT NULL) + ) +); + +CREATE INDEX IF NOT EXISTS idx_object_requirements_object ON object_requirements(object_id); + +-- Objekt-Typen Referenz +COMMENT ON TABLE objects IS 'Wachobjekte/Standorte - object_type: building, event, construction, retail, industrial, residential, other'; +COMMENT ON TABLE object_contacts IS 'Ansprechpartner pro Objekt'; +COMMENT ON TABLE object_instructions IS 'Dienstanweisungen - category: general, patrol, emergency, access, reporting'; +COMMENT ON TABLE object_documents IS 'Dokumente - document_type: floorplan, keylist, contract, photo, other'; +COMMENT ON TABLE object_checkpoints IS 'Kontrollpunkte für Wächterkontrolle';