Files
triqura-ecd/supabase/migrations/20251122_screening_intake_schema.sql
2025-11-23 10:13:00 +01:00

423 lines
16 KiB
PL/PgSQL

-- ============================================================================
-- SCREENING & INTAKE SCHEMA - Mini EPD v1.2
-- ============================================================================
-- Created: 2025-11-22
-- Purpose: Complete database schema for screening and intake workflow
-- Based on: FO v1.0 Chapter 10 (Data Requirements)
--
-- Architecture decisions:
-- - Separate tables for reusable content (anamneses, examinations, risk_assessments)
-- - JSONB for context-bound data (kindcheck, treatment_advice)
-- - Reuse existing FHIR tables where possible (observations, conditions, encounters)
-- ============================================================================
-- ============================================================================
-- STEP 1: Add patient status for EpisodeOfCare workflow
-- ============================================================================
-- Create enum for patient/episode status (FHIR EpisodeOfCare.status)
CREATE TYPE episode_status AS ENUM (
'planned', -- Aangemeld / In screening
'active', -- Intake t/m behandeling (in zorg)
'finished', -- Behandeling afgerond
'cancelled' -- Niet geschikt / afgemeld
);
-- Add status column to patients table
ALTER TABLE patients
ADD COLUMN IF NOT EXISTS status episode_status DEFAULT 'planned',
ADD COLUMN IF NOT EXISTS is_john_doe BOOLEAN DEFAULT false;
-- Add index for status filtering
CREATE INDEX IF NOT EXISTS idx_patients_status ON patients(status);
COMMENT ON COLUMN patients.status IS 'Episode of care status: planned (screening) → active (in zorg) → finished/cancelled';
COMMENT ON COLUMN patients.is_john_doe IS 'Crisis admission without complete personal data (BSN optional)';
-- ============================================================================
-- STEP 2: Screening module tables
-- ============================================================================
-- 2.1 Main screening table (FO 10.2)
CREATE TABLE IF NOT EXISTS screenings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
patient_id UUID NOT NULL REFERENCES patients(id) ON DELETE CASCADE,
-- Hulpvraag (FO 4.3 Sectie 3)
request_for_help TEXT,
-- Screeningsbesluit (FO 4.3 Sectie 4)
decision TEXT CHECK (decision IN ('geschikt', 'niet_geschikt')),
decision_department TEXT, -- Volwassenen, Jeugd, Forensisch, Verslaving, Ouderen, FACT
decision_notes TEXT,
decision_date TIMESTAMPTZ,
decision_by UUID REFERENCES practitioners(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_screenings_patient ON screenings(patient_id);
CREATE INDEX idx_screenings_decision ON screenings(decision);
COMMENT ON TABLE screenings IS 'Screening process per patient: request for help, activities, documents, and decision';
-- Enable RLS
ALTER TABLE screenings ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Enable read for authenticated users" ON screenings
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "Enable insert for authenticated users" ON screenings
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
CREATE POLICY "Enable update for authenticated users" ON screenings
FOR UPDATE USING (auth.role() = 'authenticated');
-- 2.2 Screening activities / timeline (FO 10.3)
CREATE TABLE IF NOT EXISTS screening_activities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
screening_id UUID NOT NULL REFERENCES screenings(id) ON DELETE CASCADE,
activity_text TEXT NOT NULL,
created_by UUID REFERENCES practitioners(id),
created_by_name TEXT, -- Denormalized for display
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_screening_activities_screening ON screening_activities(screening_id);
CREATE INDEX idx_screening_activities_created_at ON screening_activities(created_at DESC);
COMMENT ON TABLE screening_activities IS 'Activity log / timeline during screening process (FO 4.3 Sectie 1)';
-- Enable RLS
ALTER TABLE screening_activities ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Enable read for authenticated users" ON screening_activities
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "Enable insert for authenticated users" ON screening_activities
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
-- 2.3 Screening documents (FO 10.4)
CREATE TABLE IF NOT EXISTS screening_documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
screening_id UUID NOT NULL REFERENCES screenings(id) ON DELETE CASCADE,
file_name TEXT NOT NULL,
file_type TEXT, -- PDF, DOC, DOCX, JPG, PNG
file_size INTEGER, -- bytes
file_path TEXT, -- Storage path or URL
document_type TEXT CHECK (document_type IN ('verwijsbrief', 'verhuisbericht', 'indicatie', 'overig')),
uploaded_by UUID REFERENCES practitioners(id),
uploaded_by_name TEXT, -- Denormalized
uploaded_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_screening_documents_screening ON screening_documents(screening_id);
COMMENT ON TABLE screening_documents IS 'Uploaded documents during screening (referral letters, etc.)';
-- Enable RLS
ALTER TABLE screening_documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Enable read for authenticated users" ON screening_documents
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "Enable insert for authenticated users" ON screening_documents
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
CREATE POLICY "Enable delete for authenticated users" ON screening_documents
FOR DELETE USING (auth.role() = 'authenticated');
-- ============================================================================
-- STEP 3: Intake module tables
-- ============================================================================
-- 3.1 Main intake table (FO 10.5)
CREATE TABLE IF NOT EXISTS intakes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
patient_id UUID NOT NULL REFERENCES patients(id) ON DELETE CASCADE,
-- Intake metadata
title TEXT NOT NULL, -- "Aanvang zorg", "Overplaatsing Forensisch"
department TEXT NOT NULL, -- Volwassenen, Jeugd, Forensisch, etc.
psychologist_id UUID REFERENCES practitioners(id),
-- Status and dates
status TEXT NOT NULL DEFAULT 'bezig' CHECK (status IN ('bezig', 'afgerond')),
start_date DATE NOT NULL DEFAULT CURRENT_DATE,
end_date DATE,
-- General notes (FO 4.4.3)
notes TEXT,
-- Context-bound data (JSONB for flexibility)
kindcheck_data JSONB DEFAULT '{}',
treatment_advice JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_intakes_patient ON intakes(patient_id);
CREATE INDEX idx_intakes_status ON intakes(status);
CREATE INDEX idx_intakes_psychologist ON intakes(psychologist_id);
COMMENT ON TABLE intakes IS 'Intake sessions per patient (multiple possible per patient, e.g., department transfer)';
COMMENT ON COLUMN intakes.kindcheck_data IS 'Child safety check data (JSONB): children_present, ages, concerns, actions';
COMMENT ON COLUMN intakes.treatment_advice IS 'Treatment advice (JSONB): advice_text, target_department, care_program';
-- Enable RLS
ALTER TABLE intakes ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Enable read for authenticated users" ON intakes
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "Enable insert for authenticated users" ON intakes
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
CREATE POLICY "Enable update for authenticated users" ON intakes
FOR UPDATE USING (auth.role() = 'authenticated');
-- 3.2 Extend encounters table for intake contacts (FO 10.6)
-- Reuse existing FHIR encounters table, add intake_id reference
ALTER TABLE encounters
ADD COLUMN IF NOT EXISTS intake_id UUID REFERENCES intakes(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_encounters_intake ON encounters(intake_id);
COMMENT ON COLUMN encounters.intake_id IS 'Link encounter to specific intake (null for other encounter types)';
-- 3.3 Anamneses table (FO 10.9) - Separate for reusability
CREATE TABLE IF NOT EXISTS anamneses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
intake_id UUID NOT NULL REFERENCES intakes(id) ON DELETE CASCADE,
anamnese_date DATE NOT NULL DEFAULT CURRENT_DATE,
anamnese_type TEXT NOT NULL CHECK (anamnese_type IN (
'psychiatrisch',
'sociaal',
'medisch',
'familie',
'ontwikkeling',
'overig'
)),
content TEXT NOT NULL, -- Rich text / markdown
notes TEXT,
created_by UUID REFERENCES practitioners(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_anamneses_intake ON anamneses(intake_id);
CREATE INDEX idx_anamneses_type ON anamneses(anamnese_type);
COMMENT ON TABLE anamneses IS 'Anamnesis records (psychiatric, social, medical, family, developmental) - reusable in care plans';
-- Enable RLS
ALTER TABLE anamneses ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Enable read for authenticated users" ON anamneses
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "Enable insert for authenticated users" ON anamneses
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
CREATE POLICY "Enable update for authenticated users" ON anamneses
FOR UPDATE USING (auth.role() = 'authenticated');
-- 3.4 Examinations table (FO 10.10) - Separate for reusability
CREATE TABLE IF NOT EXISTS examinations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
intake_id UUID NOT NULL REFERENCES intakes(id) ON DELETE CASCADE,
examination_date DATE NOT NULL DEFAULT CURRENT_DATE,
examination_type TEXT NOT NULL CHECK (examination_type IN (
'bloedonderzoek',
'neuropsychologisch',
'psychodiagnostiek',
'iq_test',
'persoonlijkheid',
'medisch', -- EEG, ECG, etc.
'overig'
)),
performed_by TEXT,
reason TEXT,
findings TEXT NOT NULL,
document_url TEXT, -- Link to uploaded report
notes TEXT,
created_by UUID REFERENCES practitioners(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_examinations_intake ON examinations(intake_id);
CREATE INDEX idx_examinations_type ON examinations(examination_type);
COMMENT ON TABLE examinations IS 'Medical and psychological examinations - reusable in care plans';
-- Enable RLS
ALTER TABLE examinations ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Enable read for authenticated users" ON examinations
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "Enable insert for authenticated users" ON examinations
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
CREATE POLICY "Enable update for authenticated users" ON examinations
FOR UPDATE USING (auth.role() = 'authenticated');
-- 3.5 Risk assessments table (FO 10.8) - Separate for history tracking
CREATE TABLE IF NOT EXISTS risk_assessments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
intake_id UUID NOT NULL REFERENCES intakes(id) ON DELETE CASCADE,
assessment_date DATE NOT NULL DEFAULT CURRENT_DATE,
risk_type TEXT NOT NULL CHECK (risk_type IN (
'suicidaliteit',
'agressie',
'zelfverwaarlozing',
'middelenmisbruik',
'verward_gedrag',
'overig'
)),
risk_level TEXT NOT NULL CHECK (risk_level IN ('laag', 'gemiddeld', 'hoog', 'zeer_hoog')),
rationale TEXT NOT NULL,
measures TEXT, -- Maatregelen
evaluation_date DATE,
notes TEXT,
created_by UUID REFERENCES practitioners(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_risk_assessments_intake ON risk_assessments(intake_id);
CREATE INDEX idx_risk_assessments_type ON risk_assessments(risk_type);
CREATE INDEX idx_risk_assessments_level ON risk_assessments(risk_level);
COMMENT ON TABLE risk_assessments IS 'Risk assessments (suicide, aggression, etc.) with history tracking';
-- Enable RLS
ALTER TABLE risk_assessments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Enable read for authenticated users" ON risk_assessments
FOR SELECT USING (auth.role() = 'authenticated');
CREATE POLICY "Enable insert for authenticated users" ON risk_assessments
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
CREATE POLICY "Enable update for authenticated users" ON risk_assessments
FOR UPDATE USING (auth.role() = 'authenticated');
-- ============================================================================
-- STEP 4: Extend care_plans for intake references
-- ============================================================================
-- Add columns to reference intake components in care plans
ALTER TABLE care_plans
ADD COLUMN IF NOT EXISTS based_on_intake_id UUID REFERENCES intakes(id),
ADD COLUMN IF NOT EXISTS based_on_anamneses UUID[],
ADD COLUMN IF NOT EXISTS based_on_examinations UUID[],
ADD COLUMN IF NOT EXISTS based_on_risk_assessments UUID[];
COMMENT ON COLUMN care_plans.based_on_intake_id IS 'Primary intake this care plan is based on';
COMMENT ON COLUMN care_plans.based_on_anamneses IS 'Array of anamnese IDs referenced in this care plan';
COMMENT ON COLUMN care_plans.based_on_examinations IS 'Array of examination IDs referenced in this care plan';
COMMENT ON COLUMN care_plans.based_on_risk_assessments IS 'Array of risk assessment IDs referenced in this care plan';
-- ============================================================================
-- STEP 5: Triggers for updated_at timestamps
-- ============================================================================
-- Function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Apply triggers
CREATE TRIGGER update_screenings_updated_at BEFORE UPDATE ON screenings
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_intakes_updated_at BEFORE UPDATE ON intakes
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_anamneses_updated_at BEFORE UPDATE ON anamneses
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_examinations_updated_at BEFORE UPDATE ON examinations
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_risk_assessments_updated_at BEFORE UPDATE ON risk_assessments
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================================
-- STEP 6: Helper views for common queries
-- ============================================================================
-- View: Active intakes with patient info
CREATE OR REPLACE VIEW active_intakes_overview AS
SELECT
i.id,
i.title,
i.status,
i.start_date,
i.end_date,
i.department,
p.name_family,
p.name_given,
p.birth_date,
pr.name_family as psychologist_name,
COUNT(DISTINCT e.id) as contact_count,
COUNT(DISTINCT c.id) as diagnosis_count
FROM intakes i
JOIN patients p ON i.patient_id = p.id
LEFT JOIN practitioners pr ON i.psychologist_id = pr.id
LEFT JOIN encounters e ON e.intake_id = i.id
LEFT JOIN conditions c ON c.patient_id = p.id AND c.encounter_id IN (
SELECT id FROM encounters WHERE intake_id = i.id
)
WHERE i.status = 'bezig'
GROUP BY i.id, p.name_family, p.name_given, p.birth_date, pr.name_family;
COMMENT ON VIEW active_intakes_overview IS 'Overview of active intakes with patient info and counts';
-- ============================================================================
-- SUMMARY
-- ============================================================================
-- This migration creates:
-- ✓ Patient status column (episode_status enum)
-- ✓ Screenings module (3 tables: screenings, activities, documents)
-- ✓ Intakes module (4 new tables + extend encounters)
-- - intakes (with JSONB for kindcheck, treatment_advice)
-- - anamneses (separate for reusability)
-- - examinations (separate for reusability)
-- - risk_assessments (separate for history)
-- ✓ Care plan extensions (reference intake components)
-- ✓ RLS policies for all tables
-- ✓ Indexes for performance
-- ✓ Helper views
--
-- Reuses existing FHIR tables:
-- ✓ observations (for ROM measurements)
-- ✓ conditions (for diagnoses/DSM-5)
-- ✓ encounters (extended with intake_id for contacts)
-- ✓ practitioners (for users/psychologists)
--
-- Total new tables: 7
-- Extended tables: 3 (patients, encounters, care_plans)
-- ============================================================================