FHIR, API, datamodel

This commit is contained in:
colinislit
2025-11-21 22:32:29 +01:00
parent 5f7ef0801d
commit d5c6cf208b
39 changed files with 17974 additions and 1004 deletions

View 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
-- ============================================================================

View 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)
-- ============================================================================