Files
triqura-ecd/docs/specs/archive/bouwplan-pragmatisch-fhir.md
2025-11-23 10:13:00 +01:00

55 KiB

🚀 Bouwplan Mini-EPD — Pragmatische FHIR Aanpak

💡 Focus: Data-uitwisselbaarheid met MedMIJ/FHIR bouwstenen voor prototype

Dit bouwplan beschrijft een pragmatische implementatie van FHIR resources gericht op interoperabiliteit en API-based data-uitwisseling. Niet alle 13 FHIR resources worden geïmplementeerd—alleen wat nodig is voor een werkend, uitwisselbaar prototype.


Projectnaam: Mini-EPD Prototype (Pragmatic FHIR Edition) Versie: v2.0 (Pragmatisch) Datum: 21 november 2024 Auteur: Colin Lit (ikbenlit.nl)

📊 Voortgang

Voltooide Epics: 2 van 7 (29%) Voltooide Story Points: 34 van 117 (29%) Status: Epic 1 & 2 Gereed - FHIR Foundation Complete!

Epic Status Voltooiingsdatum
E0 - Setup & Config Gereed Pre-project
E1 - FHIR Core Schema Gereed 21 november 2024
E2 - Patients & Practitioners Gereed 21 november 2024
E3 - Encounters (Intake) To Do -
E4 - Conditions (DSM-5) To Do -
E5 - CarePlans (Treatment) 🎯 To Do -
E6 - Observations (ROM) To Do -
E7 - API Polish & Demo To Do -

Huidige Sprint: Epic 3 - Encounters (Intake)


1. Doel en Context

🎯 Primair Doel: Een werkend EPD-prototype bouwen waarbij behandelplannen en klinische data uitwisselbaar zijn via FHIR-compliant API's. Het systeem moet data kunnen exporteren én importeren in standaard FHIR JSON formaat.

📘 Context: Dit is een pragmatisch prototype met focus op:

  • Data-uitwisselbaarheid: Andere systemen kunnen jouw data lezen/schrijven
  • MedMIJ/FHIR bouwstenen: Basis voor toekomstige certificering
  • API-first: Behandelplannen via GET /api/fhir/CarePlan/{id}
  • ⚠️ Goed genoeg AVG: Niet 100% compliant, maar verantwoord voor prototype
  • ⚠️ Geen complete features: Focus op kern, rest komt later

Wat dit NIET is:

  • Volledig production-ready EPD
  • 100% MedMIJ-gecertificeerd
  • Compleet consent management systeem
  • Multi-tenant SaaS platform

Referentie documenten:

  • docs/datamodel-documentatie.md - FHIR 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 clientspatients, treatment_planscare_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

    // Database → FHIR JSON
    export function dbCarePlanToFHIR(dbRow: CarePlanRow): FHIRCarePlan {
      return {
        resourceType: "CarePlan",
        id: dbRow.id,
        status: dbRow.status,
        intent: dbRow.intent,
        subject: {
          reference: `Patient/${dbRow.patient_id}`,
          display: dbRow.patient_name
        },
        // ... mapping logic
      };
    }
    
    // FHIR JSON → Database
    export function fhirCarePlanToDB(fhir: FHIRCarePlan): CarePlanInsert {
      return {
        id: fhir.id,
        status: fhir.status,
        patient_id: extractIdFromReference(fhir.subject.reference),
        // ... mapping logic
      };
    }
    
  • API Response Format

    // All FHIR endpoints return application/fhir+json
    return Response.json(fhirResource, {
      headers: {
        'Content-Type': 'application/fhir+json',
        'X-FHIR-Version': '4.0.1'
      }
    });
    
  • Error Handling

    • FHIR OperationOutcome voor API errors
    • User-friendly messages in UI
    • Structured logging voor debugging
  • Security

    • RLS policies per FHIR resource
    • Bearer token auth voor API (simpel, geen OAuth2)
    • Input validation met Zod + FHIR schema validation

3. Epics & Stories Overzicht

🎯 Pragmatische implementatie: 8 epics, 32 stories, ~120 story points

Epic ID Titel Doel Status Stories Story Points Weken
E0 Setup & Config Next.js, Supabase (done) Gereed 5 10 0
E1 FHIR Core Schema 6 FHIR tabellen + migratie Gereed 5 21 1.5
E2 Patients & Practitioners FHIR Patient/Practitioner CRUD + API Gereed 4 13 1
E3 Encounters (Intake) Contactmoment registratie + API To Do 4 13 1
E4 Conditions (DSM-5) Diagnose registratie + API To Do 4 13 1
E5 CarePlans (Treatment) 🎯 Behandelplan CRUD + FHIR API To Do 5 21 2
E6 Observations (ROM) ROM-metingen + API To Do 4 13 1
E7 API Polish & Demo Swagger docs, demo scenario, testing To Do 4 13 0.5

Totaal: 35 stories, ~117 story points, 8 weken @ 15 points/week


4. Epics & Stories (Uitwerking)

Epic 0 — Setup & Configuratie

Status: GEREED (al gedaan in huidige setup)

Wat is al actief:

  • Next.js project
  • Supabase project + connection
  • Supabase Auth (20 users)
  • RLS enabled op tabellen
  • Demo users systeem
  • Basis tabellen: clients, intake_notes, treatment_plans, ai_events

Geen actie vereist - ga door naar Epic 1


Epic 1 — FHIR Core Schema & Migratie

Epic Doel: FHIR-compliant database tabellen toevoegen en bestaande data migreren. Status: GEREED (21 november 2024)

Story ID Beschrijving Acceptatiecriteria Status Afhankelijkheden Story Points
E1.S1 Pragmatisch FHIR schema maken Nieuw schema: 20241121_pragmatic_fhir_schema.sql met 6 resources + embedded goals/activities Gereed E0 5
E1.S2 Migratie toepassen SQL uitvoeren in Supabase, verify tabellen Gereed E1.S1 3
E1.S3 TypeScript types genereren supabase gen types/types/database.ts Gereed E1.S2 2
E1.S4 Data migratie script clientspatients, treatment_planscare_plans Gereed E1.S3 8
E1.S5 Seed data FHIR Demo practitioners, organizations, FHIR patients Gereed E1.S4 3

Technical Notes:

E1.S1: Nieuw Schema Bestand Maken

Het originele schema docs/archive/schemas/20241121_fhir_ggz_schema.sql bevat alle 13 FHIR resources. Voor de pragmatische aanpak maken we een nieuw schema bestand: docs/archive/schemas/20241121_pragmatic_fhir_schema.sql (beide archived)

Verschillen met origineel schema:

Aspect Origineel Schema Pragmatisch Schema
Resources 13 tabellen 7 tabellen (6 FHIR + 1 org)
Goals Aparte goals tabel Embedded in care_plans.goals JSONB
Activities Aparte care_plan_activities tabel Embedded in care_plans.activities JSONB
Medications medication_statements tabel Niet geïmplementeerd
Consents consents tabel Niet geïmplementeerd
Flags flags tabel Niet geïmplementeerd
Documents document_references tabel Niet geïmplementeerd
BSN encryptie pgcrypto encryptie Placeholder BSN (demo)

FHIR Resources Implementeren (7 tabellen):

-- Core 6 FHIR resources + 1 organization
CREATE TABLE practitioners (...);       -- FHIR Practitioner
CREATE TABLE organizations (...);       -- FHIR Organization (1 default)
CREATE TABLE patients (...);            -- FHIR Patient (BSN placeholder)
CREATE TABLE encounters (...);          -- FHIR Encounter
CREATE TABLE conditions (...);          -- FHIR Condition (DSM-5)
CREATE TABLE observations (...);        -- FHIR Observation (ROM)
CREATE TABLE care_plans (...);          -- FHIR CarePlan (goals/activities embedded!)

-- Behouden:
 clients (legacy, read-only na migratie)
 intake_notes (blijft bestaan, later DocumentReference)
 treatment_plans (legacy, read-only na migratie)
 ai_events (blijft bestaan)
 demo_users (blijft bestaan)

Belangrijk verschil: care_plans tabel

-- Pragmatisch: Embedded goals en activities
CREATE TABLE care_plans (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  -- ... andere velden ...

  -- PRAGMATISCH: JSONB embedded ipv aparte tabellen
  goals JSONB DEFAULT '[]'::jsonb,
  activities JSONB DEFAULT '[]'::jsonb,

  -- Goals structure: [{description, target}, ...]
  -- Activities structure: [{detail: {code, status, scheduledTiming}}, ...]
);

-- VS Origineel: Aparte tabellen
-- CREATE TABLE goals (...);                -- ❌ Niet in pragmatisch schema
-- CREATE TABLE care_plan_activities (...); -- ❌ Niet in pragmatisch schema

Migratie Strategie:

// scripts/migrate-to-fhir.ts
async function migrateClientsToPatients() {
  const clients = await supabase.from('clients').select('*');

  for (const client of clients.data) {
    await supabase.from('patients').insert({
      id: client.id,  // Behoud UUID voor referential integrity
      identifier_bsn: '999999990',  // Placeholder BSN
      identifier_client_number: client.id,
      name_family: client.last_name,
      name_given: [client.first_name],
      birth_date: client.birth_date,
      gender: 'unknown',
      active: true,
      created_at: client.created_at,
      updated_at: client.updated_at
    });
  }

  console.log(`✅ Migrated ${clients.data.length} clients → patients`);
}

async function migrateTreatmentPlansToCarePlans() {
  const plans = await supabase
    .from('treatment_plans')
    .select('*, clients(first_name, last_name)');

  for (const plan of plans.data) {
    const goals = plan.plan.doelen?.map(doel => ({
      description: { text: doel },
      // FHIR Goal structure embedded
    })) || [];

    const activities = plan.plan.interventies?.map(interventie => ({
      detail: {
        code: { text: interventie },
        status: 'not-started',
        // FHIR Activity structure embedded
      }
    })) || [];

    await supabase.from('care_plans').insert({
      id: plan.id,
      patient_id: plan.client_id,
      status: plan.status === 'gepubliceerd' ? 'active' : 'draft',
      intent: 'plan',
      title: `Behandelplan v${plan.version}`,
      category_code: 'ggz-behandelplan',
      category_display: 'GGZ Behandelplan',
      goals: goals,  // JSONB embedded
      activities: activities,  // JSONB embedded (pragmatisch!)
      period_start: plan.created_at,
      created_date: plan.created_at,
      created_by: plan.created_by
    });
  }

  console.log(`✅ Migrated ${plans.data.length} treatment_plans → care_plans`);
}

Acceptance:

  • 6 FHIR tabellen aanwezig in Supabase
  • TypeScript types gegenereerd
  • Bestaande data gemigreerd (clients → patients, treatment_plans → care_plans)
  • Legacy tabellen read-only (geen DELETE policies)
  • Seed data met 3 demo patients, 2 practitioners, 1 organization

FHIR Schema Details:

-- Simplified: Goals en Activities embedded in JSONB
CREATE TABLE care_plans (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  identifier TEXT UNIQUE DEFAULT gen_random_uuid()::TEXT,

  status careplan_status NOT NULL DEFAULT 'draft',
  intent TEXT NOT NULL DEFAULT 'plan',

  title TEXT NOT NULL,
  description TEXT,

  patient_id UUID REFERENCES patients(id) NOT NULL,
  encounter_id UUID REFERENCES encounters(id),
  author_id UUID REFERENCES practitioners(id),

  period_start DATE,
  period_end DATE,

  category_code TEXT DEFAULT 'ggz-behandelplan',
  category_display TEXT DEFAULT 'GGZ Behandelplan',

  -- PRAGMATISCH: Embedded JSON ipv aparte tabellen
  goals JSONB DEFAULT '[]'::jsonb,  -- Array van FHIR Goal structures
  activities JSONB DEFAULT '[]'::jsonb,  -- Array van FHIR Activity structures

  addresses_condition_ids UUID[],  -- Welke diagnoses behandeld

  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

COMMENT ON COLUMN care_plans.goals IS 'FHIR Goal structures embedded as JSONB array';
COMMENT ON COLUMN care_plans.activities IS 'FHIR CarePlan.activity structures embedded as JSONB array';

Epic 2 — Patients & Practitioners

Epic Doel: FHIR Patient en Practitioner CRUD + API endpoints. Status: GEREED (21 november 2024)

Story ID Beschrijving Acceptatiecriteria Status Afhankelijkheden Story Points
E2.S1 FHIR transforms library DB → FHIR, FHIR → DB functies Gereed E1.S5 5
E2.S2 Patient API endpoints GET/POST/PUT /api/fhir/Patient Gereed E2.S1 3
E2.S3 Practitioner API endpoints GET/POST /api/fhir/Practitioner Gereed E2.S1 2
E2.S4 Patient UI (CRUD) /epd/patients lijst + detail + forms Gereed E2.S2 3

Technical Notes:

FHIR Transform Library:

// lib/fhir/transforms/patient.ts
import type { FHIRPatient, Database } from '@/types';

type PatientRow = Database['public']['Tables']['patients']['Row'];

export function dbPatientToFHIR(row: PatientRow): FHIRPatient {
  return {
    resourceType: "Patient",
    id: row.id,
    identifier: [
      {
        system: "http://fhir.nl/fhir/NamingSystem/bsn",
        value: row.identifier_bsn
      },
      {
        system: "urn:oid:2.16.840.1.113883.2.4.3.11.999.7.6",
        value: row.identifier_client_number || row.id
      }
    ],
    name: [{
      use: "official",
      family: row.name_family,
      given: row.name_given,
      prefix: row.name_prefix ? [row.name_prefix] : undefined
    }],
    birthDate: row.birth_date,
    gender: row.gender as "male" | "female" | "other" | "unknown",
    telecom: [
      row.telecom_phone && {
        system: "phone",
        value: row.telecom_phone,
        use: "mobile"
      },
      row.telecom_email && {
        system: "email",
        value: row.telecom_email
      }
    ].filter(Boolean),
    address: row.address_line ? [{
      use: "home",
      line: row.address_line,
      city: row.address_city,
      postalCode: row.address_postal_code,
      country: row.address_country || "NL"
    }] : undefined,
    active: row.active,
    meta: {
      lastUpdated: row.updated_at
    }
  };
}

export function fhirPatientToDB(fhir: FHIRPatient): Partial<PatientRow> {
  const bsn = fhir.identifier?.find(i =>
    i.system === "http://fhir.nl/fhir/NamingSystem/bsn"
  )?.value;

  const name = fhir.name?.[0];
  const address = fhir.address?.[0];
  const phone = fhir.telecom?.find(t => t.system === "phone")?.value;
  const email = fhir.telecom?.find(t => t.system === "email")?.value;

  return {
    id: fhir.id,
    identifier_bsn: bsn || '999999990',
    name_family: name?.family || '',
    name_given: name?.given || [],
    name_prefix: name?.prefix?.[0],
    birth_date: fhir.birthDate,
    gender: fhir.gender || 'unknown',
    telecom_phone: phone,
    telecom_email: email,
    address_line: address?.line,
    address_city: address?.city,
    address_postal_code: address?.postalCode,
    address_country: address?.country || 'NL',
    active: fhir.active ?? true
  };
}

API Endpoints:

// app/api/fhir/Patient/route.ts
import { dbPatientToFHIR, fhirPatientToDB } from '@/lib/fhir/transforms/patient';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const id = searchParams.get('_id');

  let query = supabase.from('patients').select('*');

  if (id) {
    query = query.eq('id', id).single();
    const { data, error } = await query;

    if (error || !data) {
      return Response.json({
        resourceType: "OperationOutcome",
        issue: [{
          severity: "error",
          code: "not-found",
          diagnostics: "Patient not found"
        }]
      }, { status: 404 });
    }

    return Response.json(dbPatientToFHIR(data), {
      headers: { 'Content-Type': 'application/fhir+json' }
    });
  }

  // Search all patients
  const { data, error } = await query;

  return Response.json({
    resourceType: "Bundle",
    type: "searchset",
    total: data?.length || 0,
    entry: data?.map(row => ({
      resource: dbPatientToFHIR(row)
    })) || []
  }, {
    headers: { 'Content-Type': 'application/fhir+json' }
  });
}

export async function POST(request: Request) {
  const fhirPatient = await request.json();
  const dbPatient = fhirPatientToDB(fhirPatient);

  const { data, error } = await supabase
    .from('patients')
    .insert(dbPatient)
    .select()
    .single();

  if (error) {
    return Response.json({
      resourceType: "OperationOutcome",
      issue: [{
        severity: "error",
        code: "processing",
        diagnostics: error.message
      }]
    }, { status: 400 });
  }

  return Response.json(dbPatientToFHIR(data), {
    status: 201,
    headers: {
      'Content-Type': 'application/fhir+json',
      'Location': `/api/fhir/Patient/${data.id}`
    }
  });
}

Acceptance: VOLTOOID

  • /api/fhir/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):

const encounterTypes = [
  { code: 'intake', display: 'Intakegesprek' },
  { code: 'diagnostiek', display: 'Diagnostisch onderzoek' },
  { code: 'behandeling', display: 'Behandelsessie' },
  { code: 'follow-up', display: 'Follow-up gesprek' },
  { code: 'crisis', display: 'Crisisinterventie' }
];

const encounterClass = [
  { code: 'AMB', display: 'Ambulatory (polikliniek)' },
  { code: 'IMP', display: 'Inpatient (kliniek)' },
  { code: 'VR', display: 'Virtual (online)' }
];

Encounter → Intake Notes Koppeling:

// Behoud intake_notes, link naar encounter
async function linkIntakeNoteToEncounter(noteId: string, encounterId: string) {
  await supabase
    .from('intake_notes')
    .update({
      encounter_id: encounterId,  // Add column via migration
      tag: 'Intake'
    })
    .eq('id', noteId);
}

Acceptance:

  • /api/fhir/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:

// scripts/seed-dsm5-codes.ts
const dsm5Codes = [
  {
    code: 'F32.2',
    display: 'Depressieve episode, ernstig zonder psychotische kenmerken',
    system: 'http://hl7.org/fhir/sid/icd-10',
    category: 'stemming'
  },
  {
    code: 'F41.1',
    display: 'Gegeneraliseerde angststoornis',
    system: 'http://hl7.org/fhir/sid/icd-10',
    category: 'angst'
  },
  {
    code: 'F60.31',
    display: 'Borderline persoonlijkheidsstoornis',
    system: 'http://hl7.org/fhir/sid/icd-10',
    category: 'persoonlijkheid'
  },
  {
    code: 'F20.0',
    display: 'Paranoïde schizofrenie',
    system: 'http://hl7.org/fhir/sid/icd-10',
    category: 'psychotisch'
  },
  {
    code: 'F84.0',
    display: 'Autismespectrumstoornis',
    system: 'http://hl7.org/fhir/sid/icd-10',
    category: 'ontwikkeling'
  }
  // ... 45 more
];

Condition FHIR Example:

{
  "resourceType": "Condition",
  "id": "condition-123",
  "clinicalStatus": {
    "coding": [{
      "system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
      "code": "active"
    }]
  },
  "verificationStatus": {
    "coding": [{
      "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status",
      "code": "confirmed"
    }]
  },
  "category": [{
    "coding": [{
      "system": "http://terminology.hl7.org/CodeSystem/condition-category",
      "code": "encounter-diagnosis"
    }]
  }],
  "severity": {
    "coding": [{
      "system": "http://snomed.info/sct",
      "code": "24484000",
      "display": "Severe"
    }]
  },
  "code": {
    "coding": [{
      "system": "http://hl7.org/fhir/sid/icd-10",
      "code": "F32.2",
      "display": "Depressieve episode, ernstig"
    }]
  },
  "subject": {
    "reference": "Patient/patient-456"
  },
  "encounter": {
    "reference": "Encounter/encounter-789"
  },
  "onsetDateTime": "2024-01-15",
  "recordedDate": "2024-01-15T14:30:00Z",
  "recorder": {
    "reference": "Practitioner/practitioner-1"
  }
}

Acceptance:

  • DSM-5 codes in database (lookup tabel of JSONB)
  • /api/fhir/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):

// lib/fhir/transforms/careplan.ts
import type { FHIRCarePlan, Database } from '@/types';

type CarePlanRow = Database['public']['Tables']['care_plans']['Row'] & {
  patient: Database['public']['Tables']['patients']['Row'];
  author: Database['public']['Tables']['practitioners']['Row'];
  conditions: Database['public']['Tables']['conditions']['Row'][];
};

export function dbCarePlanToFHIR(row: CarePlanRow): FHIRCarePlan {
  return {
    resourceType: "CarePlan",
    id: row.id,
    identifier: [{
      system: "urn:oid:2.16.840.1.113883.2.4.3.11.999.7.6",
      value: row.identifier
    }],
    status: row.status as "draft" | "active" | "on-hold" | "revoked" | "completed",
    intent: row.intent as "proposal" | "plan" | "order",
    category: [{
      coding: [{
        system: "http://hl7.org/fhir/care-plan-category",
        code: row.category_code || "ggz-behandelplan",
        display: row.category_display || "GGZ Behandelplan"
      }]
    }],
    title: row.title,
    description: row.description,
    subject: {
      reference: `Patient/${row.patient_id}`,
      display: `${row.patient.name_given.join(' ')} ${row.patient.name_family}`
    },
    encounter: row.encounter_id ? {
      reference: `Encounter/${row.encounter_id}`
    } : undefined,
    period: {
      start: row.period_start,
      end: row.period_end
    },
    created: row.created_date,
    author: row.author_id ? {
      reference: `Practitioner/${row.author_id}`,
      display: `${row.author.name_given.join(' ')} ${row.author.name_family}`
    } : undefined,
    addresses: row.addresses_condition_ids?.map(conditionId => ({
      reference: `Condition/${conditionId}`
    })) || [],
    // GOALS: Embedded JSONB → FHIR Goal array
    goal: (row.goals as any[])?.map((goal: any) => ({
      description: {
        text: goal.description || goal.text
      },
      target: goal.target ? [{
        measure: goal.target.measure,
        detailQuantity: goal.target.detailQuantity,
        dueDate: goal.target.dueDate
      }] : undefined
    })) || [],
    // ACTIVITIES: Embedded JSONB → FHIR Activity array
    activity: (row.activities as any[])?.map((activity: any) => ({
      detail: {
        code: {
          text: activity.code?.text || activity.description
        },
        status: activity.status || "not-started",
        scheduledTiming: activity.scheduledTiming,
        performer: activity.performer ? [{
          reference: `Practitioner/${activity.performer}`
        }] : undefined,
        description: activity.description,
        location: activity.location
      }
    })) || [],
    meta: {
      lastUpdated: row.updated_at
    }
  };
}

export function fhirCarePlanToDB(fhir: FHIRCarePlan): Partial<CarePlanRow> {
  // Extract patient ID from reference
  const patientId = fhir.subject?.reference?.replace('Patient/', '');
  const authorId = fhir.author?.reference?.replace('Practitioner/', '');
  const encounterId = fhir.encounter?.reference?.replace('Encounter/', '');

  // Extract condition IDs
  const conditionIds = fhir.addresses?.map(addr =>
    addr.reference?.replace('Condition/', '')
  ).filter(Boolean) as string[];

  // Convert FHIR goals to JSONB
  const goals = fhir.goal?.map(goal => ({
    description: goal.description?.text,
    target: goal.target?.[0] ? {
      measure: goal.target[0].measure,
      detailQuantity: goal.target[0].detailQuantity,
      dueDate: goal.target[0].dueDate
    } : undefined
  }));

  // Convert FHIR activities to JSONB
  const activities = fhir.activity?.map(activity => ({
    code: {
      text: activity.detail?.code?.text
    },
    status: activity.detail?.status || 'not-started',
    scheduledTiming: activity.detail?.scheduledTiming,
    performer: activity.detail?.performer?.[0]?.reference?.replace('Practitioner/', ''),
    description: activity.detail?.description,
    location: activity.detail?.location
  }));

  return {
    id: fhir.id,
    status: fhir.status || 'draft',
    intent: fhir.intent || 'plan',
    title: fhir.title || '',
    description: fhir.description,
    patient_id: patientId,
    encounter_id: encounterId,
    author_id: authorId,
    period_start: fhir.period?.start,
    period_end: fhir.period?.end,
    category_code: fhir.category?.[0]?.coding?.[0]?.code || 'ggz-behandelplan',
    category_display: fhir.category?.[0]?.coding?.[0]?.display || 'GGZ Behandelplan',
    addresses_condition_ids: conditionIds,
    goals: goals as any,  // JSONB
    activities: activities as any,  // JSONB
    created_date: fhir.created
  };
}

API Endpoint met Join:

// app/api/fhir/CarePlan/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const { data, error } = await supabase
    .from('care_plans')
    .select(`
      *,
      patient:patients(*),
      author:practitioners(*),
      conditions:conditions(*)
    `)
    .eq('id', params.id)
    .single();

  if (error || !data) {
    return Response.json({
      resourceType: "OperationOutcome",
      issue: [{
        severity: "error",
        code: "not-found",
        diagnostics: "CarePlan not found"
      }]
    }, { status: 404 });
  }

  const fhirCarePlan = dbCarePlanToFHIR(data);

  return Response.json(fhirCarePlan, {
    headers: {
      'Content-Type': 'application/fhir+json',
      'X-FHIR-Version': '4.0.1'
    }
  });
}

// POST: Import FHIR CarePlan
export async function POST(request: Request) {
  const fhirCarePlan = await request.json() as FHIRCarePlan;

  // Validate FHIR structure (basic)
  if (fhirCarePlan.resourceType !== 'CarePlan') {
    return Response.json({
      resourceType: "OperationOutcome",
      issue: [{
        severity: "error",
        code: "invalid",
        diagnostics: "resourceType must be 'CarePlan'"
      }]
    }, { status: 400 });
  }

  const dbCarePlan = fhirCarePlanToDB(fhirCarePlan);

  const { data, error } = await supabase
    .from('care_plans')
    .insert(dbCarePlan)
    .select()
    .single();

  if (error) {
    return Response.json({
      resourceType: "OperationOutcome",
      issue: [{
        severity: "error",
        code: "processing",
        diagnostics: error.message
      }]
    }, { status: 400 });
  }

  return Response.json(dbCarePlanToFHIR(data), {
    status: 201,
    headers: {
      'Content-Type': 'application/fhir+json',
      'Location': `/api/fhir/CarePlan/${data.id}`
    }
  });
}

UI Wizard:

// components/features/care-plans/CarePlanWizard.tsx
const steps = [
  {
    title: 'Diagnoses selecteren',
    description: 'Welke aandoeningen worden behandeld?',
    component: ConditionSelector
  },
  {
    title: 'Doelen formuleren',
    description: 'Wat willen we bereiken? (SMART)',
    component: GoalsEditor
  },
  {
    title: 'Interventies plannen',
    description: 'Welke behandelactiviteiten?',
    component: ActivitiesEditor
  },
  {
    title: 'Review & Opslaan',
    description: 'Controleer en publiceer behandelplan',
    component: CarePlanReview
  }
];

Acceptance:

  • /api/fhir/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:

// lib/rom/instruments.ts
export const romInstruments = {
  'PHQ-9': {
    code: '44249-1',  // LOINC code
    system: 'http://loinc.org',
    display: 'PHQ-9 (Patient Health Questionnaire)',
    category: 'survey',
    valueType: 'quantity',
    range: { min: 0, max: 27 },
    interpretation: {
      '0-4': 'Minimaal',
      '5-9': 'Licht',
      '10-14': 'Matig',
      '15-19': 'Matig-ernstig',
      '20-27': 'Ernstig'
    }
  },
  'GAD-7': {
    code: '69737-5',  // LOINC code
    system: 'http://loinc.org',
    display: 'GAD-7 (Generalized Anxiety Disorder)',
    category: 'survey',
    valueType: 'quantity',
    range: { min: 0, max: 21 },
    interpretation: {
      '0-4': 'Minimaal',
      '5-9': 'Licht',
      '10-14': 'Matig',
      '15-21': 'Ernstig'
    }
  }
};

Observation FHIR Example:

{
  "resourceType": "Observation",
  "id": "obs-phq9-123",
  "status": "final",
  "category": [{
    "coding": [{
      "system": "http://terminology.hl7.org/CodeSystem/observation-category",
      "code": "survey"
    }]
  }],
  "code": {
    "coding": [{
      "system": "http://loinc.org",
      "code": "44249-1",
      "display": "PHQ-9 total score"
    }]
  },
  "subject": {
    "reference": "Patient/patient-456"
  },
  "encounter": {
    "reference": "Encounter/encounter-789"
  },
  "effectiveDateTime": "2024-01-15T10:30:00Z",
  "issued": "2024-01-15T10:35:00Z",
  "performer": [{
    "reference": "Practitioner/practitioner-1"
  }],
  "valueQuantity": {
    "value": 18,
    "unit": "score",
    "system": "http://unitsofmeasure.org",
    "code": "{score}"
  },
  "interpretation": [{
    "coding": [{
      "system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation",
      "code": "H",
      "display": "High"
    }],
    "text": "Matig-ernstige depressie"
  }]
}

Acceptance:

  • /api/fhir/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:

// app/api/docs/route.ts
import { generateOpenAPISpec } from '@/lib/openapi';

export async function GET() {
  const spec = {
    openapi: '3.0.0',
    info: {
      title: 'Mini-EPD FHIR API',
      version: '1.0.0',
      description: 'FHIR R4 compliant API voor GGZ data-uitwisseling'
    },
    servers: [{
      url: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000',
      description: 'Development server'
    }],
    paths: {
      '/api/fhir/Patient': {
        get: {
          summary: 'Search patients',
          tags: ['Patient'],
          responses: {
            '200': {
              description: 'FHIR Bundle met Patient resources',
              content: {
                'application/fhir+json': {
                  schema: { $ref: '#/components/schemas/PatientBundle' }
                }
              }
            }
          }
        },
        post: {
          summary: 'Create patient',
          tags: ['Patient'],
          requestBody: {
            content: {
              'application/fhir+json': {
                schema: { $ref: '#/components/schemas/Patient' }
              }
            }
          },
          responses: {
            '201': { description: 'Patient created' }
          }
        }
      },
      '/api/fhir/CarePlan/{id}': {
        get: {
          summary: 'Get CarePlan by ID',
          tags: ['CarePlan'],
          parameters: [{
            name: 'id',
            in: 'path',
            required: true,
            schema: { type: 'string' }
          }],
          responses: {
            '200': {
              description: 'FHIR CarePlan resource',
              content: {
                'application/fhir+json': {
                  schema: { $ref: '#/components/schemas/CarePlan' }
                }
              }
            }
          }
        }
      }
      // ... alle andere endpoints
    },
    components: {
      schemas: {
        Patient: { /* FHIR Patient schema */ },
        CarePlan: { /* FHIR CarePlan schema */ }
      }
    }
  };

  return Response.json(spec);
}

Demo Scenario:

# Demo Scenario: Data Uitwisselbaarheid

## Stap 1: Patient aanmaken
1. Open UI: `/patients/new`
2. Vul in: Jan de Vries, 1980-05-15, man
3. Opslaan → Patient ID: `patient-123`

## Stap 2: Intake registreren
1. Open: `/patients/patient-123/encounters/new`
2. Type: Intakegesprek
3. Notitie toevoegen via intake_notes
4. Opslaan → Encounter ID: `encounter-456`

## Stap 3: Diagnose toevoegen
1. Open: `/patients/patient-123/conditions/new`
2. Zoek: F32.2 (Depressieve episode)
3. Ernst: Ernstig, Status: Active
4. Opslaan → Condition ID: `condition-789`

## Stap 4: ROM-meting
1. Open: `/patients/patient-123/observations/new`
2. Instrument: PHQ-9
3. Invullen → Score: 18
4. Opslaan → Observation ID: `obs-123`

## Stap 5: Behandelplan opstellen
1. Open: `/patients/patient-123/care-plans/new`
2. Wizard:
   - Diagnose: F32.2 selecteren
   - Doel: "PHQ-9 < 10 binnen 12 weken"
   - Interventie: "CGT 1x/week, 12 sessies"
3. Opslaan → CarePlan ID: `careplan-abc`

## Stap 6: API Export (DEMO!)
```bash
# Export CarePlan als FHIR JSON
curl http://localhost:3000/api/fhir/CarePlan/careplan-abc \
  -H "Accept: application/fhir+json" \
  > careplan-export.json

# Toon in Postman of browser
cat careplan-export.json | jq

Stap 7: API Import (DEMO!)

# Edit careplan-export.json (verander status naar "on-hold")
# Import in nieuw systeem
curl -X POST http://andere-epd.com/api/fhir/CarePlan \
  -H "Content-Type: application/fhir+json" \
  -d @careplan-export.json

# → Success! Behandelplan geïmporteerd in ander EPD

Result: Data-uitwisselbaarheid aangetoond


**Acceptance:**
- ✅ Swagger UI beschikbaar op `/api/docs`
- ✅ Alle FHIR endpoints gedocumenteerd
- ✅ FHIR validatie werkt (optional, strikte mode)
- ✅ Demo scenario compleet doorlopen zonder errors
- ✅ Export/Import test succesvol met externe tool

---

## 5. Kwaliteit & Testplan

### Test Types

| Test Type | Scope | Tools | Coverage Target |
|-----------|-------|-------|-----------------|
| Unit Tests | FHIR transforms, utilities | Vitest | 80%+ voor `/lib/fhir` |
| Integration Tests | API endpoints | Vitest + Supertest | 100% FHIR endpoints |
| FHIR Validation | FHIR JSON output | @hapi/fhir validator | All resources valid |
| Manual Testing | UI flows + API export/import | Manual checklist | 5 critical flows |
| Performance | API response times | Lighthouse, k6 | < 500ms p95 |

### FHIR Compliance Testing

**Validator Setup:**
```bash
npm install @hapi/fhir-validator
// tests/fhir-validation.test.ts
import { Validator } from '@hapi/fhir-validator';

const validator = new Validator();

test('CarePlan output is valid FHIR R4', async () => {
  const carePlan = await fetch('/api/fhir/CarePlan/test-123')
    .then(r => r.json());

  const result = validator.validate(carePlan, {
    resourceType: 'CarePlan',
    version: 'R4'
  });

  expect(result.valid).toBe(true);
  expect(result.errors).toEqual([]);
});

Manual Test Checklist

FHIR API Tests:

  • GET /api/fhir/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)

-- Voeg FHIR tabellen toe (NIET vervangen)
-- Behoud: clients, intake_notes, treatment_plans, ai_events

-- Nieuwe tabellen:
CREATE TABLE practitioners (...);
CREATE TABLE organizations (...);
CREATE TABLE patients (...);  -- Naast clients
CREATE TABLE encounters (...);
CREATE TABLE conditions (...);
CREATE TABLE observations (...);
CREATE TABLE care_plans (...);  -- Naast treatment_plans

Fase 2: Data Migratie

# Run migratie script
pnpm run migrate:to-fhir

# Output:
✅ Migrated 3 clients → 3 patients
✅ Migrated 0 treatment_plans → 0 care_plans
✅ Created 1 default organization
✅ Created 2 demo practitioners

Fase 3: Legacy Tabellen (Read-Only)

-- RLS policies aanpassen: clients/treatment_plans read-only
CREATE POLICY "Legacy: Read only" ON clients
  FOR SELECT USING (auth.role() = 'authenticated');

-- Geen INSERT/UPDATE/DELETE policies

Fase 4: UI Cutover

// Feature flag in code
const USE_FHIR_SCHEMA = process.env.NEXT_PUBLIC_USE_FHIR === 'true';

// Gradual rollout
if (USE_FHIR_SCHEMA) {
  // Gebruik patients tabel
  const patient = await supabase.from('patients').select('*');
} else {
  // Fallback naar clients tabel
  const client = await supabase.from('clients').select('*');
}

Deployment Strategie

Vercel Deployment:

# Preview deployment (test)
vercel deploy --preview

# Production deployment
vercel deploy --prod

Environment Variables (Vercel):

NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxx...
SUPABASE_SERVICE_ROLE_KEY=eyJxxx...
NEXT_PUBLIC_USE_FHIR=true
NEXT_PUBLIC_API_URL=https://mini-epd.vercel.app

7. Risico's & Mitigatie

Risico Kans Impact Mitigatie Owner
FHIR mapping errors Hoog Hoog Unit tests voor alle transforms, FHIR validator Developer
Data migratie data loss Middel Kritiek Backup voor migratie, rollback script, test eerst op staging Developer
API performance issues Middel Middel Database indexes, query optimization, pagination Developer
FHIR compliance gaps Middel Middel Strikte validator tests, documenteer deviaties Developer
Scope creep (extra features) Hoog Middel Strikte MVP scope, "nice to have" backlog PM
Browser compatibility Laag Laag Test op Chrome/Firefox/Safari Developer
Supabase rate limits Laag Middel Monitor usage, optimize queries DevOps

Kritieke Risico's:

  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 Documentatiedocs/datamodel-documentatie.md
  • Volledig FHIR Schemadocs/archive/schemas/20241121_fhir_ggz_schema.sql (archived, gebruiken we deels)
  • Bouwplan Templatedocs/templates/bouwplan_template.md
  • Origineel Bouwplandocs/bouwplan-mini-epd.md (volledig, 13 resources)

FHIR & Healthcare Standards

FHIR Specificaties:

Nederlandse Standaarden:

Tools:

Technical Stack


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