supabase/volumes/db/cc/63-academic-calendar.sql

226 lines
12 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- ============================================================
-- 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 (MonFri 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');