Terminal
This commit is contained in:
@@ -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());
|
||||
Reference in New Issue
Block a user