feat: migrate clients module to patients + add docs

This commit is contained in:
colinislit
2025-11-23 10:13:00 +01:00
parent 8e3925ea09
commit 6fcb9a0e7b
277 changed files with 9131 additions and 458 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,704 +0,0 @@
-- ============================================================================
-- PRAGMATIC FHIR GGZ EPD SCHEMA
-- ============================================================================
-- Created: 2024-11-21
-- Version: Pragmatic v2.0
-- Description: Simplified FHIR schema for prototype focused on data interoperability
--
-- Differences from full schema (20241121_fhir_ggz_schema.sql):
-- - Only 7 tables (6 FHIR resources + organizations)
-- - Goals embedded in care_plans.goals JSONB (not separate table)
-- - Activities embedded in care_plans.activities JSONB (not separate table)
-- - No medications, consents, flags, documents tables
-- - BSN placeholders (no encryption for demo)
-- - Keeps existing tables: clients, intake_notes, treatment_plans, ai_events
-- ============================================================================
-- ============================================================================
-- ENABLE EXTENSIONS
-- ============================================================================
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Note: pgcrypto not needed for pragmatic version (no BSN encryption)
-- ============================================================================
-- ENUM TYPES (for type safety)
-- ============================================================================
-- FHIR Gender
DO $$ BEGIN
CREATE TYPE gender_type AS ENUM ('male', 'female', 'other', 'unknown');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- FHIR Encounter Status
DO $$ BEGIN
CREATE TYPE encounter_status AS ENUM (
'planned', 'in-progress', 'on-hold', 'completed',
'cancelled', 'entered-in-error', 'unknown'
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- FHIR Condition Clinical Status
DO $$ BEGIN
CREATE TYPE condition_clinical_status AS ENUM (
'active', 'recurrence', 'relapse', 'inactive',
'remission', 'resolved', 'unknown'
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- FHIR Condition Verification Status
DO $$ BEGIN
CREATE TYPE condition_verification_status AS ENUM (
'unconfirmed', 'provisional', 'differential',
'confirmed', 'refuted', 'entered-in-error'
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- FHIR Observation Status
DO $$ BEGIN
CREATE TYPE observation_status AS ENUM (
'registered', 'preliminary', 'final', 'amended',
'corrected', 'cancelled', 'entered-in-error', 'unknown'
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- FHIR CarePlan Status
DO $$ BEGIN
CREATE TYPE careplan_status AS ENUM (
'draft', 'active', 'on-hold', 'revoked',
'completed', 'entered-in-error', 'unknown'
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- ============================================================================
-- TABLE: practitioners (FHIR: Practitioner)
-- Behandelaren/professionals
-- ============================================================================
CREATE TABLE IF NOT EXISTS practitioners (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- FHIR Practitioner fields
identifier_big TEXT UNIQUE, -- BIG-nummer (optioneel)
identifier_agb TEXT, -- AGB-code
-- Name (HumanName)
name_prefix TEXT, -- "Drs.", "Dr."
name_given TEXT[] NOT NULL, -- Voornamen
name_family TEXT NOT NULL, -- Achternaam
name_suffix TEXT, -- "PhD", "MSc"
-- Qualification
qualification TEXT[], -- ["GZ-psycholoog", "Psychotherapeut"]
-- Contact
telecom_phone TEXT,
telecom_email TEXT,
-- Active
active BOOLEAN DEFAULT true,
-- Link to auth user
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
-- Metadata
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE practitioners IS 'FHIR: Practitioner - Behandelaren en zorgprofessionals';
-- ============================================================================
-- TABLE: organizations (FHIR: Organization)
-- GGZ-instellingen
-- ============================================================================
CREATE TABLE IF NOT EXISTS organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- FHIR Organization fields
identifier_agb TEXT UNIQUE, -- AGB-code instelling
identifier_kvk TEXT, -- KVK-nummer
-- Name
name TEXT NOT NULL,
alias TEXT[], -- Alternative names
-- Type
type_code TEXT DEFAULT 'prov', -- healthcare provider
type_display TEXT DEFAULT 'Healthcare Provider',
-- Contact
telecom_phone TEXT,
telecom_email TEXT,
telecom_website TEXT,
-- Address
address_line TEXT[],
address_city TEXT,
address_postal_code TEXT,
address_country TEXT DEFAULT 'NL',
-- Active
active BOOLEAN DEFAULT true,
-- Metadata
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE organizations IS 'FHIR: Organization - GGZ-instellingen';
-- ============================================================================
-- TABLE: patients (FHIR: Patient / ZIB: Patient)
-- Cliënten/patiënten
-- ============================================================================
CREATE TABLE IF NOT EXISTS patients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- FHIR Patient.identifier
-- PRAGMATIC: No encryption, use placeholder BSN for demo
identifier_bsn TEXT DEFAULT '999999990', -- Placeholder BSN
identifier_client_number TEXT, -- Interne cliëntnummer
-- FHIR Patient.name (HumanName)
name_family TEXT NOT NULL, -- Achternaam
name_given TEXT[] NOT NULL, -- Voornamen array
name_prefix TEXT, -- Voorvoegsel (van, de, etc)
name_use TEXT DEFAULT 'official', -- official, maiden, nickname
-- FHIR Patient.birthDate
birth_date DATE NOT NULL,
-- FHIR Patient.gender
gender gender_type NOT NULL,
-- FHIR Patient.telecom (ContactPoint)
telecom_phone TEXT,
telecom_email TEXT,
-- FHIR Patient.address (Address)
address_line TEXT[], -- Straat + huisnummer
address_city TEXT,
address_postal_code TEXT,
address_country TEXT DEFAULT 'NL',
-- Insurance (ZIB: Payer)
insurance_company TEXT, -- Zorgverzekeraar
insurance_number TEXT, -- Polisnummer
-- FHIR Patient.contact (naasten)
emergency_contact_name TEXT,
emergency_contact_relationship TEXT,
emergency_contact_phone TEXT,
-- FHIR Patient.active
active BOOLEAN DEFAULT true,
-- FHIR Patient.generalPractitioner (huisarts)
general_practitioner_name TEXT,
general_practitioner_agb TEXT,
-- Metadata
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE patients IS 'FHIR: Patient / ZIB: Patient - Cliënten/patiënten (pragmatic version with placeholder BSN)';
COMMENT ON COLUMN patients.identifier_bsn IS 'Placeholder BSN for demo (not encrypted in pragmatic version)';
-- ============================================================================
-- TABLE: encounters (FHIR: Encounter / ZIB: Contact)
-- Contactmomenten (intake, behandelsessie, etc)
-- ============================================================================
CREATE TABLE IF NOT EXISTS encounters (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- FHIR Encounter.identifier
identifier TEXT UNIQUE DEFAULT gen_random_uuid()::TEXT,
-- FHIR Encounter.status
status encounter_status NOT NULL DEFAULT 'planned',
-- FHIR Encounter.class
class_code TEXT NOT NULL, -- AMB (ambulatory), IMP (inpatient), EMER (emergency), VR (virtual)
class_display TEXT NOT NULL,
-- FHIR Encounter.type
type_code TEXT NOT NULL, -- intake, diagnostiek, behandeling, follow-up, crisis
type_display TEXT NOT NULL,
-- FHIR Encounter.priority
priority_code TEXT, -- routine, urgent, emergency
priority_display TEXT,
-- FHIR Encounter.subject (patient)
patient_id UUID REFERENCES patients(id) ON DELETE CASCADE NOT NULL,
-- FHIR Encounter.participant (behandelaar)
practitioner_id UUID REFERENCES practitioners(id),
-- FHIR Encounter.serviceProvider (instelling)
organization_id UUID REFERENCES organizations(id),
-- FHIR Encounter.period
period_start TIMESTAMPTZ NOT NULL,
period_end TIMESTAMPTZ,
-- FHIR Encounter.reasonCode
reason_code TEXT[], -- DSM-5 codes, SNOMED codes
reason_display TEXT[], -- Human-readable reason
-- FHIR Encounter.hospitalization (indien opname)
admission_source TEXT,
discharge_disposition TEXT,
-- Free text notes
notes TEXT,
-- Link to intake_notes (existing table)
intake_note_id UUID REFERENCES intake_notes(id),
-- Metadata
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE encounters IS 'FHIR: Encounter / ZIB: Contact - Contactmomenten (intake, behandeling, etc)';
COMMENT ON COLUMN encounters.intake_note_id IS 'Link to existing intake_notes table for backwards compatibility';
-- ============================================================================
-- TABLE: conditions (FHIR: Condition / ZIB: Problem)
-- DSM-5 diagnoses en problemlijst
-- ============================================================================
CREATE TABLE IF NOT EXISTS conditions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- FHIR Condition.identifier
identifier TEXT UNIQUE DEFAULT gen_random_uuid()::TEXT,
-- FHIR Condition.clinicalStatus
clinical_status condition_clinical_status NOT NULL DEFAULT 'active',
-- FHIR Condition.verificationStatus
verification_status condition_verification_status NOT NULL DEFAULT 'provisional',
-- FHIR Condition.category
category TEXT NOT NULL DEFAULT 'encounter-diagnosis', -- of 'problem-list-item'
-- FHIR Condition.severity
severity_code TEXT, -- mild, moderate, severe
severity_display TEXT,
-- FHIR Condition.code (DSM-5 / ICD-10)
code_system TEXT NOT NULL DEFAULT 'http://hl7.org/fhir/sid/icd-10',
code_code TEXT NOT NULL, -- "F32.2", "F41.1"
code_display TEXT NOT NULL, -- "Depressieve episode, ernstig"
-- FHIR Condition.bodySite (indien relevant)
body_site_code TEXT,
body_site_display TEXT,
-- FHIR Condition.subject (patient)
patient_id UUID REFERENCES patients(id) ON DELETE CASCADE NOT NULL,
-- FHIR Condition.encounter (wanneer gesteld)
encounter_id UUID REFERENCES encounters(id),
-- FHIR Condition.onsetDateTime / abatementDateTime
onset_datetime TIMESTAMPTZ,
onset_age INTEGER, -- Leeftijd bij ontstaan (optioneel)
abatement_datetime TIMESTAMPTZ,
abatement_age INTEGER,
-- FHIR Condition.recordedDate
recorded_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- FHIR Condition.recorder (wie legde vast)
recorder_id UUID REFERENCES practitioners(id),
-- FHIR Condition.asserter (wie stelde diagnose)
asserter_id UUID REFERENCES practitioners(id),
-- FHIR Condition.note
note TEXT,
-- Metadata
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE conditions IS 'FHIR: Condition / ZIB: Problem - DSM-5 diagnoses en problemlijst';
-- ============================================================================
-- TABLE: observations (FHIR: Observation)
-- ROM-scores, risico's, klachten, metingen
-- ============================================================================
CREATE TABLE IF NOT EXISTS observations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- FHIR Observation.identifier
identifier TEXT UNIQUE DEFAULT gen_random_uuid()::TEXT,
-- FHIR Observation.status
status observation_status NOT NULL DEFAULT 'final',
-- FHIR Observation.category
category TEXT NOT NULL, -- vital-signs, social-history, exam, survey, therapy
-- FHIR Observation.code (wat werd geobserveerd)
code_system TEXT NOT NULL, -- SNOMED, LOINC, custom
code_code TEXT NOT NULL,
code_display TEXT NOT NULL,
-- FHIR Observation.subject (patient)
patient_id UUID REFERENCES patients(id) ON DELETE CASCADE NOT NULL,
-- FHIR Observation.encounter
encounter_id UUID REFERENCES encounters(id),
-- FHIR Observation.effectiveDateTime
effective_datetime TIMESTAMPTZ NOT NULL,
-- FHIR Observation.issued
issued TIMESTAMPTZ DEFAULT NOW(),
-- FHIR Observation.performer (wie deed observatie)
performer_id UUID REFERENCES practitioners(id),
-- FHIR Observation.value[x] (polymorf!)
value_type TEXT NOT NULL, -- quantity, string, boolean, codeableConcept
value_quantity_value NUMERIC,
value_quantity_unit TEXT,
value_quantity_comparator TEXT, -- <, <=, >=, >
value_string TEXT,
value_boolean BOOLEAN,
value_codeable_concept JSONB, -- {system, code, display}
-- FHIR Observation.interpretation
interpretation_code TEXT, -- H (high), L (low), N (normal)
interpretation_display TEXT,
-- FHIR Observation.note
note TEXT,
-- FHIR Observation.bodySite
body_site TEXT,
-- FHIR Observation.method
method_code TEXT,
method_display TEXT,
-- Reference range (normaalwaarden)
reference_range_low NUMERIC,
reference_range_high NUMERIC,
reference_range_text TEXT,
-- Metadata
created_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE observations IS 'FHIR: Observation - ROM-scores, risico-inschattingen, metingen';
-- ============================================================================
-- TABLE: care_plans (FHIR: CarePlan)
-- Behandelplannen met embedded goals en activities
-- ============================================================================
CREATE TABLE IF NOT EXISTS care_plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- FHIR CarePlan.identifier
identifier TEXT UNIQUE DEFAULT gen_random_uuid()::TEXT,
-- FHIR CarePlan.status
status careplan_status NOT NULL DEFAULT 'draft',
-- FHIR CarePlan.intent
intent TEXT NOT NULL DEFAULT 'plan', -- proposal, plan, order, option
-- FHIR CarePlan.category
category_code TEXT DEFAULT 'ggz-behandelplan',
category_display TEXT DEFAULT 'GGZ Behandelplan',
-- FHIR CarePlan.title
title TEXT NOT NULL,
-- FHIR CarePlan.description
description TEXT,
-- FHIR CarePlan.subject (patient)
patient_id UUID REFERENCES patients(id) ON DELETE CASCADE NOT NULL,
-- FHIR CarePlan.encounter (intake waar uit voortkomt)
encounter_id UUID REFERENCES encounters(id),
-- FHIR CarePlan.period
period_start DATE,
period_end DATE,
-- FHIR CarePlan.created
created_date TIMESTAMPTZ DEFAULT NOW(),
-- FHIR CarePlan.author (regiebehandelaar)
author_id UUID REFERENCES practitioners(id),
-- FHIR CarePlan.contributor
contributor_ids UUID[], -- Array van practitioner IDs
-- FHIR CarePlan.careTeam
care_team_ids UUID[], -- Array van practitioner IDs
-- FHIR CarePlan.addresses (welke diagnoses)
addresses_condition_ids UUID[], -- Array van condition IDs
-- ========================================================================
-- PRAGMATIC APPROACH: EMBEDDED GOALS AND ACTIVITIES
-- ========================================================================
-- FHIR CarePlan.goal (behandeldoelen als JSONB array)
goals JSONB DEFAULT '[]'::jsonb,
-- Structure: [
-- {
-- "description": {"text": "PHQ-9 score < 10"},
-- "target": [{
-- "measure": {"coding": [{"system": "...", "code": "44249-1"}]},
-- "detailQuantity": {"value": 10, "comparator": "<"},
-- "dueDate": "2024-06-30"
-- }]
-- }
-- ]
-- FHIR CarePlan.activity (behandelactiviteiten als JSONB array)
activities JSONB DEFAULT '[]'::jsonb,
-- Structure: [
-- {
-- "detail": {
-- "code": {"text": "Cognitieve gedragstherapie"},
-- "status": "in-progress",
-- "scheduledTiming": {"repeat": {"frequency": 1, "period": 1, "periodUnit": "wk"}},
-- "performer": ["practitioner-id"],
-- "description": "Individuele CGT sessies, 12 weken",
-- "location": "Polikliniek"
-- }
-- }
-- ]
-- ========================================================================
-- FHIR CarePlan.note
note TEXT,
-- Metadata
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE care_plans IS 'FHIR: CarePlan - Behandelplannen met embedded goals en activities (pragmatic approach)';
COMMENT ON COLUMN care_plans.goals IS 'FHIR Goal structures embedded as JSONB array (pragmatic: no separate table)';
COMMENT ON COLUMN care_plans.activities IS 'FHIR CarePlan.activity structures embedded as JSONB array (pragmatic: no separate table)';
-- ============================================================================
-- INDEXES (voor performance)
-- ============================================================================
-- Practitioners
CREATE INDEX IF NOT EXISTS idx_practitioners_user_id ON practitioners(user_id);
CREATE INDEX IF NOT EXISTS idx_practitioners_active ON practitioners(active);
-- Patients
CREATE INDEX IF NOT EXISTS idx_patients_bsn ON patients(identifier_bsn);
CREATE INDEX IF NOT EXISTS idx_patients_active ON patients(active);
CREATE INDEX IF NOT EXISTS idx_patients_client_number ON patients(identifier_client_number);
-- Encounters
CREATE INDEX IF NOT EXISTS idx_encounters_patient_id ON encounters(patient_id);
CREATE INDEX IF NOT EXISTS idx_encounters_practitioner_id ON encounters(practitioner_id);
CREATE INDEX IF NOT EXISTS idx_encounters_status ON encounters(status);
CREATE INDEX IF NOT EXISTS idx_encounters_period_start ON encounters(period_start DESC);
CREATE INDEX IF NOT EXISTS idx_encounters_intake_note_id ON encounters(intake_note_id);
-- Conditions
CREATE INDEX IF NOT EXISTS idx_conditions_patient_id ON conditions(patient_id);
CREATE INDEX IF NOT EXISTS idx_conditions_encounter_id ON conditions(encounter_id);
CREATE INDEX IF NOT EXISTS idx_conditions_clinical_status ON conditions(clinical_status);
CREATE INDEX IF NOT EXISTS idx_conditions_code ON conditions(code_code);
-- Observations
CREATE INDEX IF NOT EXISTS idx_observations_patient_id ON observations(patient_id);
CREATE INDEX IF NOT EXISTS idx_observations_encounter_id ON observations(encounter_id);
CREATE INDEX IF NOT EXISTS idx_observations_category ON observations(category);
CREATE INDEX IF NOT EXISTS idx_observations_effective_datetime ON observations(effective_datetime DESC);
CREATE INDEX IF NOT EXISTS idx_observations_code ON observations(code_code);
-- Care Plans
CREATE INDEX IF NOT EXISTS idx_care_plans_patient_id ON care_plans(patient_id);
CREATE INDEX IF NOT EXISTS idx_care_plans_status ON care_plans(status);
CREATE INDEX IF NOT EXISTS idx_care_plans_author_id ON care_plans(author_id);
CREATE INDEX IF NOT EXISTS idx_care_plans_encounter_id ON care_plans(encounter_id);
-- GIN indexes for JSONB columns (for efficient querying)
CREATE INDEX IF NOT EXISTS idx_care_plans_goals_gin ON care_plans USING GIN (goals);
CREATE INDEX IF NOT EXISTS idx_care_plans_activities_gin ON care_plans USING GIN (activities);
-- ============================================================================
-- ROW LEVEL SECURITY (RLS) - basis setup
-- ============================================================================
-- Enable RLS on all tables
ALTER TABLE practitioners ENABLE ROW LEVEL SECURITY;
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE patients ENABLE ROW LEVEL SECURITY;
ALTER TABLE encounters ENABLE ROW LEVEL SECURITY;
ALTER TABLE conditions ENABLE ROW LEVEL SECURITY;
ALTER TABLE observations ENABLE ROW LEVEL SECURITY;
ALTER TABLE care_plans ENABLE ROW LEVEL SECURITY;
-- Voor MVP: authenticated users kunnen alles zien (later verfijnen)
-- Practitioners: can read/update their own record
DROP POLICY IF EXISTS "Practitioners can view own record" ON practitioners;
CREATE POLICY "Practitioners can view own record" ON practitioners
FOR SELECT USING (user_id = auth.uid());
DROP POLICY IF EXISTS "Practitioners can update own record" ON practitioners;
CREATE POLICY "Practitioners can update own record" ON practitioners
FOR UPDATE USING (user_id = auth.uid());
-- Patients: authenticated users can view/create/update
DROP POLICY IF EXISTS "Authenticated users can view patients" ON patients;
CREATE POLICY "Authenticated users can view patients" ON patients
FOR SELECT USING (auth.role() = 'authenticated');
DROP POLICY IF EXISTS "Authenticated users can insert patients" ON patients;
CREATE POLICY "Authenticated users can insert patients" ON patients
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
DROP POLICY IF EXISTS "Authenticated users can update patients" ON patients;
CREATE POLICY "Authenticated users can update patients" ON patients
FOR UPDATE USING (auth.role() = 'authenticated');
-- Encounters: authenticated users can view/create/update
DROP POLICY IF EXISTS "Authenticated users can view encounters" ON encounters;
CREATE POLICY "Authenticated users can view encounters" ON encounters
FOR SELECT USING (auth.role() = 'authenticated');
DROP POLICY IF EXISTS "Authenticated users can insert encounters" ON encounters;
CREATE POLICY "Authenticated users can insert encounters" ON encounters
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
DROP POLICY IF EXISTS "Authenticated users can update encounters" ON encounters;
CREATE POLICY "Authenticated users can update encounters" ON encounters
FOR UPDATE USING (auth.role() = 'authenticated');
-- Conditions: authenticated users can view/create/update
DROP POLICY IF EXISTS "Authenticated users can view conditions" ON conditions;
CREATE POLICY "Authenticated users can view conditions" ON conditions
FOR SELECT USING (auth.role() = 'authenticated');
DROP POLICY IF EXISTS "Authenticated users can insert conditions" ON conditions;
CREATE POLICY "Authenticated users can insert conditions" ON conditions
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
DROP POLICY IF EXISTS "Authenticated users can update conditions" ON conditions;
CREATE POLICY "Authenticated users can update conditions" ON conditions
FOR UPDATE USING (auth.role() = 'authenticated');
-- Observations: authenticated users can view/create
DROP POLICY IF EXISTS "Authenticated users can view observations" ON observations;
CREATE POLICY "Authenticated users can view observations" ON observations
FOR SELECT USING (auth.role() = 'authenticated');
DROP POLICY IF EXISTS "Authenticated users can insert observations" ON observations;
CREATE POLICY "Authenticated users can insert observations" ON observations
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
-- Care Plans: authenticated users can view/create/update
DROP POLICY IF EXISTS "Authenticated users can view care plans" ON care_plans;
CREATE POLICY "Authenticated users can view care plans" ON care_plans
FOR SELECT USING (auth.role() = 'authenticated');
DROP POLICY IF EXISTS "Authenticated users can insert care plans" ON care_plans;
CREATE POLICY "Authenticated users can insert care plans" ON care_plans
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
DROP POLICY IF EXISTS "Authenticated users can update care plans" ON care_plans;
CREATE POLICY "Authenticated users can update care plans" ON care_plans
FOR UPDATE USING (auth.role() = 'authenticated');
-- ============================================================================
-- FUNCTIONS - updated_at trigger
-- ============================================================================
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Apply trigger to all tables with updated_at
DROP TRIGGER IF EXISTS set_updated_at ON practitioners;
CREATE TRIGGER set_updated_at BEFORE UPDATE ON practitioners
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
DROP TRIGGER IF EXISTS set_updated_at ON organizations;
CREATE TRIGGER set_updated_at BEFORE UPDATE ON organizations
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
DROP TRIGGER IF EXISTS set_updated_at ON patients;
CREATE TRIGGER set_updated_at BEFORE UPDATE ON patients
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
DROP TRIGGER IF EXISTS set_updated_at ON encounters;
CREATE TRIGGER set_updated_at BEFORE UPDATE ON encounters
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
DROP TRIGGER IF EXISTS set_updated_at ON conditions;
CREATE TRIGGER set_updated_at BEFORE UPDATE ON conditions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
DROP TRIGGER IF EXISTS set_updated_at ON care_plans;
CREATE TRIGGER set_updated_at BEFORE UPDATE ON care_plans
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================================
-- SEED DATA (demo/development)
-- ============================================================================
-- Default organization
INSERT INTO organizations (id, name, identifier_agb, active)
VALUES (
'00000000-0000-0000-0000-000000000001'::uuid,
'Demo GGZ Instelling',
'AGB-DEMO-001',
true
)
ON CONFLICT (id) DO NOTHING;
-- ============================================================================
-- SUMMARY
-- ============================================================================
-- ✅ Created 7 tables: practitioners, organizations, patients, encounters, conditions, observations, care_plans
-- ✅ Goals and activities embedded in care_plans JSONB (pragmatic approach)
-- ✅ No medications, consents, flags, documents tables (out of scope)
-- ✅ Simplified BSN (placeholder for demo)
-- ✅ RLS enabled on all tables
-- ✅ Indexes for performance
-- ✅ Triggers for updated_at
-- ✅ 1 default organization seeded
--
-- NEXT STEPS:
-- 1. Run this migration in Supabase
-- 2. Generate TypeScript types: supabase gen types
-- 3. Create data migration script: clients → patients, treatment_plans → care_plans
-- 4. Seed demo data: practitioners, patients, encounters
-- ============================================================================

View File

@@ -1,235 +0,0 @@
-- ============================================================================
-- DATA MIGRATION: Legacy tables → FHIR tables
-- ============================================================================
-- Created: 2024-11-21
-- Purpose: Migrate existing data from clients/treatment_plans to patients/care_plans
--
-- IMPORTANT: This migration preserves UUIDs for referential integrity
-- Run this AFTER applying the pragmatic FHIR schema
-- ============================================================================
-- ============================================================================
-- STEP 1: Migrate clients → patients
-- ============================================================================
-- Check for existing data
DO $$
DECLARE
client_count INTEGER;
patient_count INTEGER;
BEGIN
SELECT COUNT(*) INTO client_count FROM clients;
SELECT COUNT(*) INTO patient_count FROM patients;
RAISE NOTICE 'Found % clients, % patients (before migration)', client_count, patient_count;
END $$;
-- Migrate clients to patients
INSERT INTO patients (
id, -- Preserve UUID for referential integrity
name_family, -- clients.last_name
name_given, -- clients.first_name (as array)
birth_date, -- clients.birth_date
gender, -- Default 'unknown' (required field)
identifier_bsn, -- Placeholder
identifier_client_number, -- Use original client ID as client number
active, -- Default true
created_at, -- Preserve timestamp
updated_at -- Preserve timestamp
)
SELECT
c.id,
c.last_name,
ARRAY[c.first_name], -- Convert string to array
c.birth_date,
'unknown'::gender_type, -- Required field, default to unknown
'999999990', -- Placeholder BSN (demo)
c.id::text, -- Use UUID as client number for traceability
true,
c.created_at,
c.updated_at
FROM clients c
WHERE NOT EXISTS (
-- Don't re-migrate if already exists
SELECT 1 FROM patients p WHERE p.id = c.id
);
-- Report migration results
DO $$
DECLARE
migrated_count INTEGER;
BEGIN
SELECT COUNT(*) INTO migrated_count
FROM patients p
INNER JOIN clients c ON p.id = c.id;
RAISE NOTICE 'Successfully migrated % clients to patients', migrated_count;
END $$;
-- ============================================================================
-- STEP 2: Migrate treatment_plans → care_plans
-- ============================================================================
-- Check for existing data
DO $$
DECLARE
plan_count INTEGER;
careplan_count INTEGER;
BEGIN
SELECT COUNT(*) INTO plan_count FROM treatment_plans;
SELECT COUNT(*) INTO careplan_count FROM care_plans;
RAISE NOTICE 'Found % treatment_plans, % care_plans (before migration)', plan_count, careplan_count;
END $$;
-- Migrate treatment_plans to care_plans
INSERT INTO care_plans (
id, -- Preserve UUID
identifier, -- Generate from UUID
status, -- Map from treatment_plans.status
title, -- Generate default title
description, -- Extract from plan JSONB if available
patient_id, -- treatment_plans.client_id → patient_id
goals, -- Extract from plan.doelen
activities, -- Extract from plan.interventies
period_start, -- Derive from created_at
period_end, -- Derive from created_at + 3 months (default)
created_date, -- Preserve created_at
created_at, -- Preserve created_at
updated_at -- Preserve updated_at
)
SELECT
tp.id,
tp.id::text, -- Use UUID as identifier
CASE
WHEN tp.status = 'concept' THEN 'draft'::careplan_status
WHEN tp.status = 'gepubliceerd' THEN 'active'::careplan_status
ELSE 'draft'::careplan_status
END,
'Behandelplan v' || tp.version::text, -- Default title
NULL, -- No description in legacy schema
tp.client_id, -- Maps to patient_id (already migrated)
-- Transform goals from legacy structure to FHIR Goal structure
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'description', jsonb_build_object('text', goal),
'lifecycleStatus', 'active'
)
)
FROM jsonb_array_elements_text(tp.plan->'doelen') goal
),
'[]'::jsonb
),
-- Transform activities from legacy structure to FHIR CarePlan.activity structure
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'detail', jsonb_build_object(
'code', jsonb_build_object('text', interventie),
'status', 'in-progress',
'scheduledString', COALESCE(tp.plan->>'frequentie', 'Niet gespecificeerd')
)
)
)
FROM jsonb_array_elements_text(tp.plan->'interventies') interventie
),
'[]'::jsonb
),
tp.created_at::date, -- Start date from creation
(tp.created_at + interval '3 months')::date, -- Default 3-month treatment
tp.created_at,
tp.created_at,
tp.updated_at
FROM treatment_plans tp
WHERE NOT EXISTS (
-- Don't re-migrate if already exists
SELECT 1 FROM care_plans cp WHERE cp.id = tp.id
)
-- Ensure the client was migrated to patients first
AND EXISTS (
SELECT 1 FROM patients p WHERE p.id = tp.client_id
);
-- Report migration results
DO $$
DECLARE
migrated_count INTEGER;
BEGIN
SELECT COUNT(*) INTO migrated_count
FROM care_plans cp
INNER JOIN treatment_plans tp ON cp.id = tp.id;
RAISE NOTICE 'Successfully migrated % treatment_plans to care_plans', migrated_count;
END $$;
-- ============================================================================
-- STEP 3: Verification queries
-- ============================================================================
-- Verify patient migration
DO $$
DECLARE
client_count INTEGER;
patient_count INTEGER;
match_count INTEGER;
BEGIN
SELECT COUNT(*) INTO client_count FROM clients;
SELECT COUNT(*) INTO patient_count FROM patients;
SELECT COUNT(*) INTO match_count
FROM patients p
INNER JOIN clients c ON p.id = c.id;
RAISE NOTICE '=== MIGRATION VERIFICATION ===';
RAISE NOTICE 'Clients: %, Patients: %, Matched: %', client_count, patient_count, match_count;
IF match_count = client_count THEN
RAISE NOTICE '✓ All clients successfully migrated to patients';
ELSE
RAISE WARNING '✗ Migration incomplete: % clients not migrated', (client_count - match_count);
END IF;
END $$;
-- Verify care plan migration
DO $$
DECLARE
plan_count INTEGER;
careplan_count INTEGER;
match_count INTEGER;
BEGIN
SELECT COUNT(*) INTO plan_count FROM treatment_plans;
SELECT COUNT(*) INTO careplan_count FROM care_plans;
SELECT COUNT(*) INTO match_count
FROM care_plans cp
INNER JOIN treatment_plans tp ON cp.id = tp.id;
RAISE NOTICE 'Treatment Plans: %, Care Plans: %, Matched: %', plan_count, careplan_count, match_count;
IF plan_count = 0 THEN
RAISE NOTICE '- No treatment plans to migrate (table empty)';
ELSIF match_count = plan_count THEN
RAISE NOTICE '✓ All treatment plans successfully migrated to care_plans';
ELSE
RAISE WARNING '✗ Migration incomplete: % treatment plans not migrated', (plan_count - match_count);
END IF;
END $$;
-- ============================================================================
-- SUMMARY
-- ============================================================================
-- This migration script:
-- ✓ Migrates clients → patients (preserving UUIDs)
-- ✓ Migrates treatment_plans → care_plans (preserving UUIDs)
-- ✓ Transforms legacy JSONB structure to FHIR-compliant structure
-- ✓ Sets sensible defaults for required FHIR fields
-- ✓ Idempotent: safe to re-run (uses NOT EXISTS checks)
-- ✓ Preserves referential integrity
--
-- NEXT STEPS:
-- 1. Run this migration: supabase db push or apply_migration
-- 2. Verify data in patients and care_plans tables
-- 3. Update application code to use new FHIR tables
-- 4. Optionally: keep legacy tables for rollback, or drop after verification
-- ============================================================================

View File

@@ -1,418 +0,0 @@
-- ============================================================================
-- SEED DEMO DATA: Practitioners, Patients, Encounters
-- ============================================================================
-- Created: 2024-11-21
-- Purpose: Populate FHIR tables with realistic demo data for testing
-- ============================================================================
-- ============================================================================
-- STEP 1: Seed Practitioners (Behandelaren)
-- ============================================================================
-- Get the demo organization ID
DO $$
DECLARE
demo_org_id UUID;
BEGIN
SELECT id INTO demo_org_id FROM organizations WHERE identifier_agb = 'AGB-DEMO-001';
RAISE NOTICE 'Demo organization ID: %', demo_org_id;
END $$;
-- Practitioner 1: Dr. Sarah de Vries (GZ-psycholoog)
INSERT INTO practitioners (
id,
identifier_big,
identifier_agb,
name_prefix,
name_given,
name_family,
qualification,
telecom_phone,
telecom_email,
active
)
VALUES (
'10000000-0000-0000-0000-000000000001'::uuid,
'79012345601',
'03012345',
'Dr.',
ARRAY['Sarah'],
'de Vries',
ARRAY['GZ-psycholoog', 'Cognitieve gedragstherapie', 'EMDR'],
'06-12345678',
's.devries@demo-ggz.nl',
true
)
ON CONFLICT (id) DO NOTHING;
-- Practitioner 2: Drs. Mark Jansen (Psychotherapeut)
INSERT INTO practitioners (
id,
identifier_big,
identifier_agb,
name_prefix,
name_given,
name_family,
qualification,
telecom_phone,
telecom_email,
active
)
VALUES (
'10000000-0000-0000-0000-000000000002'::uuid,
'79023456702',
'03023456',
'Drs.',
ARRAY['Mark'],
'Jansen',
ARRAY['Psychotherapeut', 'Schematherapie', 'Acceptance and Commitment Therapy'],
'06-23456789',
'm.jansen@demo-ggz.nl',
true
)
ON CONFLICT (id) DO NOTHING;
-- Practitioner 3: Lisa van den Berg (Klinisch psycholoog)
INSERT INTO practitioners (
id,
identifier_big,
identifier_agb,
name_prefix,
name_given,
name_family,
qualification,
telecom_phone,
telecom_email,
active
)
VALUES (
'10000000-0000-0000-0000-000000000003'::uuid,
'79034567803',
'03034567',
NULL,
ARRAY['Lisa'],
'van den Berg',
ARRAY['Klinisch psycholoog', 'Diagnostiek', 'ROM-coördinator'],
'06-34567890',
'l.vandenberg@demo-ggz.nl',
true
)
ON CONFLICT (id) DO NOTHING;
RAISE NOTICE '✓ Seeded 3 practitioners';
-- ============================================================================
-- STEP 2: Update existing patients with complete data
-- ============================================================================
-- Get patient IDs from migrated clients
DO $$
DECLARE
patient_colin_id UUID;
patient_jan_id UUID;
patient_optimus_id UUID;
BEGIN
-- Get Colin's patient ID
SELECT id INTO patient_colin_id FROM patients WHERE name_family = 'Lit' AND 'Colin' = ANY(name_given);
-- Get Jan's patient ID
SELECT id INTO patient_jan_id FROM patients WHERE name_family = 'de Vriesh' AND 'Jan' = ANY(name_given);
-- Get Optimus's patient ID
SELECT id INTO patient_optimus_id FROM patients WHERE name_family = 'Prime' AND 'Optimus' = ANY(name_given);
-- Update Colin with complete data
UPDATE patients
SET
gender = 'male',
telecom_phone = '06-11111111',
telecom_email = 'colin.lit@example.com',
address_line = ARRAY['Kerkstraat 12'],
address_city = 'Amsterdam',
address_postal_code = '1012 AB',
insurance_company = 'VGZ',
insurance_number = 'VGZ-123456',
general_practitioner_name = 'Dr. A. Huisarts',
general_practitioner_agb = '12345678'
WHERE id = patient_colin_id;
-- Update Jan with complete data
UPDATE patients
SET
gender = 'male',
telecom_phone = '06-22222222',
telecom_email = 'jan.devriesh@example.com',
address_line = ARRAY['Hoofdstraat 45'],
address_city = 'Utrecht',
address_postal_code = '3511 AB',
insurance_company = 'CZ',
insurance_number = 'CZ-789012',
general_practitioner_name = 'Dr. B. Dokter',
general_practitioner_agb = '23456789'
WHERE id = patient_jan_id;
-- Update Optimus with complete data (easter egg patient)
UPDATE patients
SET
gender = 'other',
telecom_phone = '06-99999999',
address_line = ARRAY['Cybertron Base 1'],
address_city = 'Eindhoven',
address_postal_code = '5600 AA',
insurance_company = 'Menzis',
insurance_number = 'MEN-PRIME-01'
WHERE id = patient_optimus_id;
RAISE NOTICE '✓ Updated 3 existing patients with complete data';
END $$;
-- ============================================================================
-- STEP 3: Seed Encounters (Contactmomenten)
-- ============================================================================
-- Get organization ID
DO $$
DECLARE
demo_org_id UUID;
patient_colin_id UUID;
patient_jan_id UUID;
patient_optimus_id UUID;
practitioner_sarah_id UUID := '10000000-0000-0000-0000-000000000001'::uuid;
practitioner_mark_id UUID := '10000000-0000-0000-0000-000000000002'::uuid;
practitioner_lisa_id UUID := '10000000-0000-0000-0000-000000000003'::uuid;
BEGIN
-- Get IDs
SELECT id INTO demo_org_id FROM organizations WHERE identifier_agb = 'AGB-DEMO-001';
SELECT id INTO patient_colin_id FROM patients WHERE name_family = 'Lit' AND 'Colin' = ANY(name_given);
SELECT id INTO patient_jan_id FROM patients WHERE name_family = 'de Vriesh' AND 'Jan' = ANY(name_given);
SELECT id INTO patient_optimus_id FROM patients WHERE name_family = 'Prime' AND 'Optimus' = ANY(name_given);
-- Encounter 1: Colin's intake with Dr. Sarah de Vries (completed)
INSERT INTO encounters (
id,
status,
class_code,
class_display,
type_code,
type_display,
priority_code,
priority_display,
patient_id,
practitioner_id,
organization_id,
period_start,
period_end,
reason_code,
reason_display,
notes
)
VALUES (
'20000000-0000-0000-0000-000000000001'::uuid,
'completed',
'AMB',
'Ambulatory',
'intake',
'Intake gesprek',
'routine',
'Routine',
patient_colin_id,
practitioner_sarah_id,
demo_org_id,
'2024-10-15 10:00:00+00',
'2024-10-15 11:00:00+00',
ARRAY['F32.1', 'F51.0'],
ARRAY['Matige depressieve episode', 'Insomnia'],
'Eerste intake gesprek. Cliënt presenteert zich met depressieve klachten en slaapproblemen sinds 3 maanden. PHQ-9 score: 14.'
)
ON CONFLICT (id) DO NOTHING;
-- Encounter 2: Jan's intake with Drs. Mark Jansen (completed)
INSERT INTO encounters (
id,
status,
class_code,
class_display,
type_code,
type_display,
priority_code,
priority_display,
patient_id,
practitioner_id,
organization_id,
period_start,
period_end,
reason_code,
reason_display,
notes
)
VALUES (
'20000000-0000-0000-0000-000000000002'::uuid,
'completed',
'AMB',
'Ambulatory',
'intake',
'Intake gesprek',
'routine',
'Routine',
patient_jan_id,
practitioner_mark_id,
demo_org_id,
'2024-11-01 14:00:00+00',
'2024-11-01 15:30:00+00',
ARRAY['F41.1', 'Z63.0'],
ARRAY['Gegeneraliseerde angststoornis', 'Relatieproblemen'],
'Intake gesprek. Cliënt geeft aan last te hebben van voortdurende piekeren en angstklachten. GAD-7 score: 16. Ook relationele problematiek.'
)
ON CONFLICT (id) DO NOTHING;
-- Encounter 3: Colin's 2nd session (planned)
INSERT INTO encounters (
id,
status,
class_code,
class_display,
type_code,
type_display,
priority_code,
priority_display,
patient_id,
practitioner_id,
organization_id,
period_start,
reason_code,
reason_display,
notes
)
VALUES (
'20000000-0000-0000-0000-000000000003'::uuid,
'planned',
'AMB',
'Ambulatory',
'behandeling',
'Behandelsessie',
'routine',
'Routine',
patient_colin_id,
practitioner_sarah_id,
demo_org_id,
'2024-11-25 10:00:00+00',
ARRAY['F32.1'],
ARRAY['Matige depressieve episode'],
'Tweede sessie CGT gepland. Focus op gedragsactivatie en cognitieve herstructurering.'
)
ON CONFLICT (id) DO NOTHING;
-- Encounter 4: Optimus diagnostiek with Lisa (completed - easter egg)
INSERT INTO encounters (
id,
status,
class_code,
class_display,
type_code,
type_display,
patient_id,
practitioner_id,
organization_id,
period_start,
period_end,
reason_code,
reason_display,
notes
)
VALUES (
'20000000-0000-0000-0000-000000000004'::uuid,
'completed',
'AMB',
'Ambulatory',
'diagnostiek',
'Diagnostisch onderzoek',
patient_optimus_id,
practitioner_lisa_id,
demo_org_id,
'2024-11-10 09:00:00+00',
'2024-11-10 11:00:00+00',
ARRAY['Z03.2'],
ARRAY['Observatie voor vermoede psychische aandoening'],
'Diagnostisch onderzoek. Cliënt vertoont opvallende communicatiepatronen en metallic spraakpatroon. Nader onderzoek geïndiceerd.'
)
ON CONFLICT (id) DO NOTHING;
-- Encounter 5: Jan's follow-up (in-progress)
INSERT INTO encounters (
id,
status,
class_code,
class_display,
type_code,
type_display,
patient_id,
practitioner_id,
organization_id,
period_start,
reason_code,
reason_display,
notes
)
VALUES (
'20000000-0000-0000-0000-000000000005'::uuid,
'in-progress',
'AMB',
'Ambulatory',
'behandeling',
'Behandelsessie',
patient_jan_id,
practitioner_mark_id,
demo_org_id,
NOW(),
ARRAY['F41.1'],
ARRAY['Gegeneraliseerde angststoornis'],
'Sessie 3: ACT technieken. Werk aan psychologische flexibiliteit en acceptatie.'
)
ON CONFLICT (id) DO NOTHING;
RAISE NOTICE '✓ Seeded 5 encounters';
END $$;
-- ============================================================================
-- VERIFICATION
-- ============================================================================
DO $$
DECLARE
practitioner_count INTEGER;
patient_count INTEGER;
encounter_count INTEGER;
BEGIN
SELECT COUNT(*) INTO practitioner_count FROM practitioners;
SELECT COUNT(*) INTO patient_count FROM patients;
SELECT COUNT(*) INTO encounter_count FROM encounters;
RAISE NOTICE '=== SEED DATA SUMMARY ===';
RAISE NOTICE 'Practitioners: %', practitioner_count;
RAISE NOTICE 'Patients: %', patient_count;
RAISE NOTICE 'Encounters: %', encounter_count;
IF practitioner_count >= 3 AND patient_count >= 3 AND encounter_count >= 5 THEN
RAISE NOTICE '✓ All demo data seeded successfully!';
ELSE
RAISE WARNING '⚠ Some demo data may be missing';
END IF;
END $$;
-- ============================================================================
-- SUMMARY
-- ============================================================================
-- ✓ 3 Practitioners created (Sarah, Mark, Lisa)
-- ✓ 3 Patients updated with complete data (Colin, Jan, Optimus)
-- ✓ 5 Encounters created:
-- - 2 completed intakes
-- - 1 planned session
-- - 1 completed diagnostiek
-- - 1 in-progress behandeling
--
-- NEXT STEPS:
-- 1. Test frontend with demo data
-- 2. Create conditions (diagnoses) for patients
-- 3. Create care_plans (behandelplannen)
-- 4. Add observations (ROM scores)
-- ============================================================================

View File

@@ -1,266 +0,0 @@
-- WARNING: This schema is for context only and is not meant to be run.
-- Table order and constraints may not be valid for execution.
CREATE TABLE public.ai_events (
id uuid NOT NULL DEFAULT gen_random_uuid(),
kind text NOT NULL CHECK (kind = ANY (ARRAY['summarize'::text, 'readability'::text, 'extract'::text, 'plan'::text])),
client_id uuid,
note_id uuid,
request jsonb NOT NULL DEFAULT '{}'::jsonb,
response jsonb NOT NULL DEFAULT '{}'::jsonb,
duration_ms integer NOT NULL DEFAULT 0 CHECK (duration_ms >= 0),
created_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT ai_events_pkey PRIMARY KEY (id),
CONSTRAINT ai_events_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.clients(id),
CONSTRAINT ai_events_note_id_fkey FOREIGN KEY (note_id) REFERENCES public.intake_notes(id)
);
CREATE TABLE public.care_plans (
id uuid NOT NULL DEFAULT gen_random_uuid(),
identifier text DEFAULT (gen_random_uuid())::text UNIQUE,
status USER-DEFINED NOT NULL DEFAULT 'draft'::careplan_status,
intent text NOT NULL DEFAULT 'plan'::text,
category_code text DEFAULT 'ggz-behandelplan'::text,
category_display text DEFAULT 'GGZ Behandelplan'::text,
title text NOT NULL,
description text,
patient_id uuid NOT NULL,
encounter_id uuid,
period_start date,
period_end date,
created_date timestamp with time zone DEFAULT now(),
author_id uuid,
contributor_ids ARRAY,
care_team_ids ARRAY,
addresses_condition_ids ARRAY,
goals jsonb DEFAULT '[]'::jsonb,
activities jsonb DEFAULT '[]'::jsonb,
note text,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
CONSTRAINT care_plans_pkey PRIMARY KEY (id),
CONSTRAINT care_plans_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id),
CONSTRAINT care_plans_encounter_id_fkey FOREIGN KEY (encounter_id) REFERENCES public.encounters(id),
CONSTRAINT care_plans_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.practitioners(id)
);
CREATE TABLE public.clients (
id uuid NOT NULL DEFAULT gen_random_uuid(),
first_name text NOT NULL,
last_name text NOT NULL,
birth_date date NOT NULL,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT clients_pkey PRIMARY KEY (id)
);
CREATE TABLE public.conditions (
id uuid NOT NULL DEFAULT gen_random_uuid(),
identifier text DEFAULT (gen_random_uuid())::text UNIQUE,
clinical_status USER-DEFINED NOT NULL DEFAULT 'active'::condition_clinical_status,
verification_status USER-DEFINED NOT NULL DEFAULT 'provisional'::condition_verification_status,
category text NOT NULL DEFAULT 'encounter-diagnosis'::text,
severity_code text,
severity_display text,
code_system text NOT NULL DEFAULT 'http://hl7.org/fhir/sid/icd-10'::text,
code_code text NOT NULL,
code_display text NOT NULL,
body_site_code text,
body_site_display text,
patient_id uuid NOT NULL,
encounter_id uuid,
onset_datetime timestamp with time zone,
onset_age integer,
abatement_datetime timestamp with time zone,
abatement_age integer,
recorded_date timestamp with time zone NOT NULL DEFAULT now(),
recorder_id uuid,
asserter_id uuid,
note text,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
CONSTRAINT conditions_pkey PRIMARY KEY (id),
CONSTRAINT conditions_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id),
CONSTRAINT conditions_encounter_id_fkey FOREIGN KEY (encounter_id) REFERENCES public.encounters(id),
CONSTRAINT conditions_recorder_id_fkey FOREIGN KEY (recorder_id) REFERENCES public.practitioners(id),
CONSTRAINT conditions_asserter_id_fkey FOREIGN KEY (asserter_id) REFERENCES public.practitioners(id)
);
CREATE TABLE public.demo_users (
id uuid NOT NULL DEFAULT gen_random_uuid(),
user_id uuid UNIQUE,
access_level text NOT NULL DEFAULT 'read_only'::text CHECK (access_level = ANY (ARRAY['read_only'::text, 'interactive'::text, 'presenter'::text])),
expires_at timestamp with time zone DEFAULT (now() + '90 days'::interval),
usage_count integer DEFAULT 0,
last_login_at timestamp with time zone,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
notes text,
CONSTRAINT demo_users_pkey PRIMARY KEY (id),
CONSTRAINT demo_users_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id)
);
CREATE TABLE public.encounters (
id uuid NOT NULL DEFAULT gen_random_uuid(),
identifier text DEFAULT (gen_random_uuid())::text UNIQUE,
status USER-DEFINED NOT NULL DEFAULT 'planned'::encounter_status,
class_code text NOT NULL,
class_display text NOT NULL,
type_code text NOT NULL,
type_display text NOT NULL,
priority_code text,
priority_display text,
patient_id uuid NOT NULL,
practitioner_id uuid,
organization_id uuid,
period_start timestamp with time zone NOT NULL,
period_end timestamp with time zone,
reason_code ARRAY,
reason_display ARRAY,
admission_source text,
discharge_disposition text,
notes text,
intake_note_id uuid,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
CONSTRAINT encounters_pkey PRIMARY KEY (id),
CONSTRAINT encounters_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id),
CONSTRAINT encounters_practitioner_id_fkey FOREIGN KEY (practitioner_id) REFERENCES public.practitioners(id),
CONSTRAINT encounters_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organizations(id),
CONSTRAINT encounters_intake_note_id_fkey FOREIGN KEY (intake_note_id) REFERENCES public.intake_notes(id)
);
CREATE TABLE public.intake_notes (
id uuid NOT NULL DEFAULT gen_random_uuid(),
client_id uuid NOT NULL,
title text,
tag text CHECK (tag = ANY (ARRAY['Intake'::text, 'Evaluatie'::text, 'Plan'::text])),
content_json jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (content_json IS NOT NULL),
content_text text,
author uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT intake_notes_pkey PRIMARY KEY (id),
CONSTRAINT intake_notes_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.clients(id)
);
CREATE TABLE public.observations (
id uuid NOT NULL DEFAULT gen_random_uuid(),
identifier text DEFAULT (gen_random_uuid())::text UNIQUE,
status USER-DEFINED NOT NULL DEFAULT 'final'::observation_status,
category text NOT NULL,
code_system text NOT NULL,
code_code text NOT NULL,
code_display text NOT NULL,
patient_id uuid NOT NULL,
encounter_id uuid,
effective_datetime timestamp with time zone NOT NULL,
issued timestamp with time zone DEFAULT now(),
performer_id uuid,
value_type text NOT NULL,
value_quantity_value numeric,
value_quantity_unit text,
value_quantity_comparator text,
value_string text,
value_boolean boolean,
value_codeable_concept jsonb,
interpretation_code text,
interpretation_display text,
note text,
body_site text,
method_code text,
method_display text,
reference_range_low numeric,
reference_range_high numeric,
reference_range_text text,
created_at timestamp with time zone DEFAULT now(),
CONSTRAINT observations_pkey PRIMARY KEY (id),
CONSTRAINT observations_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id),
CONSTRAINT observations_encounter_id_fkey FOREIGN KEY (encounter_id) REFERENCES public.encounters(id),
CONSTRAINT observations_performer_id_fkey FOREIGN KEY (performer_id) REFERENCES public.practitioners(id)
);
CREATE TABLE public.organizations (
id uuid NOT NULL DEFAULT gen_random_uuid(),
identifier_agb text UNIQUE,
identifier_kvk text,
name text NOT NULL,
alias ARRAY,
type_code text DEFAULT 'prov'::text,
type_display text DEFAULT 'Healthcare Provider'::text,
telecom_phone text,
telecom_email text,
telecom_website text,
address_line ARRAY,
address_city text,
address_postal_code text,
address_country text DEFAULT 'NL'::text,
active boolean DEFAULT true,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
CONSTRAINT organizations_pkey PRIMARY KEY (id)
);
CREATE TABLE public.patients (
id uuid NOT NULL DEFAULT gen_random_uuid(),
identifier_bsn text DEFAULT '999999990'::text,
identifier_client_number text,
name_family text NOT NULL,
name_given ARRAY NOT NULL,
name_prefix text,
name_use text DEFAULT 'official'::text,
birth_date date NOT NULL,
gender USER-DEFINED NOT NULL,
telecom_phone text,
telecom_email text,
address_line ARRAY,
address_city text,
address_postal_code text,
address_country text DEFAULT 'NL'::text,
insurance_company text,
insurance_number text,
emergency_contact_name text,
emergency_contact_relationship text,
emergency_contact_phone text,
active boolean DEFAULT true,
general_practitioner_name text,
general_practitioner_agb text,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
CONSTRAINT patients_pkey PRIMARY KEY (id)
);
CREATE TABLE public.practitioners (
id uuid NOT NULL DEFAULT gen_random_uuid(),
identifier_big text UNIQUE,
identifier_agb text,
name_prefix text,
name_given ARRAY NOT NULL,
name_family text NOT NULL,
name_suffix text,
qualification ARRAY,
telecom_phone text,
telecom_email text,
active boolean DEFAULT true,
user_id uuid,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
CONSTRAINT practitioners_pkey PRIMARY KEY (id),
CONSTRAINT practitioners_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id)
);
CREATE TABLE public.problem_profiles (
id uuid NOT NULL DEFAULT gen_random_uuid(),
client_id uuid NOT NULL,
category text NOT NULL CHECK (category = ANY (ARRAY['stemming_depressie'::text, 'angst'::text, 'gedrag_impuls'::text, 'middelen_gebruik'::text, 'cognitief'::text, 'context_psychosociaal'::text])),
severity text NOT NULL CHECK (severity = ANY (ARRAY['laag'::text, 'middel'::text, 'hoog'::text])),
remarks text,
source_note_id uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT problem_profiles_pkey PRIMARY KEY (id),
CONSTRAINT problem_profiles_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.clients(id),
CONSTRAINT problem_profiles_source_note_id_fkey FOREIGN KEY (source_note_id) REFERENCES public.intake_notes(id)
);
CREATE TABLE public.treatment_plans (
id uuid NOT NULL DEFAULT gen_random_uuid(),
client_id uuid NOT NULL,
version integer NOT NULL DEFAULT 1 CHECK (version > 0),
status text NOT NULL DEFAULT 'concept'::text CHECK (status = ANY (ARRAY['concept'::text, 'gepubliceerd'::text])),
plan jsonb NOT NULL DEFAULT '{"doelen": [], "frequentie": "", "interventies": [], "meetmomenten": []}'::jsonb CHECK (plan ? 'doelen'::text AND plan ? 'interventies'::text AND plan ? 'frequentie'::text AND plan ? 'meetmomenten'::text),
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
published_at timestamp with time zone,
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT treatment_plans_pkey PRIMARY KEY (id),
CONSTRAINT treatment_plans_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.clients(id)
);

View File

@@ -1,422 +0,0 @@
-- ============================================================================
-- 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)
-- ============================================================================

74
lib/types/intake.ts Normal file
View File

@@ -0,0 +1,74 @@
/**
* Intake Types
*
* Type definitions for intake-related data structures
* Used across API routes, server actions, and components
*/
import { Database } from '@/lib/supabase/database.types';
import { z } from 'zod';
// Base intake type from database
export type Intake = Database['public']['Tables']['intakes']['Row'];
export type IntakeInsert = Database['public']['Tables']['intakes']['Insert'];
export type IntakeUpdate = Database['public']['Tables']['intakes']['Update'];
// Department enum values
export const INTAKE_DEPARTMENTS = ['Volwassenen', 'Jeugd', 'Ouderen'] as const;
export type IntakeDepartment = (typeof INTAKE_DEPARTMENTS)[number];
// Status enum values (mapped from database 'bezig'/'afgerond' to UI 'Open'/'Afgerond')
export const INTAKE_STATUSES = ['bezig', 'afgerond'] as const;
export type IntakeStatus = (typeof INTAKE_STATUSES)[number];
/**
* Zod validation schema for creating a new intake
*/
export const CreateIntakeSchema = z.object({
patient_id: z.string().uuid('Patient ID moet een geldige UUID zijn'),
title: z.string().min(1, 'Titel is verplicht'),
department: z.enum(INTAKE_DEPARTMENTS, {
errorMap: () => ({ message: 'Afdeling moet Volwassenen, Jeugd of Ouderen zijn' }),
}),
start_date: z.string().min(1, 'Startdatum is verplicht'),
psychologist_id: z.string().uuid().optional(),
notes: z.string().optional(),
});
/**
* Type inferred from CreateIntakeSchema
*/
export type CreateIntakeInput = z.infer<typeof CreateIntakeSchema>;
/**
* Zod validation schema for updating an existing intake
*/
export const UpdateIntakeSchema = z.object({
title: z.string().min(1).optional(),
department: z.enum(INTAKE_DEPARTMENTS).optional(),
status: z.enum(INTAKE_STATUSES).optional(),
start_date: z.string().optional(),
end_date: z.string().nullable().optional(),
psychologist_id: z.string().uuid().nullable().optional(),
notes: z.string().nullable().optional(),
});
/**
* Type inferred from UpdateIntakeSchema
*/
export type UpdateIntakeInput = z.infer<typeof UpdateIntakeSchema>;
/**
* Response type for listing intakes
*/
export interface IntakeListResponse {
intakes: Intake[];
total: number;
}
/**
* Extended intake type with computed fields (if needed in the future)
*/
export interface IntakeWithMetadata extends Intake {
// Future: computed fields like duration, isActive, etc.
}

45
lib/types/screening.ts Normal file
View File

@@ -0,0 +1,45 @@
/**
* Screening Types
*
* Shared type definitions for screening entities and related helper types.
*/
import { Database } from '@/lib/supabase/database.types';
import { z } from 'zod';
export type Screening = Database['public']['Tables']['screenings']['Row'];
export type ScreeningActivity = Database['public']['Tables']['screening_activities']['Row'];
export type ScreeningDocument = Database['public']['Tables']['screening_documents']['Row'];
export interface ScreeningWithRelations extends Screening {
screening_activities?: ScreeningActivity[];
screening_documents?: ScreeningDocument[];
}
export const UpdateScreeningSchema = z.object({
screening_id: z.string().uuid('Screening ID moet een geldige UUID zijn'),
patient_id: z.string().uuid('Patiënt ID moet een geldige UUID zijn'),
request_for_help: z.string().optional(),
decision: z.enum(['geschikt', 'niet_geschikt']).optional(),
decision_notes: z.string().optional(),
decision_department: z.string().optional(),
});
export type UpdateScreeningInput = z.infer<typeof UpdateScreeningSchema>;
export const CreateActivitySchema = z.object({
screening_id: z.string().uuid('Screening ID moet een geldige UUID zijn'),
patient_id: z.string().uuid('Patiënt ID moet een geldige UUID zijn'),
activity_text: z
.string()
.min(3, 'Beschrijf minimaal 3 tekens')
.max(2000, 'Maximaal 2000 tekens'),
});
export type CreateActivityInput = z.infer<typeof CreateActivitySchema>;
export interface ScreeningSummary {
screening: ScreeningWithRelations;
activities: ScreeningActivity[];
documents: ScreeningDocument[];
}