FHIR, API, datamodel
This commit is contained in:
@@ -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
8
lib/fhir/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* FHIR Library
|
||||
* Main entry point for FHIR functionality
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './transforms';
|
||||
export * from './utils';
|
||||
7
lib/fhir/transforms/index.ts
Normal file
7
lib/fhir/transforms/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* FHIR Transforms
|
||||
* Export all transform functions
|
||||
*/
|
||||
|
||||
export * from './patient';
|
||||
export * from './practitioner';
|
||||
198
lib/fhir/transforms/patient.ts
Normal file
198
lib/fhir/transforms/patient.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
133
lib/fhir/transforms/practitioner.ts
Normal file
133
lib/fhir/transforms/practitioner.ts
Normal 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
220
lib/fhir/types/index.ts
Normal 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
93
lib/fhir/utils.ts
Normal 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();
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
1058
lib/supabase/20241121_fhir_ggz_schema.sql
Normal file
1058
lib/supabase/20241121_fhir_ggz_schema.sql
Normal file
File diff suppressed because it is too large
Load Diff
704
lib/supabase/20241121_pragmatic_fhir_schema.sql
Normal file
704
lib/supabase/20241121_pragmatic_fhir_schema.sql
Normal 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
|
||||
-- ============================================================================
|
||||
235
lib/supabase/migrations/20241121_migrate_legacy_to_fhir.sql
Normal file
235
lib/supabase/migrations/20241121_migrate_legacy_to_fhir.sql
Normal file
@@ -0,0 +1,235 @@
|
||||
-- ============================================================================
|
||||
-- DATA MIGRATION: Legacy tables → FHIR tables
|
||||
-- ============================================================================
|
||||
-- Created: 2024-11-21
|
||||
-- Purpose: Migrate existing data from clients/treatment_plans to patients/care_plans
|
||||
--
|
||||
-- IMPORTANT: This migration preserves UUIDs for referential integrity
|
||||
-- Run this AFTER applying the pragmatic FHIR schema
|
||||
-- ============================================================================
|
||||
|
||||
-- ============================================================================
|
||||
-- STEP 1: Migrate clients → patients
|
||||
-- ============================================================================
|
||||
|
||||
-- Check for existing data
|
||||
DO $$
|
||||
DECLARE
|
||||
client_count INTEGER;
|
||||
patient_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO client_count FROM clients;
|
||||
SELECT COUNT(*) INTO patient_count FROM patients;
|
||||
|
||||
RAISE NOTICE 'Found % clients, % patients (before migration)', client_count, patient_count;
|
||||
END $$;
|
||||
|
||||
-- Migrate clients to patients
|
||||
INSERT INTO patients (
|
||||
id, -- Preserve UUID for referential integrity
|
||||
name_family, -- clients.last_name
|
||||
name_given, -- clients.first_name (as array)
|
||||
birth_date, -- clients.birth_date
|
||||
gender, -- Default 'unknown' (required field)
|
||||
identifier_bsn, -- Placeholder
|
||||
identifier_client_number, -- Use original client ID as client number
|
||||
active, -- Default true
|
||||
created_at, -- Preserve timestamp
|
||||
updated_at -- Preserve timestamp
|
||||
)
|
||||
SELECT
|
||||
c.id,
|
||||
c.last_name,
|
||||
ARRAY[c.first_name], -- Convert string to array
|
||||
c.birth_date,
|
||||
'unknown'::gender_type, -- Required field, default to unknown
|
||||
'999999990', -- Placeholder BSN (demo)
|
||||
c.id::text, -- Use UUID as client number for traceability
|
||||
true,
|
||||
c.created_at,
|
||||
c.updated_at
|
||||
FROM clients c
|
||||
WHERE NOT EXISTS (
|
||||
-- Don't re-migrate if already exists
|
||||
SELECT 1 FROM patients p WHERE p.id = c.id
|
||||
);
|
||||
|
||||
-- Report migration results
|
||||
DO $$
|
||||
DECLARE
|
||||
migrated_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO migrated_count
|
||||
FROM patients p
|
||||
INNER JOIN clients c ON p.id = c.id;
|
||||
|
||||
RAISE NOTICE 'Successfully migrated % clients to patients', migrated_count;
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- STEP 2: Migrate treatment_plans → care_plans
|
||||
-- ============================================================================
|
||||
|
||||
-- Check for existing data
|
||||
DO $$
|
||||
DECLARE
|
||||
plan_count INTEGER;
|
||||
careplan_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO plan_count FROM treatment_plans;
|
||||
SELECT COUNT(*) INTO careplan_count FROM care_plans;
|
||||
|
||||
RAISE NOTICE 'Found % treatment_plans, % care_plans (before migration)', plan_count, careplan_count;
|
||||
END $$;
|
||||
|
||||
-- Migrate treatment_plans to care_plans
|
||||
INSERT INTO care_plans (
|
||||
id, -- Preserve UUID
|
||||
identifier, -- Generate from UUID
|
||||
status, -- Map from treatment_plans.status
|
||||
title, -- Generate default title
|
||||
description, -- Extract from plan JSONB if available
|
||||
patient_id, -- treatment_plans.client_id → patient_id
|
||||
goals, -- Extract from plan.doelen
|
||||
activities, -- Extract from plan.interventies
|
||||
period_start, -- Derive from created_at
|
||||
period_end, -- Derive from created_at + 3 months (default)
|
||||
created_date, -- Preserve created_at
|
||||
created_at, -- Preserve created_at
|
||||
updated_at -- Preserve updated_at
|
||||
)
|
||||
SELECT
|
||||
tp.id,
|
||||
tp.id::text, -- Use UUID as identifier
|
||||
CASE
|
||||
WHEN tp.status = 'concept' THEN 'draft'::careplan_status
|
||||
WHEN tp.status = 'gepubliceerd' THEN 'active'::careplan_status
|
||||
ELSE 'draft'::careplan_status
|
||||
END,
|
||||
'Behandelplan v' || tp.version::text, -- Default title
|
||||
NULL, -- No description in legacy schema
|
||||
tp.client_id, -- Maps to patient_id (already migrated)
|
||||
-- Transform goals from legacy structure to FHIR Goal structure
|
||||
COALESCE(
|
||||
(
|
||||
SELECT jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'description', jsonb_build_object('text', goal),
|
||||
'lifecycleStatus', 'active'
|
||||
)
|
||||
)
|
||||
FROM jsonb_array_elements_text(tp.plan->'doelen') goal
|
||||
),
|
||||
'[]'::jsonb
|
||||
),
|
||||
-- Transform activities from legacy structure to FHIR CarePlan.activity structure
|
||||
COALESCE(
|
||||
(
|
||||
SELECT jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'detail', jsonb_build_object(
|
||||
'code', jsonb_build_object('text', interventie),
|
||||
'status', 'in-progress',
|
||||
'scheduledString', COALESCE(tp.plan->>'frequentie', 'Niet gespecificeerd')
|
||||
)
|
||||
)
|
||||
)
|
||||
FROM jsonb_array_elements_text(tp.plan->'interventies') interventie
|
||||
),
|
||||
'[]'::jsonb
|
||||
),
|
||||
tp.created_at::date, -- Start date from creation
|
||||
(tp.created_at + interval '3 months')::date, -- Default 3-month treatment
|
||||
tp.created_at,
|
||||
tp.created_at,
|
||||
tp.updated_at
|
||||
FROM treatment_plans tp
|
||||
WHERE NOT EXISTS (
|
||||
-- Don't re-migrate if already exists
|
||||
SELECT 1 FROM care_plans cp WHERE cp.id = tp.id
|
||||
)
|
||||
-- Ensure the client was migrated to patients first
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM patients p WHERE p.id = tp.client_id
|
||||
);
|
||||
|
||||
-- Report migration results
|
||||
DO $$
|
||||
DECLARE
|
||||
migrated_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO migrated_count
|
||||
FROM care_plans cp
|
||||
INNER JOIN treatment_plans tp ON cp.id = tp.id;
|
||||
|
||||
RAISE NOTICE 'Successfully migrated % treatment_plans to care_plans', migrated_count;
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- STEP 3: Verification queries
|
||||
-- ============================================================================
|
||||
|
||||
-- Verify patient migration
|
||||
DO $$
|
||||
DECLARE
|
||||
client_count INTEGER;
|
||||
patient_count INTEGER;
|
||||
match_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO client_count FROM clients;
|
||||
SELECT COUNT(*) INTO patient_count FROM patients;
|
||||
SELECT COUNT(*) INTO match_count
|
||||
FROM patients p
|
||||
INNER JOIN clients c ON p.id = c.id;
|
||||
|
||||
RAISE NOTICE '=== MIGRATION VERIFICATION ===';
|
||||
RAISE NOTICE 'Clients: %, Patients: %, Matched: %', client_count, patient_count, match_count;
|
||||
|
||||
IF match_count = client_count THEN
|
||||
RAISE NOTICE '✓ All clients successfully migrated to patients';
|
||||
ELSE
|
||||
RAISE WARNING '✗ Migration incomplete: % clients not migrated', (client_count - match_count);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Verify care plan migration
|
||||
DO $$
|
||||
DECLARE
|
||||
plan_count INTEGER;
|
||||
careplan_count INTEGER;
|
||||
match_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO plan_count FROM treatment_plans;
|
||||
SELECT COUNT(*) INTO careplan_count FROM care_plans;
|
||||
SELECT COUNT(*) INTO match_count
|
||||
FROM care_plans cp
|
||||
INNER JOIN treatment_plans tp ON cp.id = tp.id;
|
||||
|
||||
RAISE NOTICE 'Treatment Plans: %, Care Plans: %, Matched: %', plan_count, careplan_count, match_count;
|
||||
|
||||
IF plan_count = 0 THEN
|
||||
RAISE NOTICE '- No treatment plans to migrate (table empty)';
|
||||
ELSIF match_count = plan_count THEN
|
||||
RAISE NOTICE '✓ All treatment plans successfully migrated to care_plans';
|
||||
ELSE
|
||||
RAISE WARNING '✗ Migration incomplete: % treatment plans not migrated', (plan_count - match_count);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- SUMMARY
|
||||
-- ============================================================================
|
||||
-- This migration script:
|
||||
-- ✓ Migrates clients → patients (preserving UUIDs)
|
||||
-- ✓ Migrates treatment_plans → care_plans (preserving UUIDs)
|
||||
-- ✓ Transforms legacy JSONB structure to FHIR-compliant structure
|
||||
-- ✓ Sets sensible defaults for required FHIR fields
|
||||
-- ✓ Idempotent: safe to re-run (uses NOT EXISTS checks)
|
||||
-- ✓ Preserves referential integrity
|
||||
--
|
||||
-- NEXT STEPS:
|
||||
-- 1. Run this migration: supabase db push or apply_migration
|
||||
-- 2. Verify data in patients and care_plans tables
|
||||
-- 3. Update application code to use new FHIR tables
|
||||
-- 4. Optionally: keep legacy tables for rollback, or drop after verification
|
||||
-- ============================================================================
|
||||
418
lib/supabase/migrations/20241121_seed_demo_data.sql
Normal file
418
lib/supabase/migrations/20241121_seed_demo_data.sql
Normal file
@@ -0,0 +1,418 @@
|
||||
-- ============================================================================
|
||||
-- SEED DEMO DATA: Practitioners, Patients, Encounters
|
||||
-- ============================================================================
|
||||
-- Created: 2024-11-21
|
||||
-- Purpose: Populate FHIR tables with realistic demo data for testing
|
||||
-- ============================================================================
|
||||
|
||||
-- ============================================================================
|
||||
-- STEP 1: Seed Practitioners (Behandelaren)
|
||||
-- ============================================================================
|
||||
|
||||
-- Get the demo organization ID
|
||||
DO $$
|
||||
DECLARE
|
||||
demo_org_id UUID;
|
||||
BEGIN
|
||||
SELECT id INTO demo_org_id FROM organizations WHERE identifier_agb = 'AGB-DEMO-001';
|
||||
RAISE NOTICE 'Demo organization ID: %', demo_org_id;
|
||||
END $$;
|
||||
|
||||
-- Practitioner 1: Dr. Sarah de Vries (GZ-psycholoog)
|
||||
INSERT INTO practitioners (
|
||||
id,
|
||||
identifier_big,
|
||||
identifier_agb,
|
||||
name_prefix,
|
||||
name_given,
|
||||
name_family,
|
||||
qualification,
|
||||
telecom_phone,
|
||||
telecom_email,
|
||||
active
|
||||
)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000001'::uuid,
|
||||
'79012345601',
|
||||
'03012345',
|
||||
'Dr.',
|
||||
ARRAY['Sarah'],
|
||||
'de Vries',
|
||||
ARRAY['GZ-psycholoog', 'Cognitieve gedragstherapie', 'EMDR'],
|
||||
'06-12345678',
|
||||
's.devries@demo-ggz.nl',
|
||||
true
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Practitioner 2: Drs. Mark Jansen (Psychotherapeut)
|
||||
INSERT INTO practitioners (
|
||||
id,
|
||||
identifier_big,
|
||||
identifier_agb,
|
||||
name_prefix,
|
||||
name_given,
|
||||
name_family,
|
||||
qualification,
|
||||
telecom_phone,
|
||||
telecom_email,
|
||||
active
|
||||
)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000002'::uuid,
|
||||
'79023456702',
|
||||
'03023456',
|
||||
'Drs.',
|
||||
ARRAY['Mark'],
|
||||
'Jansen',
|
||||
ARRAY['Psychotherapeut', 'Schematherapie', 'Acceptance and Commitment Therapy'],
|
||||
'06-23456789',
|
||||
'm.jansen@demo-ggz.nl',
|
||||
true
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Practitioner 3: Lisa van den Berg (Klinisch psycholoog)
|
||||
INSERT INTO practitioners (
|
||||
id,
|
||||
identifier_big,
|
||||
identifier_agb,
|
||||
name_prefix,
|
||||
name_given,
|
||||
name_family,
|
||||
qualification,
|
||||
telecom_phone,
|
||||
telecom_email,
|
||||
active
|
||||
)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000003'::uuid,
|
||||
'79034567803',
|
||||
'03034567',
|
||||
NULL,
|
||||
ARRAY['Lisa'],
|
||||
'van den Berg',
|
||||
ARRAY['Klinisch psycholoog', 'Diagnostiek', 'ROM-coördinator'],
|
||||
'06-34567890',
|
||||
'l.vandenberg@demo-ggz.nl',
|
||||
true
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
RAISE NOTICE '✓ Seeded 3 practitioners';
|
||||
|
||||
-- ============================================================================
|
||||
-- STEP 2: Update existing patients with complete data
|
||||
-- ============================================================================
|
||||
|
||||
-- Get patient IDs from migrated clients
|
||||
DO $$
|
||||
DECLARE
|
||||
patient_colin_id UUID;
|
||||
patient_jan_id UUID;
|
||||
patient_optimus_id UUID;
|
||||
BEGIN
|
||||
-- Get Colin's patient ID
|
||||
SELECT id INTO patient_colin_id FROM patients WHERE name_family = 'Lit' AND 'Colin' = ANY(name_given);
|
||||
|
||||
-- Get Jan's patient ID
|
||||
SELECT id INTO patient_jan_id FROM patients WHERE name_family = 'de Vriesh' AND 'Jan' = ANY(name_given);
|
||||
|
||||
-- Get Optimus's patient ID
|
||||
SELECT id INTO patient_optimus_id FROM patients WHERE name_family = 'Prime' AND 'Optimus' = ANY(name_given);
|
||||
|
||||
-- Update Colin with complete data
|
||||
UPDATE patients
|
||||
SET
|
||||
gender = 'male',
|
||||
telecom_phone = '06-11111111',
|
||||
telecom_email = 'colin.lit@example.com',
|
||||
address_line = ARRAY['Kerkstraat 12'],
|
||||
address_city = 'Amsterdam',
|
||||
address_postal_code = '1012 AB',
|
||||
insurance_company = 'VGZ',
|
||||
insurance_number = 'VGZ-123456',
|
||||
general_practitioner_name = 'Dr. A. Huisarts',
|
||||
general_practitioner_agb = '12345678'
|
||||
WHERE id = patient_colin_id;
|
||||
|
||||
-- Update Jan with complete data
|
||||
UPDATE patients
|
||||
SET
|
||||
gender = 'male',
|
||||
telecom_phone = '06-22222222',
|
||||
telecom_email = 'jan.devriesh@example.com',
|
||||
address_line = ARRAY['Hoofdstraat 45'],
|
||||
address_city = 'Utrecht',
|
||||
address_postal_code = '3511 AB',
|
||||
insurance_company = 'CZ',
|
||||
insurance_number = 'CZ-789012',
|
||||
general_practitioner_name = 'Dr. B. Dokter',
|
||||
general_practitioner_agb = '23456789'
|
||||
WHERE id = patient_jan_id;
|
||||
|
||||
-- Update Optimus with complete data (easter egg patient)
|
||||
UPDATE patients
|
||||
SET
|
||||
gender = 'other',
|
||||
telecom_phone = '06-99999999',
|
||||
address_line = ARRAY['Cybertron Base 1'],
|
||||
address_city = 'Eindhoven',
|
||||
address_postal_code = '5600 AA',
|
||||
insurance_company = 'Menzis',
|
||||
insurance_number = 'MEN-PRIME-01'
|
||||
WHERE id = patient_optimus_id;
|
||||
|
||||
RAISE NOTICE '✓ Updated 3 existing patients with complete data';
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- STEP 3: Seed Encounters (Contactmomenten)
|
||||
-- ============================================================================
|
||||
|
||||
-- Get organization ID
|
||||
DO $$
|
||||
DECLARE
|
||||
demo_org_id UUID;
|
||||
patient_colin_id UUID;
|
||||
patient_jan_id UUID;
|
||||
patient_optimus_id UUID;
|
||||
practitioner_sarah_id UUID := '10000000-0000-0000-0000-000000000001'::uuid;
|
||||
practitioner_mark_id UUID := '10000000-0000-0000-0000-000000000002'::uuid;
|
||||
practitioner_lisa_id UUID := '10000000-0000-0000-0000-000000000003'::uuid;
|
||||
BEGIN
|
||||
-- Get IDs
|
||||
SELECT id INTO demo_org_id FROM organizations WHERE identifier_agb = 'AGB-DEMO-001';
|
||||
SELECT id INTO patient_colin_id FROM patients WHERE name_family = 'Lit' AND 'Colin' = ANY(name_given);
|
||||
SELECT id INTO patient_jan_id FROM patients WHERE name_family = 'de Vriesh' AND 'Jan' = ANY(name_given);
|
||||
SELECT id INTO patient_optimus_id FROM patients WHERE name_family = 'Prime' AND 'Optimus' = ANY(name_given);
|
||||
|
||||
-- Encounter 1: Colin's intake with Dr. Sarah de Vries (completed)
|
||||
INSERT INTO encounters (
|
||||
id,
|
||||
status,
|
||||
class_code,
|
||||
class_display,
|
||||
type_code,
|
||||
type_display,
|
||||
priority_code,
|
||||
priority_display,
|
||||
patient_id,
|
||||
practitioner_id,
|
||||
organization_id,
|
||||
period_start,
|
||||
period_end,
|
||||
reason_code,
|
||||
reason_display,
|
||||
notes
|
||||
)
|
||||
VALUES (
|
||||
'20000000-0000-0000-0000-000000000001'::uuid,
|
||||
'completed',
|
||||
'AMB',
|
||||
'Ambulatory',
|
||||
'intake',
|
||||
'Intake gesprek',
|
||||
'routine',
|
||||
'Routine',
|
||||
patient_colin_id,
|
||||
practitioner_sarah_id,
|
||||
demo_org_id,
|
||||
'2024-10-15 10:00:00+00',
|
||||
'2024-10-15 11:00:00+00',
|
||||
ARRAY['F32.1', 'F51.0'],
|
||||
ARRAY['Matige depressieve episode', 'Insomnia'],
|
||||
'Eerste intake gesprek. Cliënt presenteert zich met depressieve klachten en slaapproblemen sinds 3 maanden. PHQ-9 score: 14.'
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Encounter 2: Jan's intake with Drs. Mark Jansen (completed)
|
||||
INSERT INTO encounters (
|
||||
id,
|
||||
status,
|
||||
class_code,
|
||||
class_display,
|
||||
type_code,
|
||||
type_display,
|
||||
priority_code,
|
||||
priority_display,
|
||||
patient_id,
|
||||
practitioner_id,
|
||||
organization_id,
|
||||
period_start,
|
||||
period_end,
|
||||
reason_code,
|
||||
reason_display,
|
||||
notes
|
||||
)
|
||||
VALUES (
|
||||
'20000000-0000-0000-0000-000000000002'::uuid,
|
||||
'completed',
|
||||
'AMB',
|
||||
'Ambulatory',
|
||||
'intake',
|
||||
'Intake gesprek',
|
||||
'routine',
|
||||
'Routine',
|
||||
patient_jan_id,
|
||||
practitioner_mark_id,
|
||||
demo_org_id,
|
||||
'2024-11-01 14:00:00+00',
|
||||
'2024-11-01 15:30:00+00',
|
||||
ARRAY['F41.1', 'Z63.0'],
|
||||
ARRAY['Gegeneraliseerde angststoornis', 'Relatieproblemen'],
|
||||
'Intake gesprek. Cliënt geeft aan last te hebben van voortdurende piekeren en angstklachten. GAD-7 score: 16. Ook relationele problematiek.'
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Encounter 3: Colin's 2nd session (planned)
|
||||
INSERT INTO encounters (
|
||||
id,
|
||||
status,
|
||||
class_code,
|
||||
class_display,
|
||||
type_code,
|
||||
type_display,
|
||||
priority_code,
|
||||
priority_display,
|
||||
patient_id,
|
||||
practitioner_id,
|
||||
organization_id,
|
||||
period_start,
|
||||
reason_code,
|
||||
reason_display,
|
||||
notes
|
||||
)
|
||||
VALUES (
|
||||
'20000000-0000-0000-0000-000000000003'::uuid,
|
||||
'planned',
|
||||
'AMB',
|
||||
'Ambulatory',
|
||||
'behandeling',
|
||||
'Behandelsessie',
|
||||
'routine',
|
||||
'Routine',
|
||||
patient_colin_id,
|
||||
practitioner_sarah_id,
|
||||
demo_org_id,
|
||||
'2024-11-25 10:00:00+00',
|
||||
ARRAY['F32.1'],
|
||||
ARRAY['Matige depressieve episode'],
|
||||
'Tweede sessie CGT gepland. Focus op gedragsactivatie en cognitieve herstructurering.'
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Encounter 4: Optimus diagnostiek with Lisa (completed - easter egg)
|
||||
INSERT INTO encounters (
|
||||
id,
|
||||
status,
|
||||
class_code,
|
||||
class_display,
|
||||
type_code,
|
||||
type_display,
|
||||
patient_id,
|
||||
practitioner_id,
|
||||
organization_id,
|
||||
period_start,
|
||||
period_end,
|
||||
reason_code,
|
||||
reason_display,
|
||||
notes
|
||||
)
|
||||
VALUES (
|
||||
'20000000-0000-0000-0000-000000000004'::uuid,
|
||||
'completed',
|
||||
'AMB',
|
||||
'Ambulatory',
|
||||
'diagnostiek',
|
||||
'Diagnostisch onderzoek',
|
||||
patient_optimus_id,
|
||||
practitioner_lisa_id,
|
||||
demo_org_id,
|
||||
'2024-11-10 09:00:00+00',
|
||||
'2024-11-10 11:00:00+00',
|
||||
ARRAY['Z03.2'],
|
||||
ARRAY['Observatie voor vermoede psychische aandoening'],
|
||||
'Diagnostisch onderzoek. Cliënt vertoont opvallende communicatiepatronen en metallic spraakpatroon. Nader onderzoek geïndiceerd.'
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Encounter 5: Jan's follow-up (in-progress)
|
||||
INSERT INTO encounters (
|
||||
id,
|
||||
status,
|
||||
class_code,
|
||||
class_display,
|
||||
type_code,
|
||||
type_display,
|
||||
patient_id,
|
||||
practitioner_id,
|
||||
organization_id,
|
||||
period_start,
|
||||
reason_code,
|
||||
reason_display,
|
||||
notes
|
||||
)
|
||||
VALUES (
|
||||
'20000000-0000-0000-0000-000000000005'::uuid,
|
||||
'in-progress',
|
||||
'AMB',
|
||||
'Ambulatory',
|
||||
'behandeling',
|
||||
'Behandelsessie',
|
||||
patient_jan_id,
|
||||
practitioner_mark_id,
|
||||
demo_org_id,
|
||||
NOW(),
|
||||
ARRAY['F41.1'],
|
||||
ARRAY['Gegeneraliseerde angststoornis'],
|
||||
'Sessie 3: ACT technieken. Werk aan psychologische flexibiliteit en acceptatie.'
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
RAISE NOTICE '✓ Seeded 5 encounters';
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- VERIFICATION
|
||||
-- ============================================================================
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
practitioner_count INTEGER;
|
||||
patient_count INTEGER;
|
||||
encounter_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO practitioner_count FROM practitioners;
|
||||
SELECT COUNT(*) INTO patient_count FROM patients;
|
||||
SELECT COUNT(*) INTO encounter_count FROM encounters;
|
||||
|
||||
RAISE NOTICE '=== SEED DATA SUMMARY ===';
|
||||
RAISE NOTICE 'Practitioners: %', practitioner_count;
|
||||
RAISE NOTICE 'Patients: %', patient_count;
|
||||
RAISE NOTICE 'Encounters: %', encounter_count;
|
||||
|
||||
IF practitioner_count >= 3 AND patient_count >= 3 AND encounter_count >= 5 THEN
|
||||
RAISE NOTICE '✓ All demo data seeded successfully!';
|
||||
ELSE
|
||||
RAISE WARNING '⚠ Some demo data may be missing';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- SUMMARY
|
||||
-- ============================================================================
|
||||
-- ✓ 3 Practitioners created (Sarah, Mark, Lisa)
|
||||
-- ✓ 3 Patients updated with complete data (Colin, Jan, Optimus)
|
||||
-- ✓ 5 Encounters created:
|
||||
-- - 2 completed intakes
|
||||
-- - 1 planned session
|
||||
-- - 1 completed diagnostiek
|
||||
-- - 1 in-progress behandeling
|
||||
--
|
||||
-- NEXT STEPS:
|
||||
-- 1. Test frontend with demo data
|
||||
-- 2. Create conditions (diagnoses) for patients
|
||||
-- 3. Create care_plans (behandelplannen)
|
||||
-- 4. Add observations (ROM scores)
|
||||
-- ============================================================================
|
||||
Reference in New Issue
Block a user