226 lines
12 KiB
SQL
226 lines
12 KiB
SQL
-- ============================================================
|
||
-- Classroom Copilot — Academic Calendar Source of Truth
|
||
-- Migration 003: Supabase-backed academic calendar & timetable tables
|
||
-- Run after: 002_schema.sql
|
||
--
|
||
-- Design: Supabase is the source of truth for all editable calendar
|
||
-- and timetable data. Neo4j is a derived graph rebuilt from these tables.
|
||
-- All tables include neo4j_node_id to track the corresponding Neo4j uuid_string.
|
||
-- ============================================================
|
||
|
||
-- ─── 1. school_timetables ────────────────────────────────────────────────────
|
||
-- One row per academic year configuration per school.
|
||
-- periods_template JSONB stores the period definitions (code, name, times, type).
|
||
|
||
CREATE TABLE IF NOT EXISTS school_timetables (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||
year_label TEXT NOT NULL, -- e.g. '2025-2026'
|
||
start_date DATE NOT NULL,
|
||
end_date DATE NOT NULL,
|
||
periods_template JSONB NOT NULL DEFAULT '[]',
|
||
neo4j_node_id TEXT, -- SchoolTimetable.uuid_string in Neo4j
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
UNIQUE (institute_id, year_label)
|
||
);
|
||
|
||
-- ─── 2. academic_years ───────────────────────────────────────────────────────
|
||
|
||
CREATE TABLE IF NOT EXISTS academic_years (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
school_timetable_id UUID NOT NULL REFERENCES school_timetables(id) ON DELETE CASCADE,
|
||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||
year_label TEXT NOT NULL, -- '2025-2026'
|
||
neo4j_node_id TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
UNIQUE (school_timetable_id, year_label)
|
||
);
|
||
|
||
-- ─── 3. academic_terms ───────────────────────────────────────────────────────
|
||
|
||
CREATE TABLE IF NOT EXISTS academic_terms (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
academic_year_id UUID NOT NULL REFERENCES academic_years(id) ON DELETE CASCADE,
|
||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||
term_name TEXT NOT NULL,
|
||
term_number INTEGER NOT NULL,
|
||
start_date DATE NOT NULL,
|
||
end_date DATE NOT NULL,
|
||
neo4j_node_id TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
UNIQUE (academic_year_id, term_number)
|
||
);
|
||
|
||
-- ─── 4. academic_weeks ───────────────────────────────────────────────────────
|
||
-- week_cycle 'A'|'B' for two-week timetable cycles.
|
||
|
||
CREATE TABLE IF NOT EXISTS academic_weeks (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
academic_term_id UUID NOT NULL REFERENCES academic_terms(id) ON DELETE CASCADE,
|
||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||
week_number INTEGER NOT NULL, -- sequential within term
|
||
start_date DATE NOT NULL, -- Monday of this week
|
||
week_cycle TEXT NOT NULL DEFAULT 'A' CHECK (week_cycle IN ('A', 'B', '')),
|
||
neo4j_node_id TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
UNIQUE (academic_term_id, week_number)
|
||
);
|
||
|
||
-- ─── 5. academic_days ────────────────────────────────────────────────────────
|
||
-- One row per school day (Mon–Fri within term bounds).
|
||
-- excluded_period_codes: period codes from the template that do NOT apply this day.
|
||
-- academic_day_number: sequential count of Academic-type days across the year.
|
||
|
||
CREATE TABLE IF NOT EXISTS academic_days (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
academic_week_id UUID NOT NULL REFERENCES academic_weeks(id) ON DELETE CASCADE,
|
||
academic_term_id UUID NOT NULL REFERENCES academic_terms(id) ON DELETE CASCADE,
|
||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||
date DATE NOT NULL,
|
||
day_of_week TEXT NOT NULL,
|
||
day_type TEXT NOT NULL DEFAULT 'Academic'
|
||
CHECK (day_type IN ('Academic', 'Holiday', 'Staff', 'OffTimetable')),
|
||
academic_day_number INTEGER, -- null for non-Academic days
|
||
excluded_period_codes TEXT[] NOT NULL DEFAULT '{}',
|
||
neo4j_node_id TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
UNIQUE (institute_id, date)
|
||
);
|
||
|
||
-- ─── 6. teacher_timetables ───────────────────────────────────────────────────
|
||
-- One per teacher per academic year.
|
||
|
||
CREATE TABLE IF NOT EXISTS teacher_timetables (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||
school_timetable_id UUID NOT NULL REFERENCES school_timetables(id) ON DELETE CASCADE,
|
||
start_date DATE NOT NULL,
|
||
end_date DATE NOT NULL,
|
||
neo4j_node_id TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
UNIQUE (profile_id, school_timetable_id)
|
||
);
|
||
|
||
-- ─── 7. teacher_timetable_slots ──────────────────────────────────────────────
|
||
-- Weekly recurring slot assignments (day + period → subject class).
|
||
|
||
CREATE TABLE IF NOT EXISTS teacher_timetable_slots (
|
||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||
teacher_timetable_id UUID NOT NULL REFERENCES teacher_timetables(id) ON DELETE CASCADE,
|
||
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||
day_of_week TEXT NOT NULL,
|
||
period_code TEXT NOT NULL,
|
||
subject_class TEXT NOT NULL,
|
||
start_time TEXT NOT NULL,
|
||
end_time TEXT NOT NULL,
|
||
neo4j_node_id TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||
UNIQUE (teacher_timetable_id, day_of_week, period_code)
|
||
);
|
||
|
||
-- ============================================================
|
||
-- Indexes
|
||
-- ============================================================
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_school_tt_institute ON school_timetables(institute_id);
|
||
CREATE INDEX IF NOT EXISTS idx_academic_years_tt ON academic_years(school_timetable_id);
|
||
CREATE INDEX IF NOT EXISTS idx_academic_years_inst ON academic_years(institute_id);
|
||
CREATE INDEX IF NOT EXISTS idx_academic_terms_year ON academic_terms(academic_year_id);
|
||
CREATE INDEX IF NOT EXISTS idx_academic_terms_inst ON academic_terms(institute_id);
|
||
CREATE INDEX IF NOT EXISTS idx_academic_weeks_term ON academic_weeks(academic_term_id);
|
||
CREATE INDEX IF NOT EXISTS idx_academic_weeks_inst ON academic_weeks(institute_id);
|
||
CREATE INDEX IF NOT EXISTS idx_academic_days_week ON academic_days(academic_week_id);
|
||
CREATE INDEX IF NOT EXISTS idx_academic_days_term ON academic_days(academic_term_id);
|
||
CREATE INDEX IF NOT EXISTS idx_academic_days_inst_date ON academic_days(institute_id, date);
|
||
CREATE INDEX IF NOT EXISTS idx_teacher_tt_profile ON teacher_timetables(profile_id);
|
||
CREATE INDEX IF NOT EXISTS idx_teacher_tt_inst ON teacher_timetables(institute_id);
|
||
CREATE INDEX IF NOT EXISTS idx_tt_slots_timetable ON teacher_timetable_slots(teacher_timetable_id);
|
||
CREATE INDEX IF NOT EXISTS idx_tt_slots_profile ON teacher_timetable_slots(profile_id);
|
||
|
||
-- ============================================================
|
||
-- updated_at triggers (tables that have updated_at)
|
||
-- ============================================================
|
||
|
||
DO $$ DECLARE t TEXT; BEGIN
|
||
FOREACH t IN ARRAY ARRAY[
|
||
'school_timetables', 'teacher_timetables', 'teacher_timetable_slots'
|
||
] LOOP
|
||
EXECUTE format(
|
||
'DROP TRIGGER IF EXISTS trg_updated_at ON %I;
|
||
CREATE TRIGGER trg_updated_at
|
||
BEFORE UPDATE ON %I
|
||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();',
|
||
t, t
|
||
);
|
||
END LOOP;
|
||
END $$;
|
||
|
||
-- ============================================================
|
||
-- Row Level Security
|
||
-- ============================================================
|
||
|
||
ALTER TABLE school_timetables ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE academic_years ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE academic_terms ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE academic_weeks ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE academic_days ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE teacher_timetables ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE teacher_timetable_slots ENABLE ROW LEVEL SECURITY;
|
||
|
||
-- school_timetables: institute members can read
|
||
CREATE POLICY "stt_inst_read" ON school_timetables FOR SELECT
|
||
USING (institute_id IN (
|
||
SELECT institute_id FROM institute_memberships WHERE profile_id = auth.uid()
|
||
));
|
||
CREATE POLICY "stt_service" ON school_timetables FOR ALL
|
||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||
|
||
-- academic_years
|
||
CREATE POLICY "ay_inst_read" ON academic_years FOR SELECT
|
||
USING (institute_id IN (
|
||
SELECT institute_id FROM institute_memberships WHERE profile_id = auth.uid()
|
||
));
|
||
CREATE POLICY "ay_service" ON academic_years FOR ALL
|
||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||
|
||
-- academic_terms
|
||
CREATE POLICY "at_inst_read" ON academic_terms FOR SELECT
|
||
USING (institute_id IN (
|
||
SELECT institute_id FROM institute_memberships WHERE profile_id = auth.uid()
|
||
));
|
||
CREATE POLICY "at_service" ON academic_terms FOR ALL
|
||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||
|
||
-- academic_weeks
|
||
CREATE POLICY "aw_inst_read" ON academic_weeks FOR SELECT
|
||
USING (institute_id IN (
|
||
SELECT institute_id FROM institute_memberships WHERE profile_id = auth.uid()
|
||
));
|
||
CREATE POLICY "aw_service" ON academic_weeks FOR ALL
|
||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||
|
||
-- academic_days
|
||
CREATE POLICY "ad_inst_read" ON academic_days FOR SELECT
|
||
USING (institute_id IN (
|
||
SELECT institute_id FROM institute_memberships WHERE profile_id = auth.uid()
|
||
));
|
||
CREATE POLICY "ad_service" ON academic_days FOR ALL
|
||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||
|
||
-- teacher_timetables: own row
|
||
CREATE POLICY "tcht_own_read" ON teacher_timetables FOR SELECT
|
||
USING (profile_id = auth.uid());
|
||
CREATE POLICY "tcht_service" ON teacher_timetables FOR ALL
|
||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||
|
||
-- teacher_timetable_slots: own rows
|
||
CREATE POLICY "tchts_own_read" ON teacher_timetable_slots FOR SELECT
|
||
USING (profile_id = auth.uid());
|
||
CREATE POLICY "tchts_service" ON teacher_timetable_slots FOR ALL
|
||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|