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

@@ -62,6 +62,103 @@ export type Database = {
},
]
}
care_plans: {
Row: {
activities: Json | null
addresses_condition_ids: string[] | null
author_id: string | null
care_team_ids: string[] | null
category_code: string | null
category_display: string | null
contributor_ids: string[] | null
created_at: string | null
created_date: string | null
description: string | null
encounter_id: string | null
goals: Json | null
id: string
identifier: string | null
intent: string
note: string | null
patient_id: string
period_end: string | null
period_start: string | null
status: Database["public"]["Enums"]["careplan_status"]
title: string
updated_at: string | null
}
Insert: {
activities?: Json | null
addresses_condition_ids?: string[] | null
author_id?: string | null
care_team_ids?: string[] | null
category_code?: string | null
category_display?: string | null
contributor_ids?: string[] | null
created_at?: string | null
created_date?: string | null
description?: string | null
encounter_id?: string | null
goals?: Json | null
id?: string
identifier?: string | null
intent?: string
note?: string | null
patient_id: string
period_end?: string | null
period_start?: string | null
status?: Database["public"]["Enums"]["careplan_status"]
title: string
updated_at?: string | null
}
Update: {
activities?: Json | null
addresses_condition_ids?: string[] | null
author_id?: string | null
care_team_ids?: string[] | null
category_code?: string | null
category_display?: string | null
contributor_ids?: string[] | null
created_at?: string | null
created_date?: string | null
description?: string | null
encounter_id?: string | null
goals?: Json | null
id?: string
identifier?: string | null
intent?: string
note?: string | null
patient_id?: string
period_end?: string | null
period_start?: string | null
status?: Database["public"]["Enums"]["careplan_status"]
title?: string
updated_at?: string | null
}
Relationships: [
{
foreignKeyName: "care_plans_author_id_fkey"
columns: ["author_id"]
isOneToOne: false
referencedRelation: "practitioners"
referencedColumns: ["id"]
},
{
foreignKeyName: "care_plans_encounter_id_fkey"
columns: ["encounter_id"]
isOneToOne: false
referencedRelation: "encounters"
referencedColumns: ["id"]
},
{
foreignKeyName: "care_plans_patient_id_fkey"
columns: ["patient_id"]
isOneToOne: false
referencedRelation: "patients"
referencedColumns: ["id"]
},
]
}
clients: {
Row: {
birth_date: string
@@ -89,6 +186,256 @@ export type Database = {
}
Relationships: []
}
conditions: {
Row: {
abatement_age: number | null
abatement_datetime: string | null
asserter_id: string | null
body_site_code: string | null
body_site_display: string | null
category: string
clinical_status: Database["public"]["Enums"]["condition_clinical_status"]
code_code: string
code_display: string
code_system: string
created_at: string | null
encounter_id: string | null
id: string
identifier: string | null
note: string | null
onset_age: number | null
onset_datetime: string | null
patient_id: string
recorded_date: string
recorder_id: string | null
severity_code: string | null
severity_display: string | null
updated_at: string | null
verification_status: Database["public"]["Enums"]["condition_verification_status"]
}
Insert: {
abatement_age?: number | null
abatement_datetime?: string | null
asserter_id?: string | null
body_site_code?: string | null
body_site_display?: string | null
category?: string
clinical_status?: Database["public"]["Enums"]["condition_clinical_status"]
code_code: string
code_display: string
code_system?: string
created_at?: string | null
encounter_id?: string | null
id?: string
identifier?: string | null
note?: string | null
onset_age?: number | null
onset_datetime?: string | null
patient_id: string
recorded_date?: string
recorder_id?: string | null
severity_code?: string | null
severity_display?: string | null
updated_at?: string | null
verification_status?: Database["public"]["Enums"]["condition_verification_status"]
}
Update: {
abatement_age?: number | null
abatement_datetime?: string | null
asserter_id?: string | null
body_site_code?: string | null
body_site_display?: string | null
category?: string
clinical_status?: Database["public"]["Enums"]["condition_clinical_status"]
code_code?: string
code_display?: string
code_system?: string
created_at?: string | null
encounter_id?: string | null
id?: string
identifier?: string | null
note?: string | null
onset_age?: number | null
onset_datetime?: string | null
patient_id?: string
recorded_date?: string
recorder_id?: string | null
severity_code?: string | null
severity_display?: string | null
updated_at?: string | null
verification_status?: Database["public"]["Enums"]["condition_verification_status"]
}
Relationships: [
{
foreignKeyName: "conditions_asserter_id_fkey"
columns: ["asserter_id"]
isOneToOne: false
referencedRelation: "practitioners"
referencedColumns: ["id"]
},
{
foreignKeyName: "conditions_encounter_id_fkey"
columns: ["encounter_id"]
isOneToOne: false
referencedRelation: "encounters"
referencedColumns: ["id"]
},
{
foreignKeyName: "conditions_patient_id_fkey"
columns: ["patient_id"]
isOneToOne: false
referencedRelation: "patients"
referencedColumns: ["id"]
},
{
foreignKeyName: "conditions_recorder_id_fkey"
columns: ["recorder_id"]
isOneToOne: false
referencedRelation: "practitioners"
referencedColumns: ["id"]
},
]
}
demo_users: {
Row: {
access_level: string
created_at: string
expires_at: string | null
id: string
last_login_at: string | null
notes: string | null
updated_at: string
usage_count: number | null
user_id: string | null
}
Insert: {
access_level?: string
created_at?: string
expires_at?: string | null
id?: string
last_login_at?: string | null
notes?: string | null
updated_at?: string
usage_count?: number | null
user_id?: string | null
}
Update: {
access_level?: string
created_at?: string
expires_at?: string | null
id?: string
last_login_at?: string | null
notes?: string | null
updated_at?: string
usage_count?: number | null
user_id?: string | null
}
Relationships: []
}
encounters: {
Row: {
admission_source: string | null
class_code: string
class_display: string
created_at: string | null
discharge_disposition: string | null
id: string
identifier: string | null
intake_note_id: string | null
notes: string | null
organization_id: string | null
patient_id: string
period_end: string | null
period_start: string
practitioner_id: string | null
priority_code: string | null
priority_display: string | null
reason_code: string[] | null
reason_display: string[] | null
status: Database["public"]["Enums"]["encounter_status"]
type_code: string
type_display: string
updated_at: string | null
}
Insert: {
admission_source?: string | null
class_code: string
class_display: string
created_at?: string | null
discharge_disposition?: string | null
id?: string
identifier?: string | null
intake_note_id?: string | null
notes?: string | null
organization_id?: string | null
patient_id: string
period_end?: string | null
period_start: string
practitioner_id?: string | null
priority_code?: string | null
priority_display?: string | null
reason_code?: string[] | null
reason_display?: string[] | null
status?: Database["public"]["Enums"]["encounter_status"]
type_code: string
type_display: string
updated_at?: string | null
}
Update: {
admission_source?: string | null
class_code?: string
class_display?: string
created_at?: string | null
discharge_disposition?: string | null
id?: string
identifier?: string | null
intake_note_id?: string | null
notes?: string | null
organization_id?: string | null
patient_id?: string
period_end?: string | null
period_start?: string
practitioner_id?: string | null
priority_code?: string | null
priority_display?: string | null
reason_code?: string[] | null
reason_display?: string[] | null
status?: Database["public"]["Enums"]["encounter_status"]
type_code?: string
type_display?: string
updated_at?: string | null
}
Relationships: [
{
foreignKeyName: "encounters_intake_note_id_fkey"
columns: ["intake_note_id"]
isOneToOne: false
referencedRelation: "intake_notes"
referencedColumns: ["id"]
},
{
foreignKeyName: "encounters_organization_id_fkey"
columns: ["organization_id"]
isOneToOne: false
referencedRelation: "organizations"
referencedColumns: ["id"]
},
{
foreignKeyName: "encounters_patient_id_fkey"
columns: ["patient_id"]
isOneToOne: false
referencedRelation: "patients"
referencedColumns: ["id"]
},
{
foreignKeyName: "encounters_practitioner_id_fkey"
columns: ["practitioner_id"]
isOneToOne: false
referencedRelation: "practitioners"
referencedColumns: ["id"]
},
]
}
intake_notes: {
Row: {
author: string | null
@@ -133,6 +480,319 @@ export type Database = {
},
]
}
observations: {
Row: {
body_site: string | null
category: string
code_code: string
code_display: string
code_system: string
created_at: string | null
effective_datetime: string
encounter_id: string | null
id: string
identifier: string | null
interpretation_code: string | null
interpretation_display: string | null
issued: string | null
method_code: string | null
method_display: string | null
note: string | null
patient_id: string
performer_id: string | null
reference_range_high: number | null
reference_range_low: number | null
reference_range_text: string | null
status: Database["public"]["Enums"]["observation_status"]
value_boolean: boolean | null
value_codeable_concept: Json | null
value_quantity_comparator: string | null
value_quantity_unit: string | null
value_quantity_value: number | null
value_string: string | null
value_type: string
}
Insert: {
body_site?: string | null
category: string
code_code: string
code_display: string
code_system: string
created_at?: string | null
effective_datetime: string
encounter_id?: string | null
id?: string
identifier?: string | null
interpretation_code?: string | null
interpretation_display?: string | null
issued?: string | null
method_code?: string | null
method_display?: string | null
note?: string | null
patient_id: string
performer_id?: string | null
reference_range_high?: number | null
reference_range_low?: number | null
reference_range_text?: string | null
status?: Database["public"]["Enums"]["observation_status"]
value_boolean?: boolean | null
value_codeable_concept?: Json | null
value_quantity_comparator?: string | null
value_quantity_unit?: string | null
value_quantity_value?: number | null
value_string?: string | null
value_type: string
}
Update: {
body_site?: string | null
category?: string
code_code?: string
code_display?: string
code_system?: string
created_at?: string | null
effective_datetime?: string
encounter_id?: string | null
id?: string
identifier?: string | null
interpretation_code?: string | null
interpretation_display?: string | null
issued?: string | null
method_code?: string | null
method_display?: string | null
note?: string | null
patient_id?: string
performer_id?: string | null
reference_range_high?: number | null
reference_range_low?: number | null
reference_range_text?: string | null
status?: Database["public"]["Enums"]["observation_status"]
value_boolean?: boolean | null
value_codeable_concept?: Json | null
value_quantity_comparator?: string | null
value_quantity_unit?: string | null
value_quantity_value?: number | null
value_string?: string | null
value_type?: string
}
Relationships: [
{
foreignKeyName: "observations_encounter_id_fkey"
columns: ["encounter_id"]
isOneToOne: false
referencedRelation: "encounters"
referencedColumns: ["id"]
},
{
foreignKeyName: "observations_patient_id_fkey"
columns: ["patient_id"]
isOneToOne: false
referencedRelation: "patients"
referencedColumns: ["id"]
},
{
foreignKeyName: "observations_performer_id_fkey"
columns: ["performer_id"]
isOneToOne: false
referencedRelation: "practitioners"
referencedColumns: ["id"]
},
]
}
organizations: {
Row: {
active: boolean | null
address_city: string | null
address_country: string | null
address_line: string[] | null
address_postal_code: string | null
alias: string[] | null
created_at: string | null
id: string
identifier_agb: string | null
identifier_kvk: string | null
name: string
telecom_email: string | null
telecom_phone: string | null
telecom_website: string | null
type_code: string | null
type_display: string | null
updated_at: string | null
}
Insert: {
active?: boolean | null
address_city?: string | null
address_country?: string | null
address_line?: string[] | null
address_postal_code?: string | null
alias?: string[] | null
created_at?: string | null
id?: string
identifier_agb?: string | null
identifier_kvk?: string | null
name: string
telecom_email?: string | null
telecom_phone?: string | null
telecom_website?: string | null
type_code?: string | null
type_display?: string | null
updated_at?: string | null
}
Update: {
active?: boolean | null
address_city?: string | null
address_country?: string | null
address_line?: string[] | null
address_postal_code?: string | null
alias?: string[] | null
created_at?: string | null
id?: string
identifier_agb?: string | null
identifier_kvk?: string | null
name?: string
telecom_email?: string | null
telecom_phone?: string | null
telecom_website?: string | null
type_code?: string | null
type_display?: string | null
updated_at?: string | null
}
Relationships: []
}
patients: {
Row: {
active: boolean | null
address_city: string | null
address_country: string | null
address_line: string[] | null
address_postal_code: string | null
birth_date: string
created_at: string | null
emergency_contact_name: string | null
emergency_contact_phone: string | null
emergency_contact_relationship: string | null
gender: Database["public"]["Enums"]["gender_type"]
general_practitioner_agb: string | null
general_practitioner_name: string | null
id: string
identifier_bsn: string | null
identifier_client_number: string | null
insurance_company: string | null
insurance_number: string | null
name_family: string
name_given: string[]
name_prefix: string | null
name_use: string | null
telecom_email: string | null
telecom_phone: string | null
updated_at: string | null
}
Insert: {
active?: boolean | null
address_city?: string | null
address_country?: string | null
address_line?: string[] | null
address_postal_code?: string | null
birth_date: string
created_at?: string | null
emergency_contact_name?: string | null
emergency_contact_phone?: string | null
emergency_contact_relationship?: string | null
gender: Database["public"]["Enums"]["gender_type"]
general_practitioner_agb?: string | null
general_practitioner_name?: string | null
id?: string
identifier_bsn?: string | null
identifier_client_number?: string | null
insurance_company?: string | null
insurance_number?: string | null
name_family: string
name_given: string[]
name_prefix?: string | null
name_use?: string | null
telecom_email?: string | null
telecom_phone?: string | null
updated_at?: string | null
}
Update: {
active?: boolean | null
address_city?: string | null
address_country?: string | null
address_line?: string[] | null
address_postal_code?: string | null
birth_date?: string
created_at?: string | null
emergency_contact_name?: string | null
emergency_contact_phone?: string | null
emergency_contact_relationship?: string | null
gender?: Database["public"]["Enums"]["gender_type"]
general_practitioner_agb?: string | null
general_practitioner_name?: string | null
id?: string
identifier_bsn?: string | null
identifier_client_number?: string | null
insurance_company?: string | null
insurance_number?: string | null
name_family?: string
name_given?: string[]
name_prefix?: string | null
name_use?: string | null
telecom_email?: string | null
telecom_phone?: string | null
updated_at?: string | null
}
Relationships: []
}
practitioners: {
Row: {
active: boolean | null
created_at: string | null
id: string
identifier_agb: string | null
identifier_big: string | null
name_family: string
name_given: string[]
name_prefix: string | null
name_suffix: string | null
qualification: string[] | null
telecom_email: string | null
telecom_phone: string | null
updated_at: string | null
user_id: string | null
}
Insert: {
active?: boolean | null
created_at?: string | null
id?: string
identifier_agb?: string | null
identifier_big?: string | null
name_family: string
name_given: string[]
name_prefix?: string | null
name_suffix?: string | null
qualification?: string[] | null
telecom_email?: string | null
telecom_phone?: string | null
updated_at?: string | null
user_id?: string | null
}
Update: {
active?: boolean | null
created_at?: string | null
id?: string
identifier_agb?: string | null
identifier_big?: string | null
name_family?: string
name_given?: string[]
name_prefix?: string | null
name_suffix?: string | null
qualification?: string[] | null
telecom_email?: string | null
telecom_phone?: string | null
updated_at?: string | null
user_id?: string | null
}
Relationships: []
}
problem_profiles: {
Row: {
category: string
@@ -230,10 +890,55 @@ export type Database = {
[_ in never]: never
}
Functions: {
[_ in never]: never
get_demo_access_level: {
Args: { check_user_id: string }
Returns: string
}
hook_check_duplicate_email: { Args: { event: Json }; Returns: Json }
is_demo_user: { Args: { check_user_id: string }; Returns: boolean }
}
Enums: {
[_ in never]: never
careplan_status:
| "draft"
| "active"
| "on-hold"
| "revoked"
| "completed"
| "entered-in-error"
| "unknown"
condition_clinical_status:
| "active"
| "recurrence"
| "relapse"
| "inactive"
| "remission"
| "resolved"
| "unknown"
condition_verification_status:
| "unconfirmed"
| "provisional"
| "differential"
| "confirmed"
| "refuted"
| "entered-in-error"
encounter_status:
| "planned"
| "in-progress"
| "on-hold"
| "completed"
| "cancelled"
| "entered-in-error"
| "unknown"
gender_type: "male" | "female" | "other" | "unknown"
observation_status:
| "registered"
| "preliminary"
| "final"
| "amended"
| "corrected"
| "cancelled"
| "entered-in-error"
| "unknown"
}
CompositeTypes: {
[_ in never]: never
@@ -360,6 +1065,53 @@ export type CompositeTypes<
export const Constants = {
public: {
Enums: {},
Enums: {
careplan_status: [
"draft",
"active",
"on-hold",
"revoked",
"completed",
"entered-in-error",
"unknown",
],
condition_clinical_status: [
"active",
"recurrence",
"relapse",
"inactive",
"remission",
"resolved",
"unknown",
],
condition_verification_status: [
"unconfirmed",
"provisional",
"differential",
"confirmed",
"refuted",
"entered-in-error",
],
encounter_status: [
"planned",
"in-progress",
"on-hold",
"completed",
"cancelled",
"entered-in-error",
"unknown",
],
gender_type: ["male", "female", "other", "unknown"],
observation_status: [
"registered",
"preliminary",
"final",
"amended",
"corrected",
"cancelled",
"entered-in-error",
"unknown",
],
},
},
} as const

8
lib/fhir/index.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* FHIR Library
* Main entry point for FHIR functionality
*/
export * from './types';
export * from './transforms';
export * from './utils';

View File

@@ -0,0 +1,7 @@
/**
* FHIR Transforms
* Export all transform functions
*/
export * from './patient';
export * from './practitioner';

View File

@@ -0,0 +1,198 @@
/**
* FHIR Patient Transforms
* Convert between Database rows and FHIR Patient resources
*/
import type { Tables, TablesInsert } from '../../database.types';
import type { FHIRPatient } from '../types';
type PatientRow = Tables<'patients'>;
type PatientInsert = TablesInsert<'patients'>;
/**
* Transform database Patient row to FHIR Patient resource
*/
export function dbPatientToFHIR(row: PatientRow): FHIRPatient {
return {
resourceType: 'Patient',
id: row.id,
// Identifiers (BSN, client number)
identifier: [
{
system: 'http://fhir.nl/fhir/NamingSystem/bsn',
value: row.identifier_bsn || undefined,
use: 'official' as const,
},
row.identifier_client_number
? {
system: 'urn:oid:2.16.840.1.113883.2.4.3.11.999.7.6',
value: row.identifier_client_number,
use: 'usual' as const,
}
: undefined,
].filter((x): x is NonNullable<typeof x> => x !== undefined),
// Active status
active: row.active ?? true,
// Name
name: [
{
use: (row.name_use as any) || 'official',
family: row.name_family,
given: row.name_given,
prefix: row.name_prefix ? [row.name_prefix] : undefined,
},
],
// Telecom (contact)
telecom: [
row.telecom_phone
? {
system: 'phone' as const,
value: row.telecom_phone,
use: 'mobile' as const,
}
: undefined,
row.telecom_email
? {
system: 'email' as const,
value: row.telecom_email,
}
: undefined,
].filter((x): x is NonNullable<typeof x> => x !== undefined),
// Gender
gender: row.gender,
// Birth date
birthDate: row.birth_date,
// Address
address: row.address_line
? [
{
use: 'home',
line: row.address_line,
city: row.address_city || undefined,
postalCode: row.address_postal_code || undefined,
country: row.address_country || 'NL',
},
]
: undefined,
// Contact person (emergency contact)
contact: row.emergency_contact_name
? [
{
relationship: [
{
coding: [
{
system: 'http://terminology.hl7.org/CodeSystem/v2-0131',
code: 'C',
display: row.emergency_contact_relationship || 'Emergency Contact',
},
],
},
],
name: {
text: row.emergency_contact_name,
},
telecom: row.emergency_contact_phone
? [
{
system: 'phone',
value: row.emergency_contact_phone,
},
]
: undefined,
},
]
: undefined,
// General Practitioner
generalPractitioner: row.general_practitioner_name
? [
{
display: row.general_practitioner_name,
identifier: row.general_practitioner_agb
? {
system: 'http://fhir.nl/fhir/NamingSystem/agb-z',
value: row.general_practitioner_agb,
}
: undefined,
},
]
: undefined,
// Meta (timestamps)
meta: {
lastUpdated: row.updated_at || undefined,
},
};
}
/**
* Transform FHIR Patient resource to database insert
*/
export function fhirPatientToDB(fhir: FHIRPatient): PatientInsert {
// Extract BSN from identifiers
const bsn = fhir.identifier?.find(
(i) => i.system === 'http://fhir.nl/fhir/NamingSystem/bsn'
)?.value;
// Extract client number from identifiers
const clientNumber = fhir.identifier?.find(
(i) => i.system === 'urn:oid:2.16.840.1.113883.2.4.3.11.999.7.6'
)?.value;
// Extract name (use first name)
const name = fhir.name?.[0];
// Extract address (use first address)
const address = fhir.address?.[0];
// Extract phone and email from telecom
const phone = fhir.telecom?.find((t) => t.system === 'phone')?.value;
const email = fhir.telecom?.find((t) => t.system === 'email')?.value;
// Extract emergency contact
const emergencyContact = fhir.contact?.[0];
const emergencyContactName = emergencyContact?.name?.text;
const emergencyContactPhone = emergencyContact?.telecom?.find(
(t) => t.system === 'phone'
)?.value;
const emergencyContactRelationship =
emergencyContact?.relationship?.[0]?.coding?.[0]?.display;
// Extract general practitioner
const gp = fhir.generalPractitioner?.[0];
const gpName = gp?.display;
const gpAgb = gp?.identifier?.value;
return {
id: fhir.id,
identifier_bsn: bsn || '999999990', // Default placeholder
identifier_client_number: clientNumber || undefined,
name_family: name?.family || '',
name_given: name?.given || [],
name_prefix: name?.prefix?.[0] || undefined,
name_use: name?.use || 'official',
birth_date: fhir.birthDate || '',
gender: fhir.gender || 'unknown',
telecom_phone: phone || undefined,
telecom_email: email || undefined,
address_line: address?.line || undefined,
address_city: address?.city || undefined,
address_postal_code: address?.postalCode || undefined,
address_country: address?.country || 'NL',
emergency_contact_name: emergencyContactName || undefined,
emergency_contact_phone: emergencyContactPhone || undefined,
emergency_contact_relationship: emergencyContactRelationship || undefined,
general_practitioner_name: gpName || undefined,
general_practitioner_agb: gpAgb || undefined,
active: fhir.active ?? true,
};
}

View File

@@ -0,0 +1,133 @@
/**
* FHIR Practitioner Transforms
* Convert between Database rows and FHIR Practitioner resources
*/
import type { Tables, TablesInsert } from '../../database.types';
import type { FHIRPractitioner } from '../types';
type PractitionerRow = Tables<'practitioners'>;
type PractitionerInsert = TablesInsert<'practitioners'>;
/**
* Transform database Practitioner row to FHIR Practitioner resource
*/
export function dbPractitionerToFHIR(row: PractitionerRow): FHIRPractitioner {
return {
resourceType: 'Practitioner',
id: row.id,
// Identifiers (BIG, AGB)
identifier: [
row.identifier_big
? {
system: 'http://fhir.nl/fhir/NamingSystem/big',
value: row.identifier_big,
use: 'official' as const,
}
: undefined,
row.identifier_agb
? {
system: 'http://fhir.nl/fhir/NamingSystem/agb-z',
value: row.identifier_agb,
use: 'official' as const,
}
: undefined,
].filter((x): x is NonNullable<typeof x> => x !== undefined),
// Active status
active: row.active ?? true,
// Name
name: [
{
use: 'official',
family: row.name_family,
given: row.name_given,
prefix: row.name_prefix ? [row.name_prefix] : undefined,
suffix: row.name_suffix ? [row.name_suffix] : undefined,
},
],
// Telecom (contact)
telecom: [
row.telecom_phone
? {
system: 'phone' as const,
value: row.telecom_phone,
use: 'work' as const,
}
: undefined,
row.telecom_email
? {
system: 'email' as const,
value: row.telecom_email,
use: 'work' as const,
}
: undefined,
].filter((x): x is NonNullable<typeof x> => x !== undefined),
// Qualifications (professional titles and specializations)
qualification: row.qualification
? row.qualification.map((qual) => ({
code: {
coding: [
{
system: 'http://terminology.hl7.org/CodeSystem/v2-0360',
display: qual,
},
],
text: qual,
},
}))
: undefined,
// Meta (timestamps)
meta: {
lastUpdated: row.updated_at || undefined,
},
};
}
/**
* Transform FHIR Practitioner resource to database insert
*/
export function fhirPractitionerToDB(
fhir: FHIRPractitioner
): PractitionerInsert {
// Extract BIG from identifiers
const big = fhir.identifier?.find(
(i) => i.system === 'http://fhir.nl/fhir/NamingSystem/big'
)?.value;
// Extract AGB from identifiers
const agb = fhir.identifier?.find(
(i) => i.system === 'http://fhir.nl/fhir/NamingSystem/agb-z'
)?.value;
// Extract name (use first name)
const name = fhir.name?.[0];
// Extract phone and email from telecom
const phone = fhir.telecom?.find((t) => t.system === 'phone')?.value;
const email = fhir.telecom?.find((t) => t.system === 'email')?.value;
// Extract qualifications
const qualifications = fhir.qualification?.map(
(q) => q.code.text || q.code.coding?.[0]?.display || ''
).filter(Boolean);
return {
id: fhir.id,
identifier_big: big || undefined,
identifier_agb: agb || undefined,
name_prefix: name?.prefix?.[0] || undefined,
name_given: name?.given || [],
name_family: name?.family || '',
name_suffix: name?.suffix?.[0] || undefined,
qualification: qualifications || undefined,
telecom_phone: phone || undefined,
telecom_email: email || undefined,
active: fhir.active ?? true,
};
}

220
lib/fhir/types/index.ts Normal file
View File

@@ -0,0 +1,220 @@
/**
* FHIR R4 Type Definitions
* Simplified types for pragmatic implementation
* Based on: http://hl7.org/fhir/R4/
*/
// ============================================================================
// Common FHIR Types
// ============================================================================
export interface FHIRIdentifier {
system?: string;
value?: string;
use?: 'usual' | 'official' | 'temp' | 'secondary';
}
export interface FHIRHumanName {
use?: 'usual' | 'official' | 'temp' | 'nickname' | 'anonymous' | 'old' | 'maiden';
text?: string;
family?: string;
given?: string[];
prefix?: string[];
suffix?: string[];
}
export interface FHIRContactPoint {
system?: 'phone' | 'fax' | 'email' | 'pager' | 'url' | 'sms' | 'other';
value?: string;
use?: 'home' | 'work' | 'temp' | 'old' | 'mobile';
rank?: number;
}
export interface FHIRAddress {
use?: 'home' | 'work' | 'temp' | 'old' | 'billing';
type?: 'postal' | 'physical' | 'both';
text?: string;
line?: string[];
city?: string;
district?: string;
state?: string;
postalCode?: string;
country?: string;
}
export interface FHIRCodeableConcept {
coding?: Array<{
system?: string;
version?: string;
code?: string;
display?: string;
}>;
text?: string;
}
export interface FHIRReference {
reference?: string;
type?: string;
identifier?: FHIRIdentifier;
display?: string;
}
export interface FHIRMeta {
versionId?: string;
lastUpdated?: string;
source?: string;
profile?: string[];
security?: FHIRCodeableConcept[];
tag?: FHIRCodeableConcept[];
}
// ============================================================================
// FHIR Patient Resource
// ============================================================================
export interface FHIRPatient {
resourceType: 'Patient';
id?: string;
meta?: FHIRMeta;
implicitRules?: string;
language?: string;
// Patient fields
identifier?: FHIRIdentifier[];
active?: boolean;
name?: FHIRHumanName[];
telecom?: FHIRContactPoint[];
gender?: 'male' | 'female' | 'other' | 'unknown';
birthDate?: string;
deceasedBoolean?: boolean;
deceasedDateTime?: string;
address?: FHIRAddress[];
maritalStatus?: FHIRCodeableConcept;
multipleBirthBoolean?: boolean;
multipleBirthInteger?: number;
photo?: Array<{
contentType?: string;
data?: string;
url?: string;
}>;
contact?: Array<{
relationship?: FHIRCodeableConcept[];
name?: FHIRHumanName;
telecom?: FHIRContactPoint[];
address?: FHIRAddress;
gender?: 'male' | 'female' | 'other' | 'unknown';
organization?: FHIRReference;
}>;
communication?: Array<{
language: FHIRCodeableConcept;
preferred?: boolean;
}>;
generalPractitioner?: FHIRReference[];
managingOrganization?: FHIRReference;
link?: Array<{
other: FHIRReference;
type: 'replaced-by' | 'replaces' | 'refer' | 'seealso';
}>;
}
// ============================================================================
// FHIR Practitioner Resource
// ============================================================================
export interface FHIRPractitioner {
resourceType: 'Practitioner';
id?: string;
meta?: FHIRMeta;
implicitRules?: string;
language?: string;
// Practitioner fields
identifier?: FHIRIdentifier[];
active?: boolean;
name?: FHIRHumanName[];
telecom?: FHIRContactPoint[];
address?: FHIRAddress[];
gender?: 'male' | 'female' | 'other' | 'unknown';
birthDate?: string;
photo?: Array<{
contentType?: string;
data?: string;
url?: string;
}>;
qualification?: Array<{
identifier?: FHIRIdentifier[];
code: FHIRCodeableConcept;
period?: {
start?: string;
end?: string;
};
issuer?: FHIRReference;
}>;
communication?: FHIRCodeableConcept[];
}
// ============================================================================
// FHIR Organization Resource
// ============================================================================
export interface FHIROrganization {
resourceType: 'Organization';
id?: string;
meta?: FHIRMeta;
identifier?: FHIRIdentifier[];
active?: boolean;
type?: FHIRCodeableConcept[];
name?: string;
alias?: string[];
telecom?: FHIRContactPoint[];
address?: FHIRAddress[];
partOf?: FHIRReference;
contact?: Array<{
purpose?: FHIRCodeableConcept;
name?: FHIRHumanName;
telecom?: FHIRContactPoint[];
address?: FHIRAddress;
}>;
endpoint?: FHIRReference[];
}
// ============================================================================
// Helper Types
// ============================================================================
/**
* FHIR Bundle for search results
*/
export interface FHIRBundle<T = any> {
resourceType: 'Bundle';
type: 'searchset' | 'collection' | 'transaction' | 'transaction-response' | 'batch' | 'batch-response' | 'history' | 'document' | 'message';
total?: number;
link?: Array<{
relation: string;
url: string;
}>;
entry?: Array<{
fullUrl?: string;
resource?: T;
search?: {
mode?: 'match' | 'include' | 'outcome';
score?: number;
};
}>;
}
/**
* FHIR OperationOutcome for errors
*/
export interface FHIROperationOutcome {
resourceType: 'OperationOutcome';
issue: Array<{
severity: 'fatal' | 'error' | 'warning' | 'information';
code: string;
details?: FHIRCodeableConcept;
diagnostics?: string;
location?: string[];
expression?: string[];
}>;
}

93
lib/fhir/utils.ts Normal file
View File

@@ -0,0 +1,93 @@
/**
* FHIR Utilities
* Helper functions for working with FHIR resources
*/
import type { FHIRReference, FHIROperationOutcome } from './types';
/**
* Extract resource ID from FHIR reference string
* Example: "Patient/123" -> "123"
*/
export function extractIdFromReference(reference?: string): string | null {
if (!reference) return null;
const parts = reference.split('/');
return parts.length === 2 ? parts[1] : null;
}
/**
* Create FHIR reference from resource type and ID
* Example: ("Patient", "123") -> "Patient/123"
*/
export function createReference(
resourceType: string,
id: string,
display?: string
): FHIRReference {
return {
reference: `${resourceType}/${id}`,
type: resourceType,
display,
};
}
/**
* Create FHIR OperationOutcome for errors
*/
export function createOperationOutcome(
severity: 'fatal' | 'error' | 'warning' | 'information',
code: string,
diagnostics: string
): FHIROperationOutcome {
return {
resourceType: 'OperationOutcome',
issue: [
{
severity,
code,
diagnostics,
},
],
};
}
/**
* Validate FHIR resource has required fields
*/
export function validateFHIRResource(
resource: any,
requiredFields: string[]
): { valid: boolean; errors: string[] } {
const errors: string[] = [];
for (const field of requiredFields) {
if (!resource[field]) {
errors.push(`Missing required field: ${field}`);
}
}
return {
valid: errors.length === 0,
errors,
};
}
/**
* Format date to FHIR date format (YYYY-MM-DD)
*/
export function toFHIRDate(date: Date | string): string {
if (typeof date === 'string') {
return date.split('T')[0];
}
return date.toISOString().split('T')[0];
}
/**
* Format datetime to FHIR datetime format (ISO 8601)
*/
export function toFHIRDateTime(date: Date | string): string {
if (typeof date === 'string') {
return date;
}
return date.toISOString();
}

View File

@@ -137,3 +137,95 @@ export async function getCategoryMetadata(): Promise<IndexData> {
}
}
}
/**
* Generate slug from heading text (must match the slugify function in mdx-components.tsx)
*/
function slugify(text: string): string {
return text
.toString()
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
.replace(/[^\w\-]+/g, '')
.replace(/\-\-+/g, '-')
}
/**
* Table of Contents item
*/
export interface TocItem {
id: string
text: string
level: number
}
/**
* Extract headings from MDX content for Table of Contents
* Extracts h1, h2, and h3 headings, filtered for main sections
*/
export function extractHeadings(content: string): TocItem[] {
const headingRegex = /^(#{1,3})\s+(.+)$/gm
const headings: TocItem[] = []
let match
// Main sections to include in TOC (h2 level)
const includedH2Sections = [
'overview',
'standaard-compliance',
'geimplementeerde-resources',
'relaties-tussen-resources',
'privacy-beveiliging',
'roadmap',
'patient-api',
'practitioner-api',
'encounter-api',
'condition-api',
'observation-api',
'careplan-api',
'authenticatie-autorisatie',
'error-handling',
]
// Resource subsections to include (h3 level)
const includedH3Sections = [
'1-practitioners-behandelaren',
'2-organizations-instellingen',
'3-patients-patientenclienten',
'4-encounters-contactmomenten',
'5-conditions-diagnoses',
'6-observations-metingen-en-observaties',
'7-careplans-behandelplannen',
]
while ((match = headingRegex.exec(content)) !== null) {
const level = match[1].length
const text = match[2]
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Remove markdown links
.replace(/`([^`]+)`/g, '$1') // Remove inline code
.replace(/\*\*([^*]+)\*\*/g, '$1') // Remove bold
.replace(/\*([^*]+)\*/g, '$1') // Remove italic
.trim()
const id = slugify(text)
// Include h2 headings from the main sections list
if (level === 2 && includedH2Sections.includes(id)) {
headings.push({
id,
text,
level,
})
}
// Include h3 headings from the resource sections list
else if (level === 3 && includedH3Sections.includes(id)) {
headings.push({
id,
text,
level,
})
}
}
return headings
}

File diff suppressed because it is too large Load Diff

View File

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

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