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