feat: migrate clients module to patients + add docs

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

69
docs/api/intakes-api.md Normal file
View File

@@ -0,0 +1,69 @@
# Intakes API
Deze custom REST API ondersteunt de intake-flow na consolidatie van `/clients/``/patients/`. Alle endpoints verwachten een geldige Supabase sessie (cookies) en geven JSON terug.
## Endpoints
### GET `/api/intakes?patientId={uuid}`
Haalt alle intakes voor één patiënt op (nieuwste eerst).
**Query parameters**
- `patientId` _(verplicht)_ — UUID van de patiënt.
**Response**
```json
{
"intakes": [
{
"id": "uuid",
"patient_id": "uuid",
"title": "Intake - Aanvang zorg",
"department": "Volwassenen",
"status": "bezig",
"start_date": "2025-11-22",
"end_date": null,
"psychologist_id": null,
"notes": null
}
],
"total": 1
}
```
### POST `/api/intakes`
Maakt een nieuwe intake.
**Body**
```json
{
"patient_id": "uuid",
"title": "Intake - Aanvang zorg",
"department": "Volwassenen",
"start_date": "2025-11-22",
"psychologist_id": "uuid?",
"notes": "optional"
}
```
**Responses**
- `201` + intake object bij succes
- `400` met `details[]` bij validatiefout
### GET `/api/intakes/{intakeId}`
Levert één intake. Retourneert `404` als het ID niet bestaat.
### PUT `/api/intakes/{intakeId}`
Partiële update. Ondersteunt `title`, `department`, `status` (`Open`/`Afgerond`), `start_date`, `end_date`, `psychologist_id`, `notes`.
### DELETE `/api/intakes/{intakeId}`
Verwijdert een intake. `204 No Content` bij succes.
## Fouten
- `401`/`403`: geen sessie of onvoldoende rechten.
- `400`: ongeldige payload (zie `details`).
- `500`: onverwachte fout; check server logs.
## Implementatieverwijzing
- Type definities: `lib/types/intake.ts`
- Server actions: `app/epd/patients/[id]/intakes/actions.ts`
- API broncode: `app/api/intakes/` en `app/api/intakes/[intakeId]/`

View File

@@ -0,0 +1,180 @@
-- ================================================
-- RLS Policy Tests
-- Created: 2024-11-15
-- Epic: E2 - Database & Auth
-- Story: E2.S2 - RLS policies implementeren
-- ================================================
-- This file contains test queries to verify RLS policies
-- Run these queries manually to verify RLS is working correctly
-- ================================================
-- ================================================
-- TEST 1: Verify RLS is enabled on all tables
-- ================================================
-- Expected: All tables should have rowsecurity = true
SELECT
tablename,
rowsecurity as rls_enabled
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
-- Expected output:
-- ai_events | true
-- clients | true
-- intake_notes | true
-- problem_profiles | true
-- treatment_plans | true
-- ================================================
-- TEST 2: Check all RLS policies exist
-- ================================================
-- Expected: Each table should have 4 policies (SELECT, INSERT, UPDATE, DELETE)
-- except ai_events which has only 2 (SELECT, INSERT)
SELECT
tablename,
COUNT(*) as policy_count,
STRING_AGG(cmd, ', ' ORDER BY cmd) as commands
FROM pg_policies
WHERE schemaname = 'public'
GROUP BY tablename
ORDER BY tablename;
-- Expected output:
-- ai_events | 2 | INSERT, SELECT
-- clients | 4 | DELETE, INSERT, SELECT, UPDATE
-- intake_notes | 4 | DELETE, INSERT, SELECT, UPDATE
-- problem_profiles | 4 | DELETE, INSERT, SELECT, UPDATE
-- treatment_plans | 4 | DELETE, INSERT, SELECT, UPDATE
-- ================================================
-- TEST 3: Verify policy predicates use auth.uid()
-- ================================================
-- Expected: All policies should check auth.uid() IS NOT NULL
SELECT
tablename,
policyname,
cmd,
qual as using_clause,
with_check
FROM pg_policies
WHERE schemaname = 'public'
AND qual NOT LIKE '%auth.uid()%'
ORDER BY tablename, policyname;
-- Expected output: Empty (no policies without auth.uid() check)
-- ================================================
-- TEST 4: Simulate authenticated user query
-- ================================================
-- This test simulates what happens when an authenticated user
-- tries to access data. In production, auth.uid() would return
-- the user's actual UUID.
-- Note: These queries will work in the SQL editor when logged in,
-- but will fail when run as unauthenticated
-- Test SELECT permission (should succeed when authenticated)
-- SELECT * FROM clients LIMIT 1;
-- Test INSERT permission (should succeed when authenticated)
-- INSERT INTO clients (first_name, last_name, birth_date)
-- VALUES ('Test', 'User', '1990-01-01');
-- Test UPDATE permission (should succeed when authenticated)
-- UPDATE clients SET first_name = 'Updated' WHERE id = 'some-uuid';
-- Test DELETE permission (should succeed when authenticated)
-- DELETE FROM clients WHERE id = 'some-uuid';
-- ================================================
-- TEST 5: Verify ai_events immutability
-- ================================================
-- Expected: ai_events should NOT have UPDATE or DELETE policies
-- (except for service role via RLS bypass)
SELECT
tablename,
policyname,
cmd
FROM pg_policies
WHERE schemaname = 'public'
AND tablename = 'ai_events'
AND cmd IN ('UPDATE', 'DELETE')
ORDER BY cmd;
-- Expected output: Empty (no UPDATE or DELETE policies for regular users)
-- ================================================
-- TEST 6: Check foreign key relationships
-- ================================================
-- Expected: All foreign keys should be properly set up
SELECT
tc.table_name,
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name,
rc.delete_rule
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
AND ccu.table_schema = tc.table_schema
JOIN information_schema.referential_constraints AS rc
ON tc.constraint_name = rc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'public'
ORDER BY tc.table_name, kcu.column_name;
-- Expected output:
-- intake_notes | client_id | clients | id | CASCADE
-- problem_profiles | client_id | clients | id | CASCADE
-- problem_profiles | source_note_id | intake_notes | id | SET NULL
-- treatment_plans | client_id | clients | id | CASCADE
-- ai_events | client_id | clients | id | SET NULL
-- ai_events | note_id | intake_notes | id | SET NULL
-- ================================================
-- PRODUCTION MIGRATION PATH
-- ================================================
-- When moving to production, enhance policies with org_id filtering:
--
-- 1. Add org_id column to all tables:
-- ALTER TABLE clients ADD COLUMN org_id UUID REFERENCES organizations(id);
--
-- 2. Update policies to filter by organization:
-- CREATE POLICY "Users can view own org clients"
-- ON clients
-- FOR SELECT
-- USING (
-- auth.uid() IS NOT NULL AND
-- org_id = (SELECT org_id FROM users WHERE id = auth.uid())
-- );
--
-- 3. Add role-based access:
-- CREATE POLICY "Admins can view all"
-- ON clients
-- FOR SELECT
-- USING (
-- auth.uid() IS NOT NULL AND
-- EXISTS (
-- SELECT 1 FROM users
-- WHERE id = auth.uid() AND role = 'admin'
-- )
-- );
-- ================================================
-- SECURITY NOTES
-- ================================================
-- 1. Current policies are MVP-level: all authenticated users can access all data
-- 2. In production, add org_id filtering for multi-tenancy
-- 3. ai_events table is append-only for regular users (audit trail)
-- 4. Service role can bypass RLS for admin operations
-- 5. All policies use auth.uid() for security
-- 6. Foreign key CASCADE ensures orphaned records are cleaned up

View File

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

View File

@@ -0,0 +1,382 @@
-- WARNING: This schema is for context only and is not meant to be run.
-- Table order and constraints may not be valid for execution.
CREATE TABLE public.ai_events (
id uuid NOT NULL DEFAULT gen_random_uuid(),
kind text NOT NULL CHECK (kind = ANY (ARRAY['summarize'::text, 'readability'::text, 'extract'::text, 'plan'::text])),
client_id uuid,
note_id uuid,
request jsonb NOT NULL DEFAULT '{}'::jsonb,
response jsonb NOT NULL DEFAULT '{}'::jsonb,
duration_ms integer NOT NULL DEFAULT 0 CHECK (duration_ms >= 0),
created_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT ai_events_pkey PRIMARY KEY (id),
CONSTRAINT ai_events_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.clients(id),
CONSTRAINT ai_events_note_id_fkey FOREIGN KEY (note_id) REFERENCES public.intake_notes(id)
);
CREATE TABLE public.anamneses (
id uuid NOT NULL DEFAULT gen_random_uuid(),
intake_id uuid NOT NULL,
anamnese_date date NOT NULL DEFAULT CURRENT_DATE,
anamnese_type text NOT NULL CHECK (anamnese_type = ANY (ARRAY['psychiatrisch'::text, 'sociaal'::text, 'medisch'::text, 'familie'::text, 'ontwikkeling'::text, 'overig'::text])),
content text NOT NULL,
notes text,
created_by uuid,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
CONSTRAINT anamneses_pkey PRIMARY KEY (id),
CONSTRAINT anamneses_intake_id_fkey FOREIGN KEY (intake_id) REFERENCES public.intakes(id),
CONSTRAINT anamneses_created_by_fkey FOREIGN KEY (created_by) REFERENCES public.practitioners(id)
);
CREATE TABLE public.care_plans (
id uuid NOT NULL DEFAULT gen_random_uuid(),
identifier text DEFAULT (gen_random_uuid())::text UNIQUE,
status USER-DEFINED NOT NULL DEFAULT 'draft'::careplan_status,
intent text NOT NULL DEFAULT 'plan'::text,
category_code text DEFAULT 'ggz-behandelplan'::text,
category_display text DEFAULT 'GGZ Behandelplan'::text,
title text NOT NULL,
description text,
patient_id uuid NOT NULL,
encounter_id uuid,
period_start date,
period_end date,
created_date timestamp with time zone DEFAULT now(),
author_id uuid,
contributor_ids ARRAY,
care_team_ids ARRAY,
addresses_condition_ids ARRAY,
goals jsonb DEFAULT '[]'::jsonb,
activities jsonb DEFAULT '[]'::jsonb,
note text,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
based_on_intake_id uuid,
based_on_anamneses ARRAY,
based_on_examinations ARRAY,
based_on_risk_assessments ARRAY,
CONSTRAINT care_plans_pkey PRIMARY KEY (id),
CONSTRAINT care_plans_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id),
CONSTRAINT care_plans_encounter_id_fkey FOREIGN KEY (encounter_id) REFERENCES public.encounters(id),
CONSTRAINT care_plans_author_id_fkey FOREIGN KEY (author_id) REFERENCES public.practitioners(id),
CONSTRAINT care_plans_based_on_intake_id_fkey FOREIGN KEY (based_on_intake_id) REFERENCES public.intakes(id)
);
CREATE TABLE public.clients (
id uuid NOT NULL DEFAULT gen_random_uuid(),
first_name text NOT NULL,
last_name text NOT NULL,
birth_date date NOT NULL,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT clients_pkey PRIMARY KEY (id)
);
CREATE TABLE public.conditions (
id uuid NOT NULL DEFAULT gen_random_uuid(),
identifier text DEFAULT (gen_random_uuid())::text UNIQUE,
clinical_status USER-DEFINED NOT NULL DEFAULT 'active'::condition_clinical_status,
verification_status USER-DEFINED NOT NULL DEFAULT 'provisional'::condition_verification_status,
category text NOT NULL DEFAULT 'encounter-diagnosis'::text,
severity_code text,
severity_display text,
code_system text NOT NULL DEFAULT 'http://hl7.org/fhir/sid/icd-10'::text,
code_code text NOT NULL,
code_display text NOT NULL,
body_site_code text,
body_site_display text,
patient_id uuid NOT NULL,
encounter_id uuid,
onset_datetime timestamp with time zone,
onset_age integer,
abatement_datetime timestamp with time zone,
abatement_age integer,
recorded_date timestamp with time zone NOT NULL DEFAULT now(),
recorder_id uuid,
asserter_id uuid,
note text,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
CONSTRAINT conditions_pkey PRIMARY KEY (id),
CONSTRAINT conditions_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id),
CONSTRAINT conditions_encounter_id_fkey FOREIGN KEY (encounter_id) REFERENCES public.encounters(id),
CONSTRAINT conditions_recorder_id_fkey FOREIGN KEY (recorder_id) REFERENCES public.practitioners(id),
CONSTRAINT conditions_asserter_id_fkey FOREIGN KEY (asserter_id) REFERENCES public.practitioners(id)
);
CREATE TABLE public.demo_users (
id uuid NOT NULL DEFAULT gen_random_uuid(),
user_id uuid UNIQUE,
access_level text NOT NULL DEFAULT 'read_only'::text CHECK (access_level = ANY (ARRAY['read_only'::text, 'interactive'::text, 'presenter'::text])),
expires_at timestamp with time zone DEFAULT (now() + '90 days'::interval),
usage_count integer DEFAULT 0,
last_login_at timestamp with time zone,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
notes text,
CONSTRAINT demo_users_pkey PRIMARY KEY (id),
CONSTRAINT demo_users_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id)
);
CREATE TABLE public.encounters (
id uuid NOT NULL DEFAULT gen_random_uuid(),
identifier text DEFAULT (gen_random_uuid())::text UNIQUE,
status USER-DEFINED NOT NULL DEFAULT 'planned'::encounter_status,
class_code text NOT NULL,
class_display text NOT NULL,
type_code text NOT NULL,
type_display text NOT NULL,
priority_code text,
priority_display text,
patient_id uuid NOT NULL,
practitioner_id uuid,
organization_id uuid,
period_start timestamp with time zone NOT NULL,
period_end timestamp with time zone,
reason_code ARRAY,
reason_display ARRAY,
admission_source text,
discharge_disposition text,
notes text,
intake_note_id uuid,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
intake_id uuid,
CONSTRAINT encounters_pkey PRIMARY KEY (id),
CONSTRAINT encounters_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id),
CONSTRAINT encounters_practitioner_id_fkey FOREIGN KEY (practitioner_id) REFERENCES public.practitioners(id),
CONSTRAINT encounters_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES public.organizations(id),
CONSTRAINT encounters_intake_note_id_fkey FOREIGN KEY (intake_note_id) REFERENCES public.intake_notes(id),
CONSTRAINT encounters_intake_id_fkey FOREIGN KEY (intake_id) REFERENCES public.intakes(id)
);
CREATE TABLE public.examinations (
id uuid NOT NULL DEFAULT gen_random_uuid(),
intake_id uuid NOT NULL,
examination_date date NOT NULL DEFAULT CURRENT_DATE,
examination_type text NOT NULL CHECK (examination_type = ANY (ARRAY['bloedonderzoek'::text, 'neuropsychologisch'::text, 'psychodiagnostiek'::text, 'iq_test'::text, 'persoonlijkheid'::text, 'medisch'::text, 'overig'::text])),
performed_by text,
reason text,
findings text NOT NULL,
document_url text,
notes text,
created_by uuid,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
CONSTRAINT examinations_pkey PRIMARY KEY (id),
CONSTRAINT examinations_intake_id_fkey FOREIGN KEY (intake_id) REFERENCES public.intakes(id),
CONSTRAINT examinations_created_by_fkey FOREIGN KEY (created_by) REFERENCES public.practitioners(id)
);
CREATE TABLE public.intake_notes (
id uuid NOT NULL DEFAULT gen_random_uuid(),
client_id uuid NOT NULL,
title text,
tag text CHECK (tag = ANY (ARRAY['Intake'::text, 'Evaluatie'::text, 'Plan'::text])),
content_json jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (content_json IS NOT NULL),
content_text text,
author uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT intake_notes_pkey PRIMARY KEY (id),
CONSTRAINT intake_notes_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.clients(id)
);
CREATE TABLE public.intakes (
id uuid NOT NULL DEFAULT gen_random_uuid(),
patient_id uuid NOT NULL,
title text NOT NULL,
department text NOT NULL,
psychologist_id uuid,
status text NOT NULL DEFAULT 'bezig'::text CHECK (status = ANY (ARRAY['bezig'::text, 'afgerond'::text])),
start_date date NOT NULL DEFAULT CURRENT_DATE,
end_date date,
notes text,
kindcheck_data jsonb DEFAULT '{}'::jsonb,
treatment_advice jsonb DEFAULT '{}'::jsonb,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
CONSTRAINT intakes_pkey PRIMARY KEY (id),
CONSTRAINT intakes_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id),
CONSTRAINT intakes_psychologist_id_fkey FOREIGN KEY (psychologist_id) REFERENCES public.practitioners(id)
);
CREATE TABLE public.observations (
id uuid NOT NULL DEFAULT gen_random_uuid(),
identifier text DEFAULT (gen_random_uuid())::text UNIQUE,
status USER-DEFINED NOT NULL DEFAULT 'final'::observation_status,
category text NOT NULL,
code_system text NOT NULL,
code_code text NOT NULL,
code_display text NOT NULL,
patient_id uuid NOT NULL,
encounter_id uuid,
effective_datetime timestamp with time zone NOT NULL,
issued timestamp with time zone DEFAULT now(),
performer_id uuid,
value_type text NOT NULL,
value_quantity_value numeric,
value_quantity_unit text,
value_quantity_comparator text,
value_string text,
value_boolean boolean,
value_codeable_concept jsonb,
interpretation_code text,
interpretation_display text,
note text,
body_site text,
method_code text,
method_display text,
reference_range_low numeric,
reference_range_high numeric,
reference_range_text text,
created_at timestamp with time zone DEFAULT now(),
CONSTRAINT observations_pkey PRIMARY KEY (id),
CONSTRAINT observations_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id),
CONSTRAINT observations_encounter_id_fkey FOREIGN KEY (encounter_id) REFERENCES public.encounters(id),
CONSTRAINT observations_performer_id_fkey FOREIGN KEY (performer_id) REFERENCES public.practitioners(id)
);
CREATE TABLE public.organizations (
id uuid NOT NULL DEFAULT gen_random_uuid(),
identifier_agb text UNIQUE,
identifier_kvk text,
name text NOT NULL,
alias ARRAY,
type_code text DEFAULT 'prov'::text,
type_display text DEFAULT 'Healthcare Provider'::text,
telecom_phone text,
telecom_email text,
telecom_website text,
address_line ARRAY,
address_city text,
address_postal_code text,
address_country text DEFAULT 'NL'::text,
active boolean DEFAULT true,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
CONSTRAINT organizations_pkey PRIMARY KEY (id)
);
CREATE TABLE public.patients (
id uuid NOT NULL DEFAULT gen_random_uuid(),
identifier_bsn text DEFAULT '999999990'::text,
identifier_client_number text,
name_family text NOT NULL,
name_given ARRAY NOT NULL,
name_prefix text,
name_use text DEFAULT 'official'::text,
birth_date date NOT NULL,
gender USER-DEFINED NOT NULL,
telecom_phone text,
telecom_email text,
address_line ARRAY,
address_city text,
address_postal_code text,
address_country text DEFAULT 'NL'::text,
insurance_company text,
insurance_number text,
emergency_contact_name text,
emergency_contact_relationship text,
emergency_contact_phone text,
active boolean DEFAULT true,
general_practitioner_name text,
general_practitioner_agb text,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
status USER-DEFINED DEFAULT 'planned'::episode_status,
is_john_doe boolean DEFAULT false,
CONSTRAINT patients_pkey PRIMARY KEY (id)
);
CREATE TABLE public.practitioners (
id uuid NOT NULL DEFAULT gen_random_uuid(),
identifier_big text UNIQUE,
identifier_agb text,
name_prefix text,
name_given ARRAY NOT NULL,
name_family text NOT NULL,
name_suffix text,
qualification ARRAY,
telecom_phone text,
telecom_email text,
active boolean DEFAULT true,
user_id uuid,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
CONSTRAINT practitioners_pkey PRIMARY KEY (id),
CONSTRAINT practitioners_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id)
);
CREATE TABLE public.problem_profiles (
id uuid NOT NULL DEFAULT gen_random_uuid(),
client_id uuid NOT NULL,
category text NOT NULL CHECK (category = ANY (ARRAY['stemming_depressie'::text, 'angst'::text, 'gedrag_impuls'::text, 'middelen_gebruik'::text, 'cognitief'::text, 'context_psychosociaal'::text])),
severity text NOT NULL CHECK (severity = ANY (ARRAY['laag'::text, 'middel'::text, 'hoog'::text])),
remarks text,
source_note_id uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT problem_profiles_pkey PRIMARY KEY (id),
CONSTRAINT problem_profiles_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.clients(id),
CONSTRAINT problem_profiles_source_note_id_fkey FOREIGN KEY (source_note_id) REFERENCES public.intake_notes(id)
);
CREATE TABLE public.risk_assessments (
id uuid NOT NULL DEFAULT gen_random_uuid(),
intake_id uuid NOT NULL,
assessment_date date NOT NULL DEFAULT CURRENT_DATE,
risk_type text NOT NULL CHECK (risk_type = ANY (ARRAY['suicidaliteit'::text, 'agressie'::text, 'zelfverwaarlozing'::text, 'middelenmisbruik'::text, 'verward_gedrag'::text, 'overig'::text])),
risk_level text NOT NULL CHECK (risk_level = ANY (ARRAY['laag'::text, 'gemiddeld'::text, 'hoog'::text, 'zeer_hoog'::text])),
rationale text NOT NULL,
measures text,
evaluation_date date,
notes text,
created_by uuid,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
CONSTRAINT risk_assessments_pkey PRIMARY KEY (id),
CONSTRAINT risk_assessments_intake_id_fkey FOREIGN KEY (intake_id) REFERENCES public.intakes(id),
CONSTRAINT risk_assessments_created_by_fkey FOREIGN KEY (created_by) REFERENCES public.practitioners(id)
);
CREATE TABLE public.screening_activities (
id uuid NOT NULL DEFAULT gen_random_uuid(),
screening_id uuid NOT NULL,
activity_text text NOT NULL,
created_by uuid,
created_by_name text,
created_at timestamp with time zone DEFAULT now(),
CONSTRAINT screening_activities_pkey PRIMARY KEY (id),
CONSTRAINT screening_activities_screening_id_fkey FOREIGN KEY (screening_id) REFERENCES public.screenings(id),
CONSTRAINT screening_activities_created_by_fkey FOREIGN KEY (created_by) REFERENCES public.practitioners(id)
);
CREATE TABLE public.screening_documents (
id uuid NOT NULL DEFAULT gen_random_uuid(),
screening_id uuid NOT NULL,
file_name text NOT NULL,
file_type text,
file_size integer,
file_path text,
document_type text CHECK (document_type = ANY (ARRAY['verwijsbrief'::text, 'verhuisbericht'::text, 'indicatie'::text, 'overig'::text])),
uploaded_by uuid,
uploaded_by_name text,
uploaded_at timestamp with time zone DEFAULT now(),
CONSTRAINT screening_documents_pkey PRIMARY KEY (id),
CONSTRAINT screening_documents_screening_id_fkey FOREIGN KEY (screening_id) REFERENCES public.screenings(id),
CONSTRAINT screening_documents_uploaded_by_fkey FOREIGN KEY (uploaded_by) REFERENCES public.practitioners(id)
);
CREATE TABLE public.screenings (
id uuid NOT NULL DEFAULT gen_random_uuid(),
patient_id uuid NOT NULL,
request_for_help text,
decision text CHECK (decision = ANY (ARRAY['geschikt'::text, 'niet_geschikt'::text])),
decision_department text,
decision_notes text,
decision_date timestamp with time zone,
decision_by uuid,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now(),
CONSTRAINT screenings_pkey PRIMARY KEY (id),
CONSTRAINT screenings_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id),
CONSTRAINT screenings_decision_by_fkey FOREIGN KEY (decision_by) REFERENCES public.practitioners(id)
);
CREATE TABLE public.treatment_plans (
id uuid NOT NULL DEFAULT gen_random_uuid(),
client_id uuid NOT NULL,
version integer NOT NULL DEFAULT 1 CHECK (version > 0),
status text NOT NULL DEFAULT 'concept'::text CHECK (status = ANY (ARRAY['concept'::text, 'gepubliceerd'::text])),
plan jsonb NOT NULL DEFAULT '{"doelen": [], "frequentie": "", "interventies": [], "meetmomenten": []}'::jsonb CHECK (plan ? 'doelen'::text AND plan ? 'interventies'::text AND plan ? 'frequentie'::text AND plan ? 'meetmomenten'::text),
created_by uuid,
created_at timestamp with time zone NOT NULL DEFAULT now(),
published_at timestamp with time zone,
updated_at timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT treatment_plans_pkey PRIMARY KEY (id),
CONSTRAINT treatment_plans_client_id_fkey FOREIGN KEY (client_id) REFERENCES public.clients(id)
);

View File

@@ -0,0 +1,29 @@
# Archived Migrations & Schema Snapshots
Deze directory bevat migrations en schema snapshots die niet meer actief gebruikt worden als migrations, maar behouden blijven voor referentie.
## Schema Snapshots
Deze bestanden zijn **niet bedoeld om uit te voeren** als migrations, maar dienen als documentatie van het huidige database schema:
- **`20251122-current-db-scheme.sql`** - Snapshot van database schema (2025-11-22)
- **`20251122-supabase-scheme.sql`** - Actueel database schema snapshot (2025-11-22)
⚠️ **Waarschuwing:** Deze bestanden bevatten een WARNING dat ze niet uitgevoerd moeten worden. Ze zijn alleen voor documentatie/referentie doeleinden.
## Test Files
- **`20241115000003_test_rls_policies.sql`** - Test queries voor RLS policies verificatie (niet een echte migration)
## Waarom gearchiveerd?
Deze bestanden zijn gearchiveerd omdat:
1. Ze geen echte migrations zijn (snapshots/test files)
2. Ze niet uitgevoerd moeten worden door Supabase CLI
3. Ze alleen voor documentatie/referentie dienen
4. Ze de migrations directory vervuilen
## Actieve Migrations
Actieve migrations staan in `/supabase/migrations/` en worden uitgevoerd door Supabase CLI.

File diff suppressed because it is too large Load Diff

View File

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

BIN
docs/fonts/Lora-Bold.woff Normal file

Binary file not shown.

BIN
docs/fonts/Lora-Bold.woff2 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
docs/fonts/Lora-Italic.woff Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
docs/fonts/Lora-Medium.woff Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More