# 🚀 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 - `lib/supabase/20241121_fhir_ggz_schema.sql` - Volledig schema (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 `20241121_fhir_ggz_schema.sql` bevat alle 13 FHIR resources. Voor de pragmatische aanpak maken we een **nieuw schema bestand**: `lib/supabase/20241121_pragmatic_fhir_schema.sql` **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 { 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 { // 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** — `lib/supabase/20241121_fhir_ggz_schema.sql` (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 |