feat: migrate clients module to patients + add docs
This commit is contained in:
1
supabase/.temp/cli-latest
Normal file
1
supabase/.temp/cli-latest
Normal file
@@ -0,0 +1 @@
|
||||
v2.58.5
|
||||
1
supabase/.temp/gotrue-version
Normal file
1
supabase/.temp/gotrue-version
Normal file
@@ -0,0 +1 @@
|
||||
v2.182.1
|
||||
1
supabase/.temp/pooler-url
Normal file
1
supabase/.temp/pooler-url
Normal file
@@ -0,0 +1 @@
|
||||
postgresql://postgres.dqugbrpwtisgyxscpefg@aws-1-eu-north-1.pooler.supabase.com:5432/postgres
|
||||
1
supabase/.temp/postgres-version
Normal file
1
supabase/.temp/postgres-version
Normal file
@@ -0,0 +1 @@
|
||||
17.6.1.042
|
||||
1
supabase/.temp/project-ref
Normal file
1
supabase/.temp/project-ref
Normal file
@@ -0,0 +1 @@
|
||||
dqugbrpwtisgyxscpefg
|
||||
1
supabase/.temp/rest-version
Normal file
1
supabase/.temp/rest-version
Normal file
@@ -0,0 +1 @@
|
||||
v13.0.5
|
||||
1
supabase/.temp/storage-migration
Normal file
1
supabase/.temp/storage-migration
Normal file
@@ -0,0 +1 @@
|
||||
iceberg-catalog-ids
|
||||
1
supabase/.temp/storage-version
Normal file
1
supabase/.temp/storage-version
Normal file
@@ -0,0 +1 @@
|
||||
v1.31.1
|
||||
@@ -1,180 +0,0 @@
|
||||
-- ================================================
|
||||
-- RLS Policy Tests
|
||||
-- Created: 2024-11-15
|
||||
-- Epic: E2 - Database & Auth
|
||||
-- Story: E2.S2 - RLS policies implementeren
|
||||
-- ================================================
|
||||
-- This file contains test queries to verify RLS policies
|
||||
-- Run these queries manually to verify RLS is working correctly
|
||||
-- ================================================
|
||||
|
||||
-- ================================================
|
||||
-- TEST 1: Verify RLS is enabled on all tables
|
||||
-- ================================================
|
||||
-- Expected: All tables should have rowsecurity = true
|
||||
|
||||
SELECT
|
||||
tablename,
|
||||
rowsecurity as rls_enabled
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY tablename;
|
||||
|
||||
-- Expected output:
|
||||
-- ai_events | true
|
||||
-- clients | true
|
||||
-- intake_notes | true
|
||||
-- problem_profiles | true
|
||||
-- treatment_plans | true
|
||||
|
||||
-- ================================================
|
||||
-- TEST 2: Check all RLS policies exist
|
||||
-- ================================================
|
||||
-- Expected: Each table should have 4 policies (SELECT, INSERT, UPDATE, DELETE)
|
||||
-- except ai_events which has only 2 (SELECT, INSERT)
|
||||
|
||||
SELECT
|
||||
tablename,
|
||||
COUNT(*) as policy_count,
|
||||
STRING_AGG(cmd, ', ' ORDER BY cmd) as commands
|
||||
FROM pg_policies
|
||||
WHERE schemaname = 'public'
|
||||
GROUP BY tablename
|
||||
ORDER BY tablename;
|
||||
|
||||
-- Expected output:
|
||||
-- ai_events | 2 | INSERT, SELECT
|
||||
-- clients | 4 | DELETE, INSERT, SELECT, UPDATE
|
||||
-- intake_notes | 4 | DELETE, INSERT, SELECT, UPDATE
|
||||
-- problem_profiles | 4 | DELETE, INSERT, SELECT, UPDATE
|
||||
-- treatment_plans | 4 | DELETE, INSERT, SELECT, UPDATE
|
||||
|
||||
-- ================================================
|
||||
-- TEST 3: Verify policy predicates use auth.uid()
|
||||
-- ================================================
|
||||
-- Expected: All policies should check auth.uid() IS NOT NULL
|
||||
|
||||
SELECT
|
||||
tablename,
|
||||
policyname,
|
||||
cmd,
|
||||
qual as using_clause,
|
||||
with_check
|
||||
FROM pg_policies
|
||||
WHERE schemaname = 'public'
|
||||
AND qual NOT LIKE '%auth.uid()%'
|
||||
ORDER BY tablename, policyname;
|
||||
|
||||
-- Expected output: Empty (no policies without auth.uid() check)
|
||||
|
||||
-- ================================================
|
||||
-- TEST 4: Simulate authenticated user query
|
||||
-- ================================================
|
||||
-- This test simulates what happens when an authenticated user
|
||||
-- tries to access data. In production, auth.uid() would return
|
||||
-- the user's actual UUID.
|
||||
|
||||
-- Note: These queries will work in the SQL editor when logged in,
|
||||
-- but will fail when run as unauthenticated
|
||||
|
||||
-- Test SELECT permission (should succeed when authenticated)
|
||||
-- SELECT * FROM clients LIMIT 1;
|
||||
|
||||
-- Test INSERT permission (should succeed when authenticated)
|
||||
-- INSERT INTO clients (first_name, last_name, birth_date)
|
||||
-- VALUES ('Test', 'User', '1990-01-01');
|
||||
|
||||
-- Test UPDATE permission (should succeed when authenticated)
|
||||
-- UPDATE clients SET first_name = 'Updated' WHERE id = 'some-uuid';
|
||||
|
||||
-- Test DELETE permission (should succeed when authenticated)
|
||||
-- DELETE FROM clients WHERE id = 'some-uuid';
|
||||
|
||||
-- ================================================
|
||||
-- TEST 5: Verify ai_events immutability
|
||||
-- ================================================
|
||||
-- Expected: ai_events should NOT have UPDATE or DELETE policies
|
||||
-- (except for service role via RLS bypass)
|
||||
|
||||
SELECT
|
||||
tablename,
|
||||
policyname,
|
||||
cmd
|
||||
FROM pg_policies
|
||||
WHERE schemaname = 'public'
|
||||
AND tablename = 'ai_events'
|
||||
AND cmd IN ('UPDATE', 'DELETE')
|
||||
ORDER BY cmd;
|
||||
|
||||
-- Expected output: Empty (no UPDATE or DELETE policies for regular users)
|
||||
|
||||
-- ================================================
|
||||
-- TEST 6: Check foreign key relationships
|
||||
-- ================================================
|
||||
-- Expected: All foreign keys should be properly set up
|
||||
|
||||
SELECT
|
||||
tc.table_name,
|
||||
kcu.column_name,
|
||||
ccu.table_name AS foreign_table_name,
|
||||
ccu.column_name AS foreign_column_name,
|
||||
rc.delete_rule
|
||||
FROM information_schema.table_constraints AS tc
|
||||
JOIN information_schema.key_column_usage AS kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
JOIN information_schema.constraint_column_usage AS ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
AND ccu.table_schema = tc.table_schema
|
||||
JOIN information_schema.referential_constraints AS rc
|
||||
ON tc.constraint_name = rc.constraint_name
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema = 'public'
|
||||
ORDER BY tc.table_name, kcu.column_name;
|
||||
|
||||
-- Expected output:
|
||||
-- intake_notes | client_id | clients | id | CASCADE
|
||||
-- problem_profiles | client_id | clients | id | CASCADE
|
||||
-- problem_profiles | source_note_id | intake_notes | id | SET NULL
|
||||
-- treatment_plans | client_id | clients | id | CASCADE
|
||||
-- ai_events | client_id | clients | id | SET NULL
|
||||
-- ai_events | note_id | intake_notes | id | SET NULL
|
||||
|
||||
-- ================================================
|
||||
-- PRODUCTION MIGRATION PATH
|
||||
-- ================================================
|
||||
-- When moving to production, enhance policies with org_id filtering:
|
||||
--
|
||||
-- 1. Add org_id column to all tables:
|
||||
-- ALTER TABLE clients ADD COLUMN org_id UUID REFERENCES organizations(id);
|
||||
--
|
||||
-- 2. Update policies to filter by organization:
|
||||
-- CREATE POLICY "Users can view own org clients"
|
||||
-- ON clients
|
||||
-- FOR SELECT
|
||||
-- USING (
|
||||
-- auth.uid() IS NOT NULL AND
|
||||
-- org_id = (SELECT org_id FROM users WHERE id = auth.uid())
|
||||
-- );
|
||||
--
|
||||
-- 3. Add role-based access:
|
||||
-- CREATE POLICY "Admins can view all"
|
||||
-- ON clients
|
||||
-- FOR SELECT
|
||||
-- USING (
|
||||
-- auth.uid() IS NOT NULL AND
|
||||
-- EXISTS (
|
||||
-- SELECT 1 FROM users
|
||||
-- WHERE id = auth.uid() AND role = 'admin'
|
||||
-- )
|
||||
-- );
|
||||
|
||||
-- ================================================
|
||||
-- SECURITY NOTES
|
||||
-- ================================================
|
||||
-- 1. Current policies are MVP-level: all authenticated users can access all data
|
||||
-- 2. In production, add org_id filtering for multi-tenancy
|
||||
-- 3. ai_events table is append-only for regular users (audit trail)
|
||||
-- 4. Service role can bypass RLS for admin operations
|
||||
-- 5. All policies use auth.uid() for security
|
||||
-- 6. Foreign key CASCADE ensures orphaned records are cleaned up
|
||||
235
supabase/migrations/20241121_migrate_legacy_to_fhir.sql
Normal file
235
supabase/migrations/20241121_migrate_legacy_to_fhir.sql
Normal file
@@ -0,0 +1,235 @@
|
||||
-- ============================================================================
|
||||
-- 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
|
||||
-- ============================================================================
|
||||
418
supabase/migrations/20241121_seed_demo_data.sql
Normal file
418
supabase/migrations/20241121_seed_demo_data.sql
Normal file
@@ -0,0 +1,418 @@
|
||||
-- ============================================================================
|
||||
-- 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)
|
||||
-- ============================================================================
|
||||
422
supabase/migrations/20251122_screening_intake_schema.sql
Normal file
422
supabase/migrations/20251122_screening_intake_schema.sql
Normal file
@@ -0,0 +1,422 @@
|
||||
-- ============================================================================
|
||||
-- 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)
|
||||
-- ============================================================================
|
||||
87
supabase/migrations/20251122_seed_default_organization.sql
Normal file
87
supabase/migrations/20251122_seed_default_organization.sql
Normal file
@@ -0,0 +1,87 @@
|
||||
-- ============================================================================
|
||||
-- SEED DEFAULT ORGANIZATION
|
||||
-- ============================================================================
|
||||
-- Created: 2025-11-22
|
||||
-- Purpose: Create default GGZ organization for development/demo
|
||||
-- Epic: E3.S1 - Organization seed
|
||||
-- ============================================================================
|
||||
|
||||
-- Create demo GGZ organization
|
||||
INSERT INTO organizations (
|
||||
id,
|
||||
identifier_agb,
|
||||
identifier_kvk,
|
||||
name,
|
||||
alias,
|
||||
type_code,
|
||||
type_display,
|
||||
telecom_phone,
|
||||
telecom_email,
|
||||
telecom_website,
|
||||
address_line,
|
||||
address_city,
|
||||
address_postal_code,
|
||||
address_country,
|
||||
active
|
||||
)
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
||||
'AGB-DEMO-001',
|
||||
'12345678',
|
||||
'Demo GGZ Instelling',
|
||||
ARRAY['Demo GGZ', 'DGGZ'],
|
||||
'prov',
|
||||
'Healthcare Provider',
|
||||
'030-1234567',
|
||||
'info@demo-ggz.nl',
|
||||
'https://demo-ggz.nl',
|
||||
ARRAY['Demonstratiestraat 1'],
|
||||
'Utrecht',
|
||||
'3511 AB',
|
||||
'NL',
|
||||
true
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
identifier_agb = EXCLUDED.identifier_agb,
|
||||
identifier_kvk = EXCLUDED.identifier_kvk,
|
||||
name = EXCLUDED.name,
|
||||
alias = EXCLUDED.alias,
|
||||
type_code = EXCLUDED.type_code,
|
||||
type_display = EXCLUDED.type_display,
|
||||
telecom_phone = EXCLUDED.telecom_phone,
|
||||
telecom_email = EXCLUDED.telecom_email,
|
||||
telecom_website = EXCLUDED.telecom_website,
|
||||
address_line = EXCLUDED.address_line,
|
||||
address_city = EXCLUDED.address_city,
|
||||
address_postal_code = EXCLUDED.address_postal_code,
|
||||
address_country = EXCLUDED.address_country,
|
||||
active = EXCLUDED.active,
|
||||
updated_at = NOW();
|
||||
|
||||
-- Verification
|
||||
DO $$
|
||||
DECLARE
|
||||
org_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO org_count FROM organizations WHERE identifier_agb = 'AGB-DEMO-001';
|
||||
|
||||
IF org_count > 0 THEN
|
||||
RAISE NOTICE '✓ Default organization created/updated successfully';
|
||||
RAISE NOTICE 'Organization: Demo GGZ Instelling (AGB-DEMO-001)';
|
||||
ELSE
|
||||
RAISE WARNING '✗ Failed to create default organization';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- SUMMARY
|
||||
-- ============================================================================
|
||||
-- ✓ Created Demo GGZ Instelling with identifier AGB-DEMO-001
|
||||
-- ✓ Idempotent: safe to re-run (uses ON CONFLICT)
|
||||
-- ✓ Required for E3.S1 acceptance criteria
|
||||
--
|
||||
-- This organization is referenced by:
|
||||
-- - Encounters (organization_id foreign key)
|
||||
-- - Practitioners (organizational affiliation)
|
||||
-- - Care plans (care team organization)
|
||||
-- ============================================================================
|
||||
Reference in New Issue
Block a user