From aac7efe2b91fe59358719f06f2a14d7b17ff1950 Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 22 Aug 2026 23:11:00 +0000 Subject: [PATCH] Terminal --- .../migrations/20260807000800_applicants.sql | 204 ------------------ 1 file changed, 204 deletions(-) delete mode 100644 supabase/migrations/20260807000800_applicants.sql diff --git a/supabase/migrations/20260807000800_applicants.sql b/supabase/migrations/20260807000800_applicants.sql deleted file mode 100644 index 4a84c9a..0000000 --- a/supabase/migrations/20260807000800_applicants.sql +++ /dev/null @@ -1,204 +0,0 @@ --- Candidate and applicant tracking — spec section 12. --- --- Section 12 sits between staff compliance (11) and administrative calendars --- (13), and the spec's own role list treats employee records as confidential --- management data. This is therefore modelled as hiring: candidates for staff --- positions, moving through a configurable pipeline. --- --- Student enrolment applications are already served by the intake_tokens / --- forms path from the earlier migrations, so they are not duplicated here. - -CREATE TABLE IF NOT EXISTS public.applicant_stages ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL UNIQUE, - slug TEXT NOT NULL UNIQUE, - sort_order INTEGER NOT NULL DEFAULT 100, - is_terminal BOOLEAN NOT NULL DEFAULT FALSE, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - -GRANT SELECT, INSERT, UPDATE, DELETE ON public.applicant_stages TO authenticated; -GRANT ALL ON public.applicant_stages TO service_role; -ALTER TABLE public.applicant_stages ENABLE ROW LEVEL SECURITY; - -INSERT INTO public.applicant_stages (name, slug, sort_order, is_terminal) VALUES - ('Applied', 'applied', 10, FALSE), - ('Screening', 'screening', 20, FALSE), - ('Interview', 'interview', 30, FALSE), - ('Reference check', 'reference_check', 40, FALSE), - ('Background check', 'background_check', 50, FALSE), - ('Offer', 'offer', 60, FALSE), - ('Hired', 'hired', 70, TRUE), - ('Declined', 'declined', 80, TRUE), - ('Withdrawn', 'withdrawn', 90, TRUE) -ON CONFLICT (slug) DO NOTHING; - -CREATE TABLE IF NOT EXISTS public.applicants ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - first_name TEXT NOT NULL, - last_name TEXT NOT NULL, - email TEXT, - phone TEXT, - - position_applied TEXT NOT NULL, - campus_id UUID REFERENCES public.campuses(id) ON DELETE SET NULL, - stage_id UUID REFERENCES public.applicant_stages(id) ON DELETE SET NULL, - - source TEXT, - applied_on DATE NOT NULL DEFAULT CURRENT_DATE, - resume_path TEXT, - desired_start_date DATE, - desired_pay_cents INTEGER, - - -- Set when a candidate is hired and becomes a user, linking the two records. - hired_user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, - hired_on DATE, - - rating SMALLINT CHECK (rating IS NULL OR (rating BETWEEN 1 AND 5)), - is_active BOOLEAN NOT NULL DEFAULT TRUE, - - created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - -GRANT SELECT, INSERT, UPDATE, DELETE ON public.applicants TO authenticated; -GRANT ALL ON public.applicants TO service_role; -ALTER TABLE public.applicants ENABLE ROW LEVEL SECURITY; - -CREATE INDEX IF NOT EXISTS app_stage_idx ON public.applicants (stage_id); -CREATE INDEX IF NOT EXISTS app_campus_idx ON public.applicants (campus_id); -CREATE INDEX IF NOT EXISTS app_hired_user_idx ON public.applicants (hired_user_id); -CREATE INDEX IF NOT EXISTS app_open_idx ON public.applicants (applied_on DESC) WHERE is_active; - -DROP TRIGGER IF EXISTS trg_app_upd ON public.applicants; -CREATE TRIGGER trg_app_upd BEFORE UPDATE ON public.applicants - FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); - --- Stage history, so "how long did this sit in screening" is answerable. -CREATE TABLE IF NOT EXISTS public.applicant_stage_history ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - applicant_id UUID NOT NULL REFERENCES public.applicants(id) ON DELETE CASCADE, - from_stage_id UUID REFERENCES public.applicant_stages(id) ON DELETE SET NULL, - to_stage_id UUID REFERENCES public.applicant_stages(id) ON DELETE SET NULL, - changed_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, - changed_at TIMESTAMPTZ NOT NULL DEFAULT now(), - note TEXT -); - -GRANT SELECT, INSERT ON public.applicant_stage_history TO authenticated; -GRANT ALL ON public.applicant_stage_history TO service_role; -ALTER TABLE public.applicant_stage_history ENABLE ROW LEVEL SECURITY; -CREATE INDEX IF NOT EXISTS ash_applicant_idx ON public.applicant_stage_history (applicant_id, changed_at DESC); -CREATE INDEX IF NOT EXISTS ash_from_idx ON public.applicant_stage_history (from_stage_id); -CREATE INDEX IF NOT EXISTS ash_to_idx ON public.applicant_stage_history (to_stage_id); - -CREATE OR REPLACE FUNCTION public.record_applicant_stage_change() -RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ -BEGIN - IF TG_OP = 'UPDATE' AND NEW.stage_id IS DISTINCT FROM OLD.stage_id THEN - INSERT INTO public.applicant_stage_history (applicant_id, from_stage_id, to_stage_id, changed_by) - VALUES (NEW.id, OLD.stage_id, NEW.stage_id, (SELECT auth.uid())); - END IF; - RETURN NEW; -END; -$$; - -DROP TRIGGER IF EXISTS trg_app_stage_history ON public.applicants; -CREATE TRIGGER trg_app_stage_history AFTER UPDATE ON public.applicants - FOR EACH ROW EXECUTE FUNCTION public.record_applicant_stage_change(); - -CREATE TABLE IF NOT EXISTS public.applicant_notes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - applicant_id UUID NOT NULL REFERENCES public.applicants(id) ON DELETE CASCADE, - author_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, - body TEXT NOT NULL, - is_confidential BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - -GRANT SELECT, INSERT, UPDATE, DELETE ON public.applicant_notes TO authenticated; -GRANT ALL ON public.applicant_notes TO service_role; -ALTER TABLE public.applicant_notes ENABLE ROW LEVEL SECURITY; -CREATE INDEX IF NOT EXISTS an_applicant_idx ON public.applicant_notes (applicant_id, created_at DESC); -CREATE INDEX IF NOT EXISTS an_author_idx ON public.applicant_notes (author_id); - -CREATE TABLE IF NOT EXISTS public.applicant_documents ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - applicant_id UUID NOT NULL REFERENCES public.applicants(id) ON DELETE CASCADE, - file_path TEXT NOT NULL, - title TEXT, - document_type TEXT, - uploaded_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - -GRANT SELECT, INSERT, UPDATE, DELETE ON public.applicant_documents TO authenticated; -GRANT ALL ON public.applicant_documents TO service_role; -ALTER TABLE public.applicant_documents ENABLE ROW LEVEL SECURITY; -CREATE INDEX IF NOT EXISTS ad_applicant_idx ON public.applicant_documents (applicant_id); - --- ============================================================================ --- POLICIES --- ============================================================================ --- Hiring data is management-only throughout. Campus admins see candidates for --- their own campus; teachers and staff see nothing. - -DROP POLICY IF EXISTS "applicant stages read" ON public.applicant_stages; -CREATE POLICY "applicant stages read" ON public.applicant_stages FOR SELECT TO authenticated - USING (public.is_management() OR public.current_user_has_any_role(ARRAY['campus_admin']::app_role[])); -DROP POLICY IF EXISTS "applicant stages manage" ON public.applicant_stages; -CREATE POLICY "applicant stages manage" ON public.applicant_stages FOR ALL TO authenticated - USING (public.is_org_admin()) WITH CHECK (public.is_org_admin()); - -DROP POLICY IF EXISTS "applicants read" ON public.applicants; -CREATE POLICY "applicants read" ON public.applicants FOR SELECT TO authenticated - USING ( - public.is_management() OR public.is_auditor() - OR (public.current_user_has_any_role(ARRAY['campus_admin']::app_role[]) - AND campus_id IN (SELECT public.user_campus_ids())) - ); - -DROP POLICY IF EXISTS "applicants manage" ON public.applicants; -CREATE POLICY "applicants manage" ON public.applicants FOR ALL TO authenticated - USING ( - public.is_management() - OR (public.current_user_has_any_role(ARRAY['campus_admin']::app_role[]) - AND campus_id IN (SELECT public.user_campus_ids())) - ) - WITH CHECK ( - public.is_management() - OR (public.current_user_has_any_role(ARRAY['campus_admin']::app_role[]) - AND campus_id IN (SELECT public.user_campus_ids())) - ); - -DROP POLICY IF EXISTS "applicant history read" ON public.applicant_stage_history; -CREATE POLICY "applicant history read" ON public.applicant_stage_history FOR SELECT TO authenticated - USING (EXISTS (SELECT 1 FROM public.applicants a WHERE a.id = applicant_id)); -DROP POLICY IF EXISTS "applicant history insert" ON public.applicant_stage_history; -CREATE POLICY "applicant history insert" ON public.applicant_stage_history FOR INSERT TO authenticated - WITH CHECK (public.is_management()); - --- Confidential notes stay with management even when a campus admin can --- otherwise see the candidate. -DROP POLICY IF EXISTS "applicant notes read" ON public.applicant_notes; -CREATE POLICY "applicant notes read" ON public.applicant_notes FOR SELECT TO authenticated - USING ( - public.is_management() - OR (NOT is_confidential - AND EXISTS (SELECT 1 FROM public.applicants a WHERE a.id = applicant_id)) - ); - -DROP POLICY IF EXISTS "applicant notes write" ON public.applicant_notes; -CREATE POLICY "applicant notes write" ON public.applicant_notes FOR ALL TO authenticated - USING (public.is_management() OR author_id = (SELECT auth.uid())) - WITH CHECK (public.is_management() OR author_id = (SELECT auth.uid())); - -DROP POLICY IF EXISTS "applicant docs read" ON public.applicant_documents; -CREATE POLICY "applicant docs read" ON public.applicant_documents FOR SELECT TO authenticated - USING (EXISTS (SELECT 1 FROM public.applicants a WHERE a.id = applicant_id)); -DROP POLICY IF EXISTS "applicant docs manage" ON public.applicant_documents; -CREATE POLICY "applicant docs manage" ON public.applicant_documents FOR ALL TO authenticated - USING (public.is_management()) WITH CHECK (public.is_management());