202 lines
13 KiB
PL/PgSQL
202 lines
13 KiB
PL/PgSQL
-- 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; |