Modified by www.SourceFiles.app

This commit is contained in:
2026-08-07 04:43:28 +00:00
parent a37dcde4fe
commit f888ca67b9
@@ -0,0 +1,194 @@
-- Staff compliance and certification tracking — spec section 11.
--
-- Certification *types* are org-level reference data; a requirement binds a
-- type to a role and optionally to a campus, so a campus with an infant room
-- can demand credentials the others do not.
--
-- Employee records are explicitly named in section 3 as something teachers must
-- not have access to, so everything here is readable only by the person it
-- concerns plus management-level roles.
CREATE TABLE IF NOT EXISTS public.certification_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
slug TEXT NOT NULL UNIQUE,
issuing_body TEXT,
description TEXT,
-- NULL means the credential does not expire.
validity_months INTEGER CHECK (validity_months IS NULL OR validity_months > 0),
renewal_reminder_days INTEGER NOT NULL DEFAULT 60,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
GRANT SELECT, INSERT, UPDATE, DELETE ON public.certification_types TO authenticated;
GRANT ALL ON public.certification_types TO service_role;
ALTER TABLE public.certification_types ENABLE ROW LEVEL SECURITY;
CREATE TABLE IF NOT EXISTS public.compliance_requirements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
certification_type_id UUID NOT NULL REFERENCES public.certification_types(id) ON DELETE CASCADE,
campus_id UUID REFERENCES public.campuses(id) ON DELETE CASCADE,
applies_to_roles app_role[] NOT NULL DEFAULT ARRAY['teacher','staff']::app_role[],
is_mandatory BOOLEAN NOT NULL DEFAULT TRUE,
grace_period_days INTEGER NOT NULL DEFAULT 0,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (certification_type_id, campus_id)
);
GRANT SELECT, INSERT, UPDATE, DELETE ON public.compliance_requirements TO authenticated;
GRANT ALL ON public.compliance_requirements TO service_role;
ALTER TABLE public.compliance_requirements ENABLE ROW LEVEL SECURITY;
CREATE INDEX IF NOT EXISTS creq_type_idx ON public.compliance_requirements (certification_type_id);
CREATE INDEX IF NOT EXISTS creq_campus_idx ON public.compliance_requirements (campus_id);
CREATE TABLE IF NOT EXISTS public.staff_certifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
certification_type_id UUID NOT NULL REFERENCES public.certification_types(id) ON DELETE RESTRICT,
certificate_number TEXT,
issued_on DATE,
expires_on DATE,
document_path TEXT,
status TEXT NOT NULL DEFAULT 'active',
verified_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
verified_at TIMESTAMPTZ,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT stc_status_valid CHECK (status IN ('pending','active','expired','revoked')),
CONSTRAINT stc_dates_ordered CHECK (issued_on IS NULL OR expires_on IS NULL OR issued_on <= expires_on)
);
GRANT SELECT, INSERT, UPDATE, DELETE ON public.staff_certifications TO authenticated;
GRANT ALL ON public.staff_certifications TO service_role;
ALTER TABLE public.staff_certifications ENABLE ROW LEVEL SECURITY;
CREATE INDEX IF NOT EXISTS stc_user_idx ON public.staff_certifications (user_id);
CREATE INDEX IF NOT EXISTS stc_type_idx ON public.staff_certifications (certification_type_id);
-- The compliance dashboard's central query: what lapses soon.
CREATE INDEX IF NOT EXISTS stc_expiring_idx ON public.staff_certifications (expires_on)
WHERE status = 'active' AND expires_on IS NOT NULL;
DROP TRIGGER IF EXISTS trg_stc_upd ON public.staff_certifications;
CREATE TRIGGER trg_stc_upd BEFORE UPDATE ON public.staff_certifications
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
-- Derive expiry from the certification type when the user did not supply one.
CREATE OR REPLACE FUNCTION public.derive_certification_expiry()
RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
DECLARE
months INTEGER;
BEGIN
IF NEW.expires_on IS NULL AND NEW.issued_on IS NOT NULL THEN
SELECT validity_months INTO months
FROM public.certification_types WHERE id = NEW.certification_type_id;
IF months IS NOT NULL THEN
NEW.expires_on := NEW.issued_on + (months || ' months')::interval;
END IF;
END IF;
-- Keep status honest without waiting for a nightly job.
IF NEW.expires_on IS NOT NULL AND NEW.expires_on < CURRENT_DATE
AND NEW.status = 'active' THEN
NEW.status := 'expired';
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_stc_expiry ON public.staff_certifications;
CREATE TRIGGER trg_stc_expiry BEFORE INSERT OR UPDATE ON public.staff_certifications
FOR EACH ROW EXECUTE FUNCTION public.derive_certification_expiry();
-- Requirements a user has not satisfied: missing entirely, expired, or lapsing
-- inside the reminder window.
CREATE OR REPLACE FUNCTION public.staff_compliance_gaps(_user UUID)
RETURNS TABLE (
certification_type_id UUID, certification_name TEXT, campus_id UUID,
gap_kind TEXT, expires_on DATE
)
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$
SELECT
t.id, t.name, req.campus_id,
CASE
WHEN c.id IS NULL THEN 'missing'
WHEN c.status = 'revoked' THEN 'revoked'
WHEN c.expires_on IS NOT NULL AND c.expires_on < CURRENT_DATE THEN 'expired'
ELSE 'expiring_soon'
END,
c.expires_on
FROM public.compliance_requirements req
JOIN public.certification_types t ON t.id = req.certification_type_id
-- The requirement applies only if the user holds one of its roles, and (when
-- campus-specific) is assigned to that campus.
JOIN public.user_roles ur ON ur.user_id = _user AND ur.role = ANY(req.applies_to_roles)
LEFT JOIN public.staff_campus_assignments sca
ON sca.user_id = _user AND sca.campus_id = req.campus_id
LEFT JOIN LATERAL (
SELECT sc.* FROM public.staff_certifications sc
WHERE sc.user_id = _user AND sc.certification_type_id = t.id
AND sc.status IN ('active','pending')
ORDER BY sc.expires_on DESC NULLS FIRST
LIMIT 1
) c ON TRUE
WHERE req.is_mandatory
AND (req.campus_id IS NULL OR sca.id IS NOT NULL)
AND (
c.id IS NULL
OR c.status = 'revoked'
OR (c.expires_on IS NOT NULL
AND c.expires_on < CURRENT_DATE + make_interval(days => t.renewal_reminder_days))
)
$$;
-- ============================================================================
-- POLICIES
-- ============================================================================
DROP POLICY IF EXISTS "cert types read" ON public.certification_types;
CREATE POLICY "cert types read" ON public.certification_types FOR SELECT TO authenticated
USING (public.is_management() OR public.is_auditor()
OR public.current_user_has_any_role(ARRAY['teacher','staff','campus_admin']::app_role[]));
DROP POLICY IF EXISTS "cert types manage" ON public.certification_types;
CREATE POLICY "cert types manage" ON public.certification_types FOR ALL TO authenticated
USING (public.is_org_admin()) WITH CHECK (public.is_org_admin());
DROP POLICY IF EXISTS "compliance req read" ON public.compliance_requirements;
CREATE POLICY "compliance req read" ON public.compliance_requirements FOR SELECT TO authenticated
USING (public.is_management() OR public.is_auditor()
OR public.current_user_has_any_role(ARRAY['teacher','staff','campus_admin']::app_role[]));
DROP POLICY IF EXISTS "compliance req manage" ON public.compliance_requirements;
CREATE POLICY "compliance req manage" ON public.compliance_requirements FOR ALL TO authenticated
USING (public.is_org_admin()) WITH CHECK (public.is_org_admin());
-- Own record, management, or the campus admin of a campus this person works at.
DROP POLICY IF EXISTS "staff certs read" ON public.staff_certifications;
CREATE POLICY "staff certs read" ON public.staff_certifications FOR SELECT TO authenticated
USING (
user_id = (SELECT auth.uid())
OR public.is_management() OR public.is_auditor()
OR (public.current_user_has_any_role(ARRAY['campus_admin']::app_role[])
AND EXISTS (SELECT 1 FROM public.staff_campus_assignments a
WHERE a.user_id = staff_certifications.user_id
AND a.campus_id IN (SELECT public.user_campus_ids())))
);
-- Staff may upload their own credentials; only management may verify them,
-- which is why verified_by/verified_at are management-writable in practice.
DROP POLICY IF EXISTS "staff certs self insert" ON public.staff_certifications;
CREATE POLICY "staff certs self insert" ON public.staff_certifications FOR INSERT TO authenticated
WITH CHECK (user_id = (SELECT auth.uid()) OR public.is_management());
DROP POLICY IF EXISTS "staff certs manage" ON public.staff_certifications;
CREATE POLICY "staff certs manage" ON public.staff_certifications FOR UPDATE TO authenticated
USING (public.is_management()) WITH CHECK (public.is_management());
DROP POLICY IF EXISTS "staff certs delete" ON public.staff_certifications;
CREATE POLICY "staff certs delete" ON public.staff_certifications FOR DELETE TO authenticated
USING (public.is_org_admin());