Implements complete database schema for screening and intake workflow: Database additions: - 7 new tables: screenings, screening_activities, screening_documents, intakes, anamneses, examinations, risk_assessments - Extended patients table with status (episode_status) and is_john_doe - Extended encounters table with intake_id for linking - Extended care_plans with intake reference columns Architecture: - Separate tables for reusable content (anamneses, examinations, risk_assessments) - JSONB fields for context-bound data (kindcheck_data, treatment_advice) - RLS policies, indexes, and updated_at triggers on all new tables - Helper view: active_intakes_overview Generated TypeScript types from schema for type-safe database access. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
423 lines
16 KiB
PL/PgSQL
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)
|
|
-- ============================================================================
|