55 KiB
🚀 Bouwplan Mini-EPD — Pragmatische FHIR Aanpak
💡 Focus: Data-uitwisselbaarheid met MedMIJ/FHIR bouwstenen voor prototype
Dit bouwplan beschrijft een pragmatische implementatie van FHIR resources gericht op interoperabiliteit en API-based data-uitwisseling. Niet alle 13 FHIR resources worden geïmplementeerd—alleen wat nodig is voor een werkend, uitwisselbaar prototype.
Projectnaam: Mini-EPD Prototype (Pragmatic FHIR Edition) Versie: v2.0 (Pragmatisch) Datum: 21 november 2024 Auteur: Colin Lit (ikbenlit.nl)
📊 Voortgang
Voltooide Epics: 2 van 7 (29%) Voltooide Story Points: 34 van 117 (29%) Status: ✅ Epic 1 & 2 Gereed - FHIR Foundation Complete!
| Epic | Status | Voltooiingsdatum |
|---|---|---|
| E0 - Setup & Config | ✅ Gereed | Pre-project |
| E1 - FHIR Core Schema | ✅ Gereed | 21 november 2024 |
| E2 - Patients & Practitioners | ✅ Gereed | 21 november 2024 |
| E3 - Encounters (Intake) | ⏳ To Do | - |
| E4 - Conditions (DSM-5) | ⏳ To Do | - |
| E5 - CarePlans (Treatment) 🎯 | ⏳ To Do | - |
| E6 - Observations (ROM) | ⏳ To Do | - |
| E7 - API Polish & Demo | ⏳ To Do | - |
Huidige Sprint: Epic 3 - Encounters (Intake)
1. Doel en Context
🎯 Primair Doel: Een werkend EPD-prototype bouwen waarbij behandelplannen en klinische data uitwisselbaar zijn via FHIR-compliant API's. Het systeem moet data kunnen exporteren én importeren in standaard FHIR JSON formaat.
📘 Context: Dit is een pragmatisch prototype met focus op:
- ✅ Data-uitwisselbaarheid: Andere systemen kunnen jouw data lezen/schrijven
- ✅ MedMIJ/FHIR bouwstenen: Basis voor toekomstige certificering
- ✅ API-first: Behandelplannen via
GET /api/fhir/CarePlan/{id} - ⚠️ Goed genoeg AVG: Niet 100% compliant, maar verantwoord voor prototype
- ⚠️ Geen complete features: Focus op kern, rest komt later
Wat dit NIET is:
- ❌ Volledig production-ready EPD
- ❌ 100% MedMIJ-gecertificeerd
- ❌ Compleet consent management systeem
- ❌ Multi-tenant SaaS platform
Referentie documenten:
docs/datamodel-documentatie.md- FHIR uitlegdocs/archive/schemas/20241121_fhir_ggz_schema.sql- Volledig schema (archived, gebruiken we deels)
2. Uitgangspunten
2.1 Technische Stack
Frontend:
- Framework: Next.js 15 (App Router)
- Styling: Tailwind CSS
- UI Components: shadcn/ui + Lucide Icons
- Forms: React Hook Form + Zod validation
- FHIR Utilities: Custom hooks voor FHIR transformaties
Backend:
- Database: Supabase (PostgreSQL) ✅ Al actief
- Auth: Supabase Auth ✅ Al actief (20 users)
- API: Next.js API Routes (FHIR-compliant endpoints)
- FHIR Validation: @hapi/fhir (optioneel, voor strikte validatie)
FHIR Implementation:
- FHIR Version: R4
- Resources: Patient, Practitioner, Encounter, Condition, Observation, CarePlan
- Format: application/fhir+json
- API Style: RESTful (geen GraphQL voor FHIR endpoints)
Development & Deployment:
- Package Manager: pnpm
- TypeScript: Strict mode enabled
- Database Migrations: Supabase migrations (versioned)
- Hosting: Vercel ✅ Waarschijnlijk al actief
- Database Hosting: Supabase Cloud ✅ Actief
2.2 Projectkaders
Tijd:
- Fase 1 (Core FHIR): 4 weken
- Fase 2 (CarePlan API): 2 weken
- Fase 3 (Polish + Demo): 2 weken
- Totaal: 8 weken voor werkend prototype
Team:
- 1 Full-stack developer (jij)
- GGZ-consultant (als sparringpartner)
Scope Pragmatisch Prototype:
✅ WEL Implementeren:
- FHIR Resources: Patient, Practitioner, Encounter, Condition, Observation, CarePlan
- CRUD UI voor alle bovenstaande resources
- RESTful FHIR API endpoints voor data export/import
- Migratie van huidige
clients→patients,treatment_plans→care_plans - Basis ROM-metingen (PHQ-9, GAD-7)
- DSM-5 diagnose registratie
- Demo data seeding
❌ NIET Implementeren (Later/Out of Scope):
- MedicationStatements (medicatie tracking)
- Consents (AVG consent management)
- Flags (safety waarschuwingen)
- DocumentReferences (documenten/verslagen)
- Goals als aparte tabel (embedded in CarePlan JSON)
- Activities als aparte tabel (embedded in CarePlan JSON)
- BSN encryptie (gebruik placeholder BSN voor demo)
- Multi-tenancy (1 organisatie hardcoded)
- MedMIJ certificering (wel compatible datastructuur)
- OAuth2/SMART-on-FHIR (simpele bearer token auth)
Data:
- Demo/fictieve data (geen productie)
- BSN placeholders (geen echte BSN's)
- Privacy by design maar geen volledige AVG audit
2.3 Programmeer Uitgangspunten
FHIR-Specific Principles:
-
FHIR Compliance > Perfectie
- Volg FHIR R4 spec waar relevant
- Pragmatisch bij optionele velden
- Documenteer deviaties in comments
-
API-First Development
- Elke resource MOET via API beschikbaar zijn
- FHIR JSON als primaire output format
- Database structure volgt FHIR resource definitie
-
Hybrid Approach
- Behoud bestaande tabellen waar mogelijk (
clients,intake_notes,ai_events) - Voeg FHIR tabellen toe waar nodig (
patients,care_plans, etc.) - Migreer data incrementeel (geen "big bang")
- Behoud bestaande tabellen waar mogelijk (
Code Quality Principles:
-
DRY (Don't Repeat Yourself)
- Herbruikbare FHIR transform functies
- Centrale FHIR type definitions
- Shared validation schemas (Zod + FHIR)
-
KISS (Keep It Simple, Stupid)
- Eenvoudige FHIR mapping (geen complexe HL7v2 conversies)
- Embedded JSON voor goals/activities (geen aparte tabellen)
- Straightforward API endpoints (geen HATEOAS links voor MVP)
-
SOC (Separation of Concerns)
/lib/fhir/- FHIR transformaties en validators/lib/db/- Database queries/app/api/fhir/- FHIR API endpoints/components/- UI componenten
Development Practices:
-
Code Organization
/app /(dashboard) # Protected routes /patients /encounters /care-plans # Treatment planning /api /fhir # FHIR endpoints /Patient /CarePlan /Condition /Observation /lib /fhir # FHIR utilities /transforms # DB → FHIR, FHIR → DB /validators # FHIR validation /types # FHIR TypeScript types /db # Database helpers -
FHIR Transformation Pattern
// Database → FHIR JSON export function dbCarePlanToFHIR(dbRow: CarePlanRow): FHIRCarePlan { return { resourceType: "CarePlan", id: dbRow.id, status: dbRow.status, intent: dbRow.intent, subject: { reference: `Patient/${dbRow.patient_id}`, display: dbRow.patient_name }, // ... mapping logic }; } // FHIR JSON → Database export function fhirCarePlanToDB(fhir: FHIRCarePlan): CarePlanInsert { return { id: fhir.id, status: fhir.status, patient_id: extractIdFromReference(fhir.subject.reference), // ... mapping logic }; } -
API Response Format
// All FHIR endpoints return application/fhir+json return Response.json(fhirResource, { headers: { 'Content-Type': 'application/fhir+json', 'X-FHIR-Version': '4.0.1' } }); -
Error Handling
- FHIR OperationOutcome voor API errors
- User-friendly messages in UI
- Structured logging voor debugging
-
Security
- RLS policies per FHIR resource
- Bearer token auth voor API (simpel, geen OAuth2)
- Input validation met Zod + FHIR schema validation
3. Epics & Stories Overzicht
🎯 Pragmatische implementatie: 8 epics, 32 stories, ~120 story points
| Epic ID | Titel | Doel | Status | Stories | Story Points | Weken |
|---|---|---|---|---|---|---|
| E0 | Setup & Config ✅ | Next.js, Supabase (done) | ✅ Gereed | 5 | 10 | 0 |
| E1 | FHIR Core Schema ✅ | 6 FHIR tabellen + migratie | ✅ Gereed | 5 | 21 | 1.5 |
| E2 | Patients & Practitioners ✅ | FHIR Patient/Practitioner CRUD + API | ✅ Gereed | 4 | 13 | 1 |
| E3 | Encounters (Intake) | Contactmoment registratie + API | ⏳ To Do | 4 | 13 | 1 |
| E4 | Conditions (DSM-5) | Diagnose registratie + API | ⏳ To Do | 4 | 13 | 1 |
| E5 | CarePlans (Treatment) 🎯 | Behandelplan CRUD + FHIR API | ⏳ To Do | 5 | 21 | 2 |
| E6 | Observations (ROM) | ROM-metingen + API | ⏳ To Do | 4 | 13 | 1 |
| E7 | API Polish & Demo | Swagger docs, demo scenario, testing | ⏳ To Do | 4 | 13 | 0.5 |
Totaal: 35 stories, ~117 story points, 8 weken @ 15 points/week
4. Epics & Stories (Uitwerking)
Epic 0 — Setup & Configuratie ✅
Status: GEREED (al gedaan in huidige setup)
Wat is al actief:
- ✅ Next.js project
- ✅ Supabase project + connection
- ✅ Supabase Auth (20 users)
- ✅ RLS enabled op tabellen
- ✅ Demo users systeem
- ✅ Basis tabellen: clients, intake_notes, treatment_plans, ai_events
Geen actie vereist - ga door naar Epic 1
Epic 1 — FHIR Core Schema & Migratie ✅
Epic Doel: FHIR-compliant database tabellen toevoegen en bestaande data migreren. Status: GEREED (21 november 2024)
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | Story Points |
|---|---|---|---|---|---|
| E1.S1 | Pragmatisch FHIR schema maken | Nieuw schema: 20241121_pragmatic_fhir_schema.sql met 6 resources + embedded goals/activities |
✅ Gereed | E0 | 5 |
| E1.S2 | Migratie toepassen | SQL uitvoeren in Supabase, verify tabellen | ✅ Gereed | E1.S1 | 3 |
| E1.S3 | TypeScript types genereren | supabase gen types → /types/database.ts |
✅ Gereed | E1.S2 | 2 |
| E1.S4 | Data migratie script | clients → patients, treatment_plans → care_plans |
✅ Gereed | E1.S3 | 8 |
| E1.S5 | Seed data FHIR | Demo practitioners, organizations, FHIR patients | ✅ Gereed | E1.S4 | 3 |
Technical Notes:
E1.S1: Nieuw Schema Bestand Maken
Het originele schema docs/archive/schemas/20241121_fhir_ggz_schema.sql bevat alle 13 FHIR resources. Voor de pragmatische aanpak maken we een nieuw schema bestand: docs/archive/schemas/20241121_pragmatic_fhir_schema.sql (beide archived)
Verschillen met origineel schema:
| Aspect | Origineel Schema | Pragmatisch Schema |
|---|---|---|
| Resources | 13 tabellen | 7 tabellen (6 FHIR + 1 org) |
| Goals | Aparte goals tabel |
Embedded in care_plans.goals JSONB |
| Activities | Aparte care_plan_activities tabel |
Embedded in care_plans.activities JSONB |
| Medications | medication_statements tabel |
❌ Niet geïmplementeerd |
| Consents | consents tabel |
❌ Niet geïmplementeerd |
| Flags | flags tabel |
❌ Niet geïmplementeerd |
| Documents | document_references tabel |
❌ Niet geïmplementeerd |
| BSN encryptie | pgcrypto encryptie |
Placeholder BSN (demo) |
FHIR Resources Implementeren (7 tabellen):
-- Core 6 FHIR resources + 1 organization
CREATE TABLE practitioners (...); -- FHIR Practitioner
CREATE TABLE organizations (...); -- FHIR Organization (1 default)
CREATE TABLE patients (...); -- FHIR Patient (BSN placeholder)
CREATE TABLE encounters (...); -- FHIR Encounter
CREATE TABLE conditions (...); -- FHIR Condition (DSM-5)
CREATE TABLE observations (...); -- FHIR Observation (ROM)
CREATE TABLE care_plans (...); -- FHIR CarePlan (goals/activities embedded!)
-- Behouden:
✅ clients (legacy, read-only na migratie)
✅ intake_notes (blijft bestaan, later DocumentReference)
✅ treatment_plans (legacy, read-only na migratie)
✅ ai_events (blijft bestaan)
✅ demo_users (blijft bestaan)
Belangrijk verschil: care_plans tabel
-- Pragmatisch: Embedded goals en activities
CREATE TABLE care_plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- ... andere velden ...
-- PRAGMATISCH: JSONB embedded ipv aparte tabellen
goals JSONB DEFAULT '[]'::jsonb,
activities JSONB DEFAULT '[]'::jsonb,
-- Goals structure: [{description, target}, ...]
-- Activities structure: [{detail: {code, status, scheduledTiming}}, ...]
);
-- VS Origineel: Aparte tabellen
-- CREATE TABLE goals (...); -- ❌ Niet in pragmatisch schema
-- CREATE TABLE care_plan_activities (...); -- ❌ Niet in pragmatisch schema
Migratie Strategie:
// scripts/migrate-to-fhir.ts
async function migrateClientsToPatients() {
const clients = await supabase.from('clients').select('*');
for (const client of clients.data) {
await supabase.from('patients').insert({
id: client.id, // Behoud UUID voor referential integrity
identifier_bsn: '999999990', // Placeholder BSN
identifier_client_number: client.id,
name_family: client.last_name,
name_given: [client.first_name],
birth_date: client.birth_date,
gender: 'unknown',
active: true,
created_at: client.created_at,
updated_at: client.updated_at
});
}
console.log(`✅ Migrated ${clients.data.length} clients → patients`);
}
async function migrateTreatmentPlansToCarePlans() {
const plans = await supabase
.from('treatment_plans')
.select('*, clients(first_name, last_name)');
for (const plan of plans.data) {
const goals = plan.plan.doelen?.map(doel => ({
description: { text: doel },
// FHIR Goal structure embedded
})) || [];
const activities = plan.plan.interventies?.map(interventie => ({
detail: {
code: { text: interventie },
status: 'not-started',
// FHIR Activity structure embedded
}
})) || [];
await supabase.from('care_plans').insert({
id: plan.id,
patient_id: plan.client_id,
status: plan.status === 'gepubliceerd' ? 'active' : 'draft',
intent: 'plan',
title: `Behandelplan v${plan.version}`,
category_code: 'ggz-behandelplan',
category_display: 'GGZ Behandelplan',
goals: goals, // JSONB embedded
activities: activities, // JSONB embedded (pragmatisch!)
period_start: plan.created_at,
created_date: plan.created_at,
created_by: plan.created_by
});
}
console.log(`✅ Migrated ${plans.data.length} treatment_plans → care_plans`);
}
Acceptance:
- ✅ 6 FHIR tabellen aanwezig in Supabase
- ✅ TypeScript types gegenereerd
- ✅ Bestaande data gemigreerd (clients → patients, treatment_plans → care_plans)
- ✅ Legacy tabellen read-only (geen DELETE policies)
- ✅ Seed data met 3 demo patients, 2 practitioners, 1 organization
FHIR Schema Details:
-- Simplified: Goals en Activities embedded in JSONB
CREATE TABLE care_plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
identifier TEXT UNIQUE DEFAULT gen_random_uuid()::TEXT,
status careplan_status NOT NULL DEFAULT 'draft',
intent TEXT NOT NULL DEFAULT 'plan',
title TEXT NOT NULL,
description TEXT,
patient_id UUID REFERENCES patients(id) NOT NULL,
encounter_id UUID REFERENCES encounters(id),
author_id UUID REFERENCES practitioners(id),
period_start DATE,
period_end DATE,
category_code TEXT DEFAULT 'ggz-behandelplan',
category_display TEXT DEFAULT 'GGZ Behandelplan',
-- PRAGMATISCH: Embedded JSON ipv aparte tabellen
goals JSONB DEFAULT '[]'::jsonb, -- Array van FHIR Goal structures
activities JSONB DEFAULT '[]'::jsonb, -- Array van FHIR Activity structures
addresses_condition_ids UUID[], -- Welke diagnoses behandeld
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON COLUMN care_plans.goals IS 'FHIR Goal structures embedded as JSONB array';
COMMENT ON COLUMN care_plans.activities IS 'FHIR CarePlan.activity structures embedded as JSONB array';
Epic 2 — Patients & Practitioners ✅
Epic Doel: FHIR Patient en Practitioner CRUD + API endpoints. Status: GEREED (21 november 2024)
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | Story Points |
|---|---|---|---|---|---|
| E2.S1 | FHIR transforms library | DB → FHIR, FHIR → DB functies | ✅ Gereed | E1.S5 | 5 |
| E2.S2 | Patient API endpoints | GET/POST/PUT /api/fhir/Patient |
✅ Gereed | E2.S1 | 3 |
| E2.S3 | Practitioner API endpoints | GET/POST /api/fhir/Practitioner |
✅ Gereed | E2.S1 | 2 |
| E2.S4 | Patient UI (CRUD) | /epd/patients lijst + detail + forms |
✅ Gereed | E2.S2 | 3 |
Technical Notes:
FHIR Transform Library:
// lib/fhir/transforms/patient.ts
import type { FHIRPatient, Database } from '@/types';
type PatientRow = Database['public']['Tables']['patients']['Row'];
export function dbPatientToFHIR(row: PatientRow): FHIRPatient {
return {
resourceType: "Patient",
id: row.id,
identifier: [
{
system: "http://fhir.nl/fhir/NamingSystem/bsn",
value: row.identifier_bsn
},
{
system: "urn:oid:2.16.840.1.113883.2.4.3.11.999.7.6",
value: row.identifier_client_number || row.id
}
],
name: [{
use: "official",
family: row.name_family,
given: row.name_given,
prefix: row.name_prefix ? [row.name_prefix] : undefined
}],
birthDate: row.birth_date,
gender: row.gender as "male" | "female" | "other" | "unknown",
telecom: [
row.telecom_phone && {
system: "phone",
value: row.telecom_phone,
use: "mobile"
},
row.telecom_email && {
system: "email",
value: row.telecom_email
}
].filter(Boolean),
address: row.address_line ? [{
use: "home",
line: row.address_line,
city: row.address_city,
postalCode: row.address_postal_code,
country: row.address_country || "NL"
}] : undefined,
active: row.active,
meta: {
lastUpdated: row.updated_at
}
};
}
export function fhirPatientToDB(fhir: FHIRPatient): Partial<PatientRow> {
const bsn = fhir.identifier?.find(i =>
i.system === "http://fhir.nl/fhir/NamingSystem/bsn"
)?.value;
const name = fhir.name?.[0];
const address = fhir.address?.[0];
const phone = fhir.telecom?.find(t => t.system === "phone")?.value;
const email = fhir.telecom?.find(t => t.system === "email")?.value;
return {
id: fhir.id,
identifier_bsn: bsn || '999999990',
name_family: name?.family || '',
name_given: name?.given || [],
name_prefix: name?.prefix?.[0],
birth_date: fhir.birthDate,
gender: fhir.gender || 'unknown',
telecom_phone: phone,
telecom_email: email,
address_line: address?.line,
address_city: address?.city,
address_postal_code: address?.postalCode,
address_country: address?.country || 'NL',
active: fhir.active ?? true
};
}
API Endpoints:
// app/api/fhir/Patient/route.ts
import { dbPatientToFHIR, fhirPatientToDB } from '@/lib/fhir/transforms/patient';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get('_id');
let query = supabase.from('patients').select('*');
if (id) {
query = query.eq('id', id).single();
const { data, error } = await query;
if (error || !data) {
return Response.json({
resourceType: "OperationOutcome",
issue: [{
severity: "error",
code: "not-found",
diagnostics: "Patient not found"
}]
}, { status: 404 });
}
return Response.json(dbPatientToFHIR(data), {
headers: { 'Content-Type': 'application/fhir+json' }
});
}
// Search all patients
const { data, error } = await query;
return Response.json({
resourceType: "Bundle",
type: "searchset",
total: data?.length || 0,
entry: data?.map(row => ({
resource: dbPatientToFHIR(row)
})) || []
}, {
headers: { 'Content-Type': 'application/fhir+json' }
});
}
export async function POST(request: Request) {
const fhirPatient = await request.json();
const dbPatient = fhirPatientToDB(fhirPatient);
const { data, error } = await supabase
.from('patients')
.insert(dbPatient)
.select()
.single();
if (error) {
return Response.json({
resourceType: "OperationOutcome",
issue: [{
severity: "error",
code: "processing",
diagnostics: error.message
}]
}, { status: 400 });
}
return Response.json(dbPatientToFHIR(data), {
status: 201,
headers: {
'Content-Type': 'application/fhir+json',
'Location': `/api/fhir/Patient/${data.id}`
}
});
}
Acceptance: ✅ VOLTOOID
- ✅
/api/fhir/PatientGET/POST/PUT werkend (app/api/fhir/Patient/route.ts,app/api/fhir/Patient/[id]/route.ts) - ✅
/api/fhir/PractitionerGET/POST werkend (app/api/fhir/Practitioner/route.ts,app/api/fhir/Practitioner/[id]/route.ts) - ✅ FHIR JSON output correct volgens spec (met
Content-Type: application/fhir+json) - ✅ Patient lijst UI toont alle patients (
/epd/patients) - ✅ Patient detail pagina toont FHIR data (
/epd/patients/[id]) - ✅ Patient create/edit forms werken (
/epd/patients/new,/epd/patients/[id]) - ✅ FHIR transforms library compleet (
lib/fhir/transforms/patient.ts,lib/fhir/transforms/practitioner.ts) - ✅ TypeScript compilatie succesvol zonder errors
Epic 3 — Encounters (Intake)
Epic Doel: Contactmoment registratie met FHIR Encounter resource.
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | Story Points |
|---|---|---|---|---|---|
| E3.S1 | Encounter FHIR transforms | DB ↔ FHIR functies | ⏳ | E2.S4 | 3 |
| E3.S2 | Encounter API endpoints | GET/POST/PUT /api/fhir/Encounter |
⏳ | E3.S1 | 3 |
| E3.S3 | Encounter tijdlijn UI | /patients/[id]/encounters chronologisch |
⏳ | E3.S2 | 4 |
| E3.S4 | Nieuw encounter formulier | Create intake/behandeling/follow-up | ⏳ | E3.S3 | 3 |
Technical Notes:
Encounter Types (GGZ):
const encounterTypes = [
{ code: 'intake', display: 'Intakegesprek' },
{ code: 'diagnostiek', display: 'Diagnostisch onderzoek' },
{ code: 'behandeling', display: 'Behandelsessie' },
{ code: 'follow-up', display: 'Follow-up gesprek' },
{ code: 'crisis', display: 'Crisisinterventie' }
];
const encounterClass = [
{ code: 'AMB', display: 'Ambulatory (polikliniek)' },
{ code: 'IMP', display: 'Inpatient (kliniek)' },
{ code: 'VR', display: 'Virtual (online)' }
];
Encounter → Intake Notes Koppeling:
// Behoud intake_notes, link naar encounter
async function linkIntakeNoteToEncounter(noteId: string, encounterId: string) {
await supabase
.from('intake_notes')
.update({
encounter_id: encounterId, // Add column via migration
tag: 'Intake'
})
.eq('id', noteId);
}
Acceptance:
- ✅
/api/fhir/EncounterCRUD werkend - ✅ Timeline toont encounters per patient
- ✅ Intake notes gekoppeld aan encounters
- ✅ Status flow: planned → in-progress → completed
- ✅ Encounter detail pagina met notes
Epic 4 — Conditions (DSM-5)
Epic Doel: Diagnose registratie met FHIR Condition resource.
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | Story Points |
|---|---|---|---|---|---|
| E4.S1 | DSM-5 codes seed | Top 50 GGZ diagnoses in database | ⏳ | E1.S5 | 2 |
| E4.S2 | Condition FHIR transforms | DB ↔ FHIR functies | ⏳ | E3.S4 | 3 |
| E4.S3 | Condition API endpoints | GET/POST/PUT /api/fhir/Condition |
⏳ | E4.S2 | 3 |
| E4.S4 | Diagnose UI | Problemlijst + diagnose toevoegen form | ⏳ | E4.S3 | 5 |
Technical Notes:
DSM-5 Seed Data:
// scripts/seed-dsm5-codes.ts
const dsm5Codes = [
{
code: 'F32.2',
display: 'Depressieve episode, ernstig zonder psychotische kenmerken',
system: 'http://hl7.org/fhir/sid/icd-10',
category: 'stemming'
},
{
code: 'F41.1',
display: 'Gegeneraliseerde angststoornis',
system: 'http://hl7.org/fhir/sid/icd-10',
category: 'angst'
},
{
code: 'F60.31',
display: 'Borderline persoonlijkheidsstoornis',
system: 'http://hl7.org/fhir/sid/icd-10',
category: 'persoonlijkheid'
},
{
code: 'F20.0',
display: 'Paranoïde schizofrenie',
system: 'http://hl7.org/fhir/sid/icd-10',
category: 'psychotisch'
},
{
code: 'F84.0',
display: 'Autismespectrumstoornis',
system: 'http://hl7.org/fhir/sid/icd-10',
category: 'ontwikkeling'
}
// ... 45 more
];
Condition FHIR Example:
{
"resourceType": "Condition",
"id": "condition-123",
"clinicalStatus": {
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
"code": "active"
}]
},
"verificationStatus": {
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/condition-ver-status",
"code": "confirmed"
}]
},
"category": [{
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/condition-category",
"code": "encounter-diagnosis"
}]
}],
"severity": {
"coding": [{
"system": "http://snomed.info/sct",
"code": "24484000",
"display": "Severe"
}]
},
"code": {
"coding": [{
"system": "http://hl7.org/fhir/sid/icd-10",
"code": "F32.2",
"display": "Depressieve episode, ernstig"
}]
},
"subject": {
"reference": "Patient/patient-456"
},
"encounter": {
"reference": "Encounter/encounter-789"
},
"onsetDateTime": "2024-01-15",
"recordedDate": "2024-01-15T14:30:00Z",
"recorder": {
"reference": "Practitioner/practitioner-1"
}
}
Acceptance:
- ✅ DSM-5 codes in database (lookup tabel of JSONB)
- ✅
/api/fhir/ConditionCRUD werkend - ✅ Problemlijst toont active diagnoses
- ✅ Diagnose toevoegen met DSM-5 autocomplete
- ✅ Status updates: active → remission → resolved
Epic 5 — CarePlans (Treatment Plans) 🎯
Epic Doel: FHIR-compliant behandelplannen met volledige CRUD en API export/import.
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | Story Points |
|---|---|---|---|---|---|
| E5.S1 | CarePlan FHIR transforms | DB ↔ FHIR functies (incl. goals/activities) | ⏳ | E4.S4 | 5 |
| E5.S2 | CarePlan API endpoints | GET/POST/PUT /api/fhir/CarePlan |
⏳ | E5.S1 | 5 |
| E5.S3 | CarePlan wizard UI | Multi-step: diagnoses → doelen → interventies | ⏳ | E5.S2 | 8 |
| E5.S4 | CarePlan detail pagina | Overzicht + voortgang + edit | ⏳ | E5.S3 | 2 |
| E5.S5 | API testen & validatie | Import/export test met externe FHIR tool | ⏳ | E5.S4 | 1 |
Technical Notes:
CarePlan FHIR Transform (Uitgebreid):
// lib/fhir/transforms/careplan.ts
import type { FHIRCarePlan, Database } from '@/types';
type CarePlanRow = Database['public']['Tables']['care_plans']['Row'] & {
patient: Database['public']['Tables']['patients']['Row'];
author: Database['public']['Tables']['practitioners']['Row'];
conditions: Database['public']['Tables']['conditions']['Row'][];
};
export function dbCarePlanToFHIR(row: CarePlanRow): FHIRCarePlan {
return {
resourceType: "CarePlan",
id: row.id,
identifier: [{
system: "urn:oid:2.16.840.1.113883.2.4.3.11.999.7.6",
value: row.identifier
}],
status: row.status as "draft" | "active" | "on-hold" | "revoked" | "completed",
intent: row.intent as "proposal" | "plan" | "order",
category: [{
coding: [{
system: "http://hl7.org/fhir/care-plan-category",
code: row.category_code || "ggz-behandelplan",
display: row.category_display || "GGZ Behandelplan"
}]
}],
title: row.title,
description: row.description,
subject: {
reference: `Patient/${row.patient_id}`,
display: `${row.patient.name_given.join(' ')} ${row.patient.name_family}`
},
encounter: row.encounter_id ? {
reference: `Encounter/${row.encounter_id}`
} : undefined,
period: {
start: row.period_start,
end: row.period_end
},
created: row.created_date,
author: row.author_id ? {
reference: `Practitioner/${row.author_id}`,
display: `${row.author.name_given.join(' ')} ${row.author.name_family}`
} : undefined,
addresses: row.addresses_condition_ids?.map(conditionId => ({
reference: `Condition/${conditionId}`
})) || [],
// GOALS: Embedded JSONB → FHIR Goal array
goal: (row.goals as any[])?.map((goal: any) => ({
description: {
text: goal.description || goal.text
},
target: goal.target ? [{
measure: goal.target.measure,
detailQuantity: goal.target.detailQuantity,
dueDate: goal.target.dueDate
}] : undefined
})) || [],
// ACTIVITIES: Embedded JSONB → FHIR Activity array
activity: (row.activities as any[])?.map((activity: any) => ({
detail: {
code: {
text: activity.code?.text || activity.description
},
status: activity.status || "not-started",
scheduledTiming: activity.scheduledTiming,
performer: activity.performer ? [{
reference: `Practitioner/${activity.performer}`
}] : undefined,
description: activity.description,
location: activity.location
}
})) || [],
meta: {
lastUpdated: row.updated_at
}
};
}
export function fhirCarePlanToDB(fhir: FHIRCarePlan): Partial<CarePlanRow> {
// Extract patient ID from reference
const patientId = fhir.subject?.reference?.replace('Patient/', '');
const authorId = fhir.author?.reference?.replace('Practitioner/', '');
const encounterId = fhir.encounter?.reference?.replace('Encounter/', '');
// Extract condition IDs
const conditionIds = fhir.addresses?.map(addr =>
addr.reference?.replace('Condition/', '')
).filter(Boolean) as string[];
// Convert FHIR goals to JSONB
const goals = fhir.goal?.map(goal => ({
description: goal.description?.text,
target: goal.target?.[0] ? {
measure: goal.target[0].measure,
detailQuantity: goal.target[0].detailQuantity,
dueDate: goal.target[0].dueDate
} : undefined
}));
// Convert FHIR activities to JSONB
const activities = fhir.activity?.map(activity => ({
code: {
text: activity.detail?.code?.text
},
status: activity.detail?.status || 'not-started',
scheduledTiming: activity.detail?.scheduledTiming,
performer: activity.detail?.performer?.[0]?.reference?.replace('Practitioner/', ''),
description: activity.detail?.description,
location: activity.detail?.location
}));
return {
id: fhir.id,
status: fhir.status || 'draft',
intent: fhir.intent || 'plan',
title: fhir.title || '',
description: fhir.description,
patient_id: patientId,
encounter_id: encounterId,
author_id: authorId,
period_start: fhir.period?.start,
period_end: fhir.period?.end,
category_code: fhir.category?.[0]?.coding?.[0]?.code || 'ggz-behandelplan',
category_display: fhir.category?.[0]?.coding?.[0]?.display || 'GGZ Behandelplan',
addresses_condition_ids: conditionIds,
goals: goals as any, // JSONB
activities: activities as any, // JSONB
created_date: fhir.created
};
}
API Endpoint met Join:
// app/api/fhir/CarePlan/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const { data, error } = await supabase
.from('care_plans')
.select(`
*,
patient:patients(*),
author:practitioners(*),
conditions:conditions(*)
`)
.eq('id', params.id)
.single();
if (error || !data) {
return Response.json({
resourceType: "OperationOutcome",
issue: [{
severity: "error",
code: "not-found",
diagnostics: "CarePlan not found"
}]
}, { status: 404 });
}
const fhirCarePlan = dbCarePlanToFHIR(data);
return Response.json(fhirCarePlan, {
headers: {
'Content-Type': 'application/fhir+json',
'X-FHIR-Version': '4.0.1'
}
});
}
// POST: Import FHIR CarePlan
export async function POST(request: Request) {
const fhirCarePlan = await request.json() as FHIRCarePlan;
// Validate FHIR structure (basic)
if (fhirCarePlan.resourceType !== 'CarePlan') {
return Response.json({
resourceType: "OperationOutcome",
issue: [{
severity: "error",
code: "invalid",
diagnostics: "resourceType must be 'CarePlan'"
}]
}, { status: 400 });
}
const dbCarePlan = fhirCarePlanToDB(fhirCarePlan);
const { data, error } = await supabase
.from('care_plans')
.insert(dbCarePlan)
.select()
.single();
if (error) {
return Response.json({
resourceType: "OperationOutcome",
issue: [{
severity: "error",
code: "processing",
diagnostics: error.message
}]
}, { status: 400 });
}
return Response.json(dbCarePlanToFHIR(data), {
status: 201,
headers: {
'Content-Type': 'application/fhir+json',
'Location': `/api/fhir/CarePlan/${data.id}`
}
});
}
UI Wizard:
// components/features/care-plans/CarePlanWizard.tsx
const steps = [
{
title: 'Diagnoses selecteren',
description: 'Welke aandoeningen worden behandeld?',
component: ConditionSelector
},
{
title: 'Doelen formuleren',
description: 'Wat willen we bereiken? (SMART)',
component: GoalsEditor
},
{
title: 'Interventies plannen',
description: 'Welke behandelactiviteiten?',
component: ActivitiesEditor
},
{
title: 'Review & Opslaan',
description: 'Controleer en publiceer behandelplan',
component: CarePlanReview
}
];
Acceptance:
- ✅
/api/fhir/CarePlanGET/POST/PUT werkend - ✅ FHIR JSON import/export werkt correct
- ✅ Wizard UI compleet doorloopbaar
- ✅ Goals embedded in JSONB (geen aparte tabel)
- ✅ Activities embedded in JSONB (geen aparte tabel)
- ✅ Koppeling naar diagnoses werkt
- ✅ TEST: Export CarePlan → Import in ander systeem (bijv. Postman/Insomnia)
Epic 6 — Observations (ROM)
Epic Doel: ROM-metingen registreren met FHIR Observation resource.
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | Story Points |
|---|---|---|---|---|---|
| E6.S1 | ROM instrument definitions | PHQ-9, GAD-7 definities in code | ⏳ | E5.S5 | 2 |
| E6.S2 | Observation FHIR transforms | DB ↔ FHIR functies | ⏳ | E6.S1 | 3 |
| E6.S3 | Observation API endpoints | GET/POST /api/fhir/Observation |
⏳ | E6.S2 | 3 |
| E6.S4 | ROM formulieren UI | PHQ-9, GAD-7 invullen + opslaan | ⏳ | E6.S3 | 5 |
Technical Notes:
ROM Instrument Definitions:
// lib/rom/instruments.ts
export const romInstruments = {
'PHQ-9': {
code: '44249-1', // LOINC code
system: 'http://loinc.org',
display: 'PHQ-9 (Patient Health Questionnaire)',
category: 'survey',
valueType: 'quantity',
range: { min: 0, max: 27 },
interpretation: {
'0-4': 'Minimaal',
'5-9': 'Licht',
'10-14': 'Matig',
'15-19': 'Matig-ernstig',
'20-27': 'Ernstig'
}
},
'GAD-7': {
code: '69737-5', // LOINC code
system: 'http://loinc.org',
display: 'GAD-7 (Generalized Anxiety Disorder)',
category: 'survey',
valueType: 'quantity',
range: { min: 0, max: 21 },
interpretation: {
'0-4': 'Minimaal',
'5-9': 'Licht',
'10-14': 'Matig',
'15-21': 'Ernstig'
}
}
};
Observation FHIR Example:
{
"resourceType": "Observation",
"id": "obs-phq9-123",
"status": "final",
"category": [{
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "survey"
}]
}],
"code": {
"coding": [{
"system": "http://loinc.org",
"code": "44249-1",
"display": "PHQ-9 total score"
}]
},
"subject": {
"reference": "Patient/patient-456"
},
"encounter": {
"reference": "Encounter/encounter-789"
},
"effectiveDateTime": "2024-01-15T10:30:00Z",
"issued": "2024-01-15T10:35:00Z",
"performer": [{
"reference": "Practitioner/practitioner-1"
}],
"valueQuantity": {
"value": 18,
"unit": "score",
"system": "http://unitsofmeasure.org",
"code": "{score}"
},
"interpretation": [{
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation",
"code": "H",
"display": "High"
}],
"text": "Matig-ernstige depressie"
}]
}
Acceptance:
- ✅
/api/fhir/ObservationGET/POST werkend - ✅ PHQ-9 formulier werkt (9 vragen, totaalscore)
- ✅ GAD-7 formulier werkt (7 vragen, totaalscore)
- ✅ Timeline toont ROM-metingen per patient
- ✅ Interpretatie automatisch berekend (bijv. "Matig-ernstig")
Epic 7 — API Polish & Demo
Epic Doel: API documentatie, testing en demo scenario voorbereiden.
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | Story Points |
|---|---|---|---|---|---|
| E7.S1 | Swagger/OpenAPI docs | /api/docs met alle FHIR endpoints |
⏳ | E6.S4 | 5 |
| E7.S2 | FHIR validatie toevoegen | @hapi/fhir validator integreren | ⏳ | E7.S1 | 3 |
| E7.S3 | Import/Export testen | CarePlan export → import in andere tool | ⏳ | E7.S2 | 3 |
| E7.S4 | Demo scenario script | Volledige flow: patient → intake → plan → API | ⏳ | E7.S3 | 2 |
Technical Notes:
Swagger Documentation:
// app/api/docs/route.ts
import { generateOpenAPISpec } from '@/lib/openapi';
export async function GET() {
const spec = {
openapi: '3.0.0',
info: {
title: 'Mini-EPD FHIR API',
version: '1.0.0',
description: 'FHIR R4 compliant API voor GGZ data-uitwisseling'
},
servers: [{
url: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000',
description: 'Development server'
}],
paths: {
'/api/fhir/Patient': {
get: {
summary: 'Search patients',
tags: ['Patient'],
responses: {
'200': {
description: 'FHIR Bundle met Patient resources',
content: {
'application/fhir+json': {
schema: { $ref: '#/components/schemas/PatientBundle' }
}
}
}
}
},
post: {
summary: 'Create patient',
tags: ['Patient'],
requestBody: {
content: {
'application/fhir+json': {
schema: { $ref: '#/components/schemas/Patient' }
}
}
},
responses: {
'201': { description: 'Patient created' }
}
}
},
'/api/fhir/CarePlan/{id}': {
get: {
summary: 'Get CarePlan by ID',
tags: ['CarePlan'],
parameters: [{
name: 'id',
in: 'path',
required: true,
schema: { type: 'string' }
}],
responses: {
'200': {
description: 'FHIR CarePlan resource',
content: {
'application/fhir+json': {
schema: { $ref: '#/components/schemas/CarePlan' }
}
}
}
}
}
}
// ... alle andere endpoints
},
components: {
schemas: {
Patient: { /* FHIR Patient schema */ },
CarePlan: { /* FHIR CarePlan schema */ }
}
}
};
return Response.json(spec);
}
Demo Scenario:
# Demo Scenario: Data Uitwisselbaarheid
## Stap 1: Patient aanmaken
1. Open UI: `/patients/new`
2. Vul in: Jan de Vries, 1980-05-15, man
3. Opslaan → Patient ID: `patient-123`
## Stap 2: Intake registreren
1. Open: `/patients/patient-123/encounters/new`
2. Type: Intakegesprek
3. Notitie toevoegen via intake_notes
4. Opslaan → Encounter ID: `encounter-456`
## Stap 3: Diagnose toevoegen
1. Open: `/patients/patient-123/conditions/new`
2. Zoek: F32.2 (Depressieve episode)
3. Ernst: Ernstig, Status: Active
4. Opslaan → Condition ID: `condition-789`
## Stap 4: ROM-meting
1. Open: `/patients/patient-123/observations/new`
2. Instrument: PHQ-9
3. Invullen → Score: 18
4. Opslaan → Observation ID: `obs-123`
## Stap 5: Behandelplan opstellen
1. Open: `/patients/patient-123/care-plans/new`
2. Wizard:
- Diagnose: F32.2 selecteren
- Doel: "PHQ-9 < 10 binnen 12 weken"
- Interventie: "CGT 1x/week, 12 sessies"
3. Opslaan → CarePlan ID: `careplan-abc`
## Stap 6: API Export (DEMO!)
```bash
# Export CarePlan als FHIR JSON
curl http://localhost:3000/api/fhir/CarePlan/careplan-abc \
-H "Accept: application/fhir+json" \
> careplan-export.json
# Toon in Postman of browser
cat careplan-export.json | jq
Stap 7: API Import (DEMO!)
# Edit careplan-export.json (verander status naar "on-hold")
# Import in nieuw systeem
curl -X POST http://andere-epd.com/api/fhir/CarePlan \
-H "Content-Type: application/fhir+json" \
-d @careplan-export.json
# → Success! Behandelplan geïmporteerd in ander EPD
Result: Data-uitwisselbaarheid aangetoond ✅
**Acceptance:**
- ✅ Swagger UI beschikbaar op `/api/docs`
- ✅ Alle FHIR endpoints gedocumenteerd
- ✅ FHIR validatie werkt (optional, strikte mode)
- ✅ Demo scenario compleet doorlopen zonder errors
- ✅ Export/Import test succesvol met externe tool
---
## 5. Kwaliteit & Testplan
### Test Types
| Test Type | Scope | Tools | Coverage Target |
|-----------|-------|-------|-----------------|
| Unit Tests | FHIR transforms, utilities | Vitest | 80%+ voor `/lib/fhir` |
| Integration Tests | API endpoints | Vitest + Supertest | 100% FHIR endpoints |
| FHIR Validation | FHIR JSON output | @hapi/fhir validator | All resources valid |
| Manual Testing | UI flows + API export/import | Manual checklist | 5 critical flows |
| Performance | API response times | Lighthouse, k6 | < 500ms p95 |
### FHIR Compliance Testing
**Validator Setup:**
```bash
npm install @hapi/fhir-validator
// tests/fhir-validation.test.ts
import { Validator } from '@hapi/fhir-validator';
const validator = new Validator();
test('CarePlan output is valid FHIR R4', async () => {
const carePlan = await fetch('/api/fhir/CarePlan/test-123')
.then(r => r.json());
const result = validator.validate(carePlan, {
resourceType: 'CarePlan',
version: 'R4'
});
expect(result.valid).toBe(true);
expect(result.errors).toEqual([]);
});
Manual Test Checklist
FHIR API Tests:
- GET
/api/fhir/Patientreturns Bundle - GET
/api/fhir/Patient/[id]returns single Patient - POST
/api/fhir/Patientcreates new patient - PUT
/api/fhir/Patient/[id]updates patient - GET
/api/fhir/CarePlan/[id]returns valid FHIR JSON - POST
/api/fhir/CarePlanaccepts FHIR JSON import - All FHIR responses have
Content-Type: application/fhir+json - Invalid requests return FHIR OperationOutcome
Data Uitwisselbaarheid:
- Export CarePlan → Import in Postman succeeds
- Export Patient → Validate with online FHIR validator
- Import external FHIR CarePlan → Saves to database correctly
- Swagger docs accessible and accurate
UI Tests:
- Patient CRUD werkt zonder errors
- Encounter timeline toont chronologisch
- Condition toevoegen met DSM-5 autocomplete
- CarePlan wizard compleet doorloopbaar
- ROM formulier berekent totaalscore correct
6. Migratie & Deployment Plan
Database Migratie (Van Simpel → FHIR)
Fase 1: Schema Toevoegen (Non-destructive)
-- Voeg FHIR tabellen toe (NIET vervangen)
-- Behoud: clients, intake_notes, treatment_plans, ai_events
-- Nieuwe tabellen:
CREATE TABLE practitioners (...);
CREATE TABLE organizations (...);
CREATE TABLE patients (...); -- Naast clients
CREATE TABLE encounters (...);
CREATE TABLE conditions (...);
CREATE TABLE observations (...);
CREATE TABLE care_plans (...); -- Naast treatment_plans
Fase 2: Data Migratie
# Run migratie script
pnpm run migrate:to-fhir
# Output:
✅ Migrated 3 clients → 3 patients
✅ Migrated 0 treatment_plans → 0 care_plans
✅ Created 1 default organization
✅ Created 2 demo practitioners
Fase 3: Legacy Tabellen (Read-Only)
-- RLS policies aanpassen: clients/treatment_plans read-only
CREATE POLICY "Legacy: Read only" ON clients
FOR SELECT USING (auth.role() = 'authenticated');
-- Geen INSERT/UPDATE/DELETE policies
Fase 4: UI Cutover
// Feature flag in code
const USE_FHIR_SCHEMA = process.env.NEXT_PUBLIC_USE_FHIR === 'true';
// Gradual rollout
if (USE_FHIR_SCHEMA) {
// Gebruik patients tabel
const patient = await supabase.from('patients').select('*');
} else {
// Fallback naar clients tabel
const client = await supabase.from('clients').select('*');
}
Deployment Strategie
Vercel Deployment:
# Preview deployment (test)
vercel deploy --preview
# Production deployment
vercel deploy --prod
Environment Variables (Vercel):
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxx...
SUPABASE_SERVICE_ROLE_KEY=eyJxxx...
NEXT_PUBLIC_USE_FHIR=true
NEXT_PUBLIC_API_URL=https://mini-epd.vercel.app
7. Risico's & Mitigatie
| Risico | Kans | Impact | Mitigatie | Owner |
|---|---|---|---|---|
| FHIR mapping errors | Hoog | Hoog | Unit tests voor alle transforms, FHIR validator | Developer |
| Data migratie data loss | Middel | Kritiek | Backup voor migratie, rollback script, test eerst op staging | Developer |
| API performance issues | Middel | Middel | Database indexes, query optimization, pagination | Developer |
| FHIR compliance gaps | Middel | Middel | Strikte validator tests, documenteer deviaties | Developer |
| Scope creep (extra features) | Hoog | Middel | Strikte MVP scope, "nice to have" backlog | PM |
| Browser compatibility | Laag | Laag | Test op Chrome/Firefox/Safari | Developer |
| Supabase rate limits | Laag | Middel | Monitor usage, optimize queries | DevOps |
Kritieke Risico's:
-
FHIR Mapping Errors → Mitigatie:
- 80%+ test coverage op transform functies
- FHIR validator in CI/CD pipeline
- Manual testing met externe FHIR tools (Postman, Hapi validator)
-
Data Migratie Data Loss → Mitigatie:
- VOLLEDIGE backup voor migratie starten
- Rollback script ready (
scripts/rollback-migration.ts) - Test migratie eerst op Supabase staging branch
- Dry-run mode in migratie script
-
API Performance → Mitigatie:
- Database indexes op foreign keys
- Pagination op alle list endpoints (max 50 per page)
- Query optimization met
explain analyze
8. Demo & Presentatieplan
Demo Scenario: "Data Uitwisselbaarheid in Actie"
Duur: 15 minuten Doelgroep: Stakeholders, potentiële partners Doel: Tonen dat data uitwisselbaar is via FHIR API
Flow:
Deel 1: UI Demo (5 min)
- Inloggen als behandelaar
- Nieuwe patient aanmaken (Jan de Vries)
- Intake registreren met notities
- Diagnose toevoegen (F32.2 - Depressie)
- ROM-meting invullen (PHQ-9 score: 18)
- Behandelplan opstellen via wizard
Deel 2: API Export Demo (5 min)
7. Open browser DevTools / Postman
8. GET /api/fhir/CarePlan/{id} → Toon FHIR JSON
9. Copy JSON naar clipboard
10. Paste in online FHIR Validator → Valid! ✅
11. Toon Swagger docs: /api/docs
Deel 3: API Import Demo (5 min)
12. Edit FHIR JSON (verander status naar "on-hold")
13. POST /api/fhir/CarePlan met gewijzigde JSON
14. Refresh UI → Behandelplan geïmporteerd! ✅
15. Conclusie: Data is uitwisselbaar tussen systemen
Success Criteria:
- Volledige flow zonder crashes
- FHIR JSON valideert correct
- Import/export werkt bidirectioneel
- Audience begrijpt data-uitwisselbaarheid
Backup Plan:
- Pre-recorded video van API calls
- Screenshots van alle stappen
- Lokale versie klaar bij internet issues
9. Evaluatie & Lessons Learned
Na MVP completion (na Epic 7):
Technische Evaluatie
FHIR Implementation:
- Welke FHIR resources waren makkelijk/moeilijk?
- Welke pragmatische keuzes (embedded JSON) werkten goed?
- Welke deviaties van FHIR spec hebben we?
- Hoe goed valideren externe tools onze output?
Database:
- Hoe verliep de migratie?
- Prestatie van JSONB voor goals/activities?
- RLS policies effectief?
- Indexing strategie optimaal?
API Design:
- Zijn endpoints intuïtief?
- Prestatie acceptabel?
- Error handling duidelijk?
- Swagger docs compleet?
Process Evaluatie
Velocity:
- Actual story points vs estimated
- Welke epics liepen uit?
- Waar onderschat/overschat?
Development Workflow:
- FHIR-first approach effectief?
- Hybrid schema strategie goed?
- Testing strategie adequaat?
Blockers:
- Waar liepen we vast?
- Technische schuld ontstaan?
- Dependencies issues?
User Feedback
Usability:
- Is de API makkelijk te gebruiken?
- Zijn FHIR transforms correct?
- UI intuïtief genoeg?
Features:
- Wat ontbreekt er nog?
- Welke features overbodig?
- Wat moet gerefactored?
10. Referenties
Mission Control Documents
Project Documentation:
- Datamodel Documentatie —
docs/datamodel-documentatie.md - Volledig FHIR Schema —
docs/archive/schemas/20241121_fhir_ggz_schema.sql(archived, gebruiken we deels) - Bouwplan Template —
docs/templates/bouwplan_template.md - Origineel Bouwplan —
docs/bouwplan-mini-epd.md(volledig, 13 resources)
FHIR & Healthcare Standards
FHIR Specificaties:
- FHIR R4 Specification: https://hl7.org/fhir/R4/
- FHIR Patient: https://hl7.org/fhir/R4/patient.html
- FHIR CarePlan: https://hl7.org/fhir/R4/careplan.html
- FHIR Condition: https://hl7.org/fhir/R4/condition.html
- FHIR Observation: https://hl7.org/fhir/R4/observation.html
- FHIR Encounter: https://hl7.org/fhir/R4/encounter.html
Nederlandse Standaarden:
- MedMIJ GGZ: https://informatiestandaarden.nictiz.nl/wiki/MedMij:V2020.01/OntwerpGGZ
- ZIBs: https://zibs.nl/
- FHIR Validator (online): https://validator.fhir.org/
Tools:
- HAPI FHIR Validator: https://hapifhir.io/hapi-fhir/docs/validation/introduction.html
- Postman FHIR Collection: https://www.postman.com/fhir
Technical Stack
- Next.js 15: https://nextjs.org/docs
- Supabase: https://supabase.com/docs
- TypeScript: https://www.typescriptlang.org/docs/
- FHIR TypeScript Types: https://github.com/Asymmetrik/node-fhir-server-core
11. Glossary & Abbreviations
FHIR Resources (Geïmplementeerd)
| Resource | Betekenis | API Endpoint |
|---|---|---|
| Patient | Patiënt/cliënt | /api/fhir/Patient |
| Practitioner | Behandelaar | /api/fhir/Practitioner |
| Encounter | Contactmoment | /api/fhir/Encounter |
| Condition | Diagnose | /api/fhir/Condition |
| Observation | Meting (ROM) | /api/fhir/Observation |
| CarePlan | Behandelplan | /api/fhir/CarePlan |
FHIR Resources (Niet Geïmplementeerd)
| Resource | Reden |
|---|---|
| MedicationStatement | Out of scope voor MVP |
| Consent | AVG niet 100% vereist |
| Flag | Safety features later |
| DocumentReference | Documenten later |
| Goal | Embedded in CarePlan (pragmatisch) |
| Activity | Embedded in CarePlan (pragmatisch) |
Technical Terms
| Term | Betekenis |
|---|---|
| FHIR R4 | Fast Healthcare Interoperability Resources, versie 4 |
| MedMIJ | Nederlands afsprakenstelsel voor patiëntportalen |
| ZIB | ZorgInformatieBouwsteen (NL healthcare data standard) |
| DSM-5 | Diagnostic and Statistical Manual (psychiatrie) |
| ROM | Routine Outcome Monitoring (vragenlijsten) |
| RLS | Row Level Security (database access control) |
| BSN | Burgerservicenummer (NL social security number) |
| LOINC | Logical Observation Identifiers Names and Codes |
| ICD-10 | International Classification of Diseases |
Appendix A: Story Point Estimatie
Fibonacci Scale:
- 1 punt: < 2 uur (trivial)
- 2 punten: 2-4 uur (simpel)
- 3 punten: 4-8 uur (gemiddeld)
- 5 punten: 1-2 dagen (complex)
- 8 punten: 2-3 dagen (zeer complex)
- 13 punten: 3-5 dagen (epic-sized, overweeg split)
Velocity:
- 1 developer, full-time: ~15 story points per week (pragmatisch MVP tempo)
- Total: ~117 story points
- Duration: 8 weken @ 15 points/week
Appendix B: FHIR Schema Vergelijking
Volledig Schema (origineel bouwplan):
- 13 FHIR resources
- Aparte tabellen voor goals, activities
- Consent management
- Flags & waarschuwingen
- Medicatie tracking
- Document management
- ~200 story points, 10-12 weken
Pragmatisch Schema (dit bouwplan):
- 6 FHIR resources (core)
- Goals/activities embedded in CarePlan JSONB
- Geen consents (later)
- Geen flags (later)
- Geen medicatie (later)
- Geen documents (later)
- ~117 story points, 8 weken
Verschil:
- ⬇️ 54% minder resources
- ⬇️ 42% minder development tijd
- ✅ Data-uitwisselbaarheid behouden
- ✅ MedMIJ-compatible datastructuur
- ✅ Schaalbaar naar volledig schema later
Appendix C: API Endpoints Overzicht
Geïmplementeerde FHIR Endpoints:
GET /api/fhir/Patient → Bundle(Patient[])
GET /api/fhir/Patient/{id} → Patient
POST /api/fhir/Patient → Patient (created)
PUT /api/fhir/Patient/{id} → Patient (updated)
GET /api/fhir/Practitioner → Bundle(Practitioner[])
GET /api/fhir/Practitioner/{id} → Practitioner
POST /api/fhir/Practitioner → Practitioner
GET /api/fhir/Encounter → Bundle(Encounter[])
GET /api/fhir/Encounter/{id} → Encounter
POST /api/fhir/Encounter → Encounter
PUT /api/fhir/Encounter/{id} → Encounter
GET /api/fhir/Condition → Bundle(Condition[])
GET /api/fhir/Condition/{id} → Condition
POST /api/fhir/Condition → Condition
PUT /api/fhir/Condition/{id} → Condition
GET /api/fhir/Observation → Bundle(Observation[])
GET /api/fhir/Observation/{id} → Observation
POST /api/fhir/Observation → Observation
GET /api/fhir/CarePlan → Bundle(CarePlan[])
GET /api/fhir/CarePlan/{id} → CarePlan 🎯
POST /api/fhir/CarePlan → CarePlan 🎯
PUT /api/fhir/CarePlan/{id} → CarePlan 🎯
Swagger Documentation:
GET /api/docs → OpenAPI 3.0 spec
GET /api/docs/ui → Swagger UI (interactive)
Response Format:
- Content-Type:
application/fhir+json - Header:
X-FHIR-Version: 4.0.1 - Errors: FHIR OperationOutcome
🎯 Dit pragmatische bouwplan is klaar voor implementatie!
Volgende stap: Start Epic 1 (FHIR Core Schema & Migratie)
Versiehistorie:
| Versie | Datum | Auteur | Wijziging |
|---|---|---|---|
| v2.0 | 21 november 2024 | Colin Lit | Pragmatische FHIR versie - 6 core resources, 8 weken, data-uitwisselbaarheid focus |
| v1.0 | 21 november 2024 | Colin Lit | Originele versie - 13 FHIR resources, 10-12 weken |