-- Migration: 017_student_booking_schema.sql -- Fahrschüler-Buchungssystem + Multi-Tenant Instructor + Break-Rules BEGIN; -- ══════════════════════════════════════════════════════ -- 1. users.role ist bereits text (kein ENUM), keine Änderung nötig -- ══════════════════════════════════════════════════════ -- ══════════════════════════════════════════════════════ -- 2. instructors erweitern: UE-Limits + Booking-Settings -- ══════════════════════════════════════════════════════ ALTER TABLE instructors ADD COLUMN IF NOT EXISTS max_daily_units integer DEFAULT NULL; ALTER TABLE instructors ADD COLUMN IF NOT EXISTS max_daily_override integer DEFAULT 0; ALTER TABLE instructors ADD COLUMN IF NOT EXISTS max_weekly_units integer DEFAULT NULL; ALTER TABLE instructors ADD COLUMN IF NOT EXISTS max_weekly_override integer DEFAULT 0; ALTER TABLE instructors ADD COLUMN IF NOT EXISTS booking_enabled integer DEFAULT 1; -- ══════════════════════════════════════════════════════ -- 3. students erweitern: user_id FK -- ══════════════════════════════════════════════════════ ALTER TABLE students ADD COLUMN IF NOT EXISTS user_id bigint REFERENCES users(id) NULL; -- ══════════════════════════════════════════════════════ -- 4. tenants erweitern: free_cancellation_hours -- ══════════════════════════════════════════════════════ ALTER TABLE tenants ADD COLUMN IF NOT EXISTS free_cancellation_hours integer DEFAULT 24; -- ══════════════════════════════════════════════════════ -- 5. tenant_registration_codes -- ══════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS tenant_registration_codes ( id bigserial PRIMARY KEY, tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, code text NOT NULL UNIQUE, is_active integer DEFAULT 1, used_count integer DEFAULT 0, created_at timestamptz DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_tenant_reg_codes_code ON tenant_registration_codes(code); CREATE INDEX IF NOT EXISTS idx_tenant_reg_codes_tenant ON tenant_registration_codes(tenant_id); -- ══════════════════════════════════════════════════════ -- 6. student_tenants -- ══════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS student_tenants ( id bigserial PRIMARY KEY, student_user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE, tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'active', 'suspended')), registered_at timestamptz DEFAULT NOW(), confirmed_at timestamptz NULL, confirmed_by bigint NULL REFERENCES users(id), UNIQUE(student_user_id, tenant_id) ); CREATE INDEX IF NOT EXISTS idx_student_tenants_user ON student_tenants(student_user_id); CREATE INDEX IF NOT EXISTS idx_student_tenants_tenant ON student_tenants(tenant_id); -- ══════════════════════════════════════════════════════ -- 7. instructor_availability -- ══════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS instructor_availability ( id bigserial PRIMARY KEY, instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, weekday integer NOT NULL CHECK (weekday BETWEEN 0 AND 6), time_from text NOT NULL, time_to text NOT NULL, is_active integer DEFAULT 1, UNIQUE(instructor_id, weekday, time_from) ); CREATE INDEX IF NOT EXISTS idx_instructor_avail_instructor ON instructor_availability(instructor_id); -- ══════════════════════════════════════════════════════ -- 8. appointment_requests -- ══════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS appointment_requests ( id bigserial PRIMARY KEY, student_user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE, instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, lesson_type_id bigint NOT NULL REFERENCES lesson_types(id), requested_start timestamptz NOT NULL, requested_end timestamptz NOT NULL, status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'rejected', 'cancelled')), requested_at timestamptz DEFAULT NOW(), responded_at timestamptz NULL, response_by bigint NULL REFERENCES users(id), notes text DEFAULT '', confirmed_appointment_id bigint NULL REFERENCES appointments(id) ); CREATE INDEX IF NOT EXISTS idx_appt_req_student ON appointment_requests(student_user_id); CREATE INDEX IF NOT EXISTS idx_appt_req_instructor ON appointment_requests(instructor_id); CREATE INDEX IF NOT EXISTS idx_appt_req_status ON appointment_requests(status); -- ══════════════════════════════════════════════════════ -- 9. instructor_vacations -- ══════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS instructor_vacations ( id bigserial PRIMARY KEY, instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, date_from date NOT NULL, date_to date NOT NULL, notes text DEFAULT '', created_at timestamptz DEFAULT NOW(), UNIQUE(instructor_id, date_from, date_to) ); CREATE INDEX IF NOT EXISTS idx_instructor_vacations_instructor ON instructor_vacations(instructor_id); CREATE INDEX IF NOT EXISTS idx_instructor_vacations_dates ON instructor_vacations(date_from, date_to); -- ══════════════════════════════════════════════════════ -- 10. private_appointments -- ══════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS private_appointments ( id bigserial PRIMARY KEY, instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, title text NOT NULL DEFAULT 'Privat', date date NOT NULL, time_from text NOT NULL, time_to text NOT NULL, color text DEFAULT '#9ca3af', created_at timestamptz DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_private_appts_instructor ON private_appointments(instructor_id); CREATE INDEX IF NOT EXISTS idx_private_appts_date ON private_appointments(date); -- ══════════════════════════════════════════════════════ -- 11. instructor_tenants -- ══════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS instructor_tenants ( id bigserial PRIMARY KEY, instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, is_primary integer DEFAULT 0, created_at timestamptz DEFAULT NOW(), UNIQUE(instructor_id, tenant_id) ); CREATE INDEX IF NOT EXISTS idx_instructor_tenants_instructor ON instructor_tenants(instructor_id); CREATE INDEX IF NOT EXISTS idx_instructor_tenants_tenant ON instructor_tenants(tenant_id); -- ══════════════════════════════════════════════════════ -- 12. instructor_travel_times -- ══════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS instructor_travel_times ( id bigserial PRIMARY KEY, instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, from_tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, to_tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, minutes integer NOT NULL, UNIQUE(instructor_id, from_tenant_id, to_tenant_id) ); CREATE INDEX IF NOT EXISTS idx_travel_times_instructor ON instructor_travel_times(instructor_id); -- ══════════════════════════════════════════════════════ -- 13. instructor_home_to_tenant -- ══════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS instructor_home_to_tenant ( id bigserial PRIMARY KEY, instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, tenant_id bigint NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, minutes integer NOT NULL, UNIQUE(instructor_id, tenant_id) ); CREATE INDEX IF NOT EXISTS idx_home_to_tenant_instructor ON instructor_home_to_tenant(instructor_id); -- ══════════════════════════════════════════════════════ -- 14. instructor_break_rules -- ══════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS instructor_break_rules ( id bigserial PRIMARY KEY, instructor_id bigint NOT NULL REFERENCES instructors(id) ON DELETE CASCADE, condition_type text NOT NULL CHECK (condition_type IN ('daily_units', 'weekly_units', 'hours_daily', 'between_appointments', 'continuous_minutes')), operator text NOT NULL CHECK (operator IN ('greater_than', 'equals', 'at_least')), threshold numeric NOT NULL, break_minutes integer NOT NULL, is_active integer DEFAULT 1, created_at timestamptz DEFAULT NOW(), UNIQUE(instructor_id, condition_type, operator, threshold) ); CREATE INDEX IF NOT EXISTS idx_break_rules_instructor ON instructor_break_rules(instructor_id); -- ══════════════════════════════════════════════════════ -- 15. Default Registration-Codes für bestehende Tenants -- ══════════════════════════════════════════════════════ INSERT INTO tenant_registration_codes (tenant_id, code, is_active) SELECT id, LPAD(id::text, 8, '0') || '-' || UPPER(SUBSTRING(MD5(id::text || 'salt') FROM 1 FOR 4)), 1 FROM tenants WHERE id NOT IN (SELECT tenant_id FROM tenant_registration_codes) ON CONFLICT (code) DO NOTHING; -- ══════════════════════════════════════════════════════ -- 16. Default Availability für alle Instructors (Mo-So 08:00-19:00) -- ══════════════════════════════════════════════════════ INSERT INTO instructor_availability (instructor_id, weekday, time_from, time_to, is_active) SELECT i.id, w.day, '08:00', '19:00', 1 FROM instructors i CROSS JOIN (VALUES (0),(1),(2),(3),(4),(5),(6)) AS w(day) WHERE i.is_active = 1 AND NOT EXISTS ( SELECT 1 FROM instructor_availability ia WHERE ia.instructor_id = i.id AND ia.weekday = w.day ); COMMIT;