1799 lines
55 KiB
Markdown
1799 lines
55 KiB
Markdown
# 🚀 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 uitleg
|
|
- `docs/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")
|
|
|
|
**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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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):**
|
|
```sql
|
|
-- 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**
|
|
```sql
|
|
-- 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:**
|
|
```typescript
|
|
// 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:**
|
|
```sql
|
|
-- 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:**
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// 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/Patient` GET/POST/PUT werkend (`app/api/fhir/Patient/route.ts`, `app/api/fhir/Patient/[id]/route.ts`)
|
|
- ✅ `/api/fhir/Practitioner` GET/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):**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
// 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/Encounter` CRUD 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:**
|
|
```typescript
|
|
// 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:**
|
|
```json
|
|
{
|
|
"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/Condition` CRUD 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):**
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// 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/CarePlan` GET/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:**
|
|
```typescript
|
|
// 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:**
|
|
```json
|
|
{
|
|
"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/Observation` GET/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:**
|
|
```typescript
|
|
// 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:**
|
|
```markdown
|
|
# 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!)
|
|
```bash
|
|
# 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
|
|
```
|
|
|
|
```typescript
|
|
// 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/Patient` returns Bundle
|
|
- [ ] GET `/api/fhir/Patient/[id]` returns single Patient
|
|
- [ ] POST `/api/fhir/Patient` creates new patient
|
|
- [ ] PUT `/api/fhir/Patient/[id]` updates patient
|
|
- [ ] GET `/api/fhir/CarePlan/[id]` returns valid FHIR JSON
|
|
- [ ] POST `/api/fhir/CarePlan` accepts 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)**
|
|
```sql
|
|
-- 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**
|
|
```bash
|
|
# 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)**
|
|
```sql
|
|
-- 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**
|
|
```typescript
|
|
// 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:**
|
|
```bash
|
|
# Preview deployment (test)
|
|
vercel deploy --preview
|
|
|
|
# Production deployment
|
|
vercel deploy --prod
|
|
```
|
|
|
|
**Environment Variables (Vercel):**
|
|
```bash
|
|
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:**
|
|
|
|
1. **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)
|
|
|
|
2. **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
|
|
|
|
3. **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)**
|
|
1. Inloggen als behandelaar
|
|
2. Nieuwe patient aanmaken (Jan de Vries)
|
|
3. Intake registreren met notities
|
|
4. Diagnose toevoegen (F32.2 - Depressie)
|
|
5. ROM-meting invullen (PHQ-9 score: 18)
|
|
6. 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 |
|