|---------------------|---------------|---------------------| | E0: Foundation | ✅ Afgerond | Types + DB migratie | | E1: Leefgebieden | ✅ Afgerond | 3 componenten | | E2: AI Generatie | ⏳ Nog te doen | - | | E3: Behandelplan UI | ⏳ Nog te doen | - | Gemaakte bestanden: - lib/types/leefgebieden.ts - 7 domeinen met kleuren/emoji's - lib/types/behandelplan.ts - SMART doelen, interventies, Zod schemas - components/behandelplan/leefgebieden-form.tsx - Intake formulier - components/behandelplan/leefgebieden-scores.tsx - Score weergave - components/behandelplan/leefgebieden-badge.tsx - Domain badges - components/behandelplan/index.ts - Exports
24 KiB
Technisch Ontwerp (TO) — Behandelplan Module
Projectnaam: Mini-EPD Prototype - AI Speedrun Versie: v1.0 Datum: 03-12-2024 Auteur: Colin Lit
1. Doel en relatie met PRD en FO
Doel van dit document: Dit Technisch Ontwerp beschrijft hoe de Behandelplan module technisch wordt gebouwd. Het PRD beschrijft het wat, het FO het hoe functioneel, en dit TO de technische implementatie.
Gerelateerde documenten:
- PRD:
prd-behandelplan-v2-final.md - FO:
fo-behandelplan-v1.md - Implementatieplan:
~/.claude/plans/effervescent-toasting-beaver.md
Scope:
- Foundation first: Types → Database → Components → AI → UI
- Simple JSON API (geen streaming)
- Simpele leefgebieden visualisatie (progress bars, geen radar chart)
2. Technische Architectuur Overzicht
┌─────────────────────────────────────────────────────────────────────────────┐
│ FRONTEND (Next.js 14) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────────┐ │
│ │ Behandelplan │ │ Leefgebieden │ │ SMART Doelen │ │
│ │ Page │ │ Components │ │ Components │ │
│ │ (Server Comp) │ │ (Client Comp) │ │ (Client Comp) │ │
│ └────────┬────────┘ └────────┬────────┘ └──────────────┬──────────────┘ │
│ │ │ │ │
│ └────────────────────┴──────────────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ Server Actions │ │
│ │ (behandelplan/ │ │
│ │ actions.ts) │ │
│ └──────────┬──────────┘ │
└────────────────────────────────────┼────────────────────────────────────────┘
│
┌────────────────────────────────────┼────────────────────────────────────────┐
│ API ROUTES │
│ ┌─────────────────────────────────▼─────────────────────────────────────┐ │
│ │ /api/behandelplan/generate │ │
│ │ (POST - AI Generation) │ │
│ └─────────────────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────▼─────────────────────────────────────┐ │
│ │ /api/behandelplan/regenerate-section │ │
│ │ (POST - Micro-regeneration) │ │
│ └─────────────────────────────────┬─────────────────────────────────────┘ │
└────────────────────────────────────┼────────────────────────────────────────┘
│
┌────────────────────────────────────┼────────────────────────────────────────┐
│ EXTERNAL SERVICES │
│ ┌─────────────────┐ │ ┌─────────────────────┐ │
│ │ Supabase │◄─────────────┴──────────────► Claude API │ │
│ │ (PostgreSQL) │ │ (Anthropic) │ │
│ │ - care_plans │ │ - claude-sonnet │ │
│ │ - patients │ │ - JSON response │ │
│ │ - intakes │ │ │ │
│ └─────────────────┘ └─────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘
3. Techstack Selectie
Bestaande Stack (hergebruiken)
| Component | Technologie | Status | Argumentatie |
|---|---|---|---|
| Frontend | Next.js 14.2.18 | ✅ Bestaand | React framework, SSR, App Router |
| Backend | Next.js API Routes | ✅ Bestaand | Co-located, TypeScript |
| Database | Supabase PostgreSQL | ✅ Bestaand | RLS, FHIR-compliant schema |
| AI | Claude Sonnet | ✅ Bestaand | API key geconfigureerd |
| Styling | TailwindCSS 3.4 | ✅ Bestaand | Utility-first, shadcn/ui |
| Icons | Lucide React | ✅ Bestaand | Consistent icon set |
| Editor | TipTap | ✅ Bestaand | Rich text editor |
| Validation | Zod | ✅ Bestaand | Schema validation |
Nieuwe Dependencies
| Component | Technologie | Nodig voor | Alternatief |
|---|---|---|---|
| Charts | Recharts 2.x | Radar chart (stretch) | ❌ Later toevoegen |
Conclusie: Geen nieuwe dependencies nodig voor MVP. Recharts alleen bij stretch goal.
4. Datamodel
4.1 Bestaande Tabellen (hergebruiken)
-- FHIR CarePlan (hoofdtabel voor behandelplannen)
care_plans (
id UUID PRIMARY KEY,
patient_id UUID REFERENCES patients(id),
title TEXT,
status careplan_status, -- draft | active | completed | revoked
intent TEXT,
goals JSONB, -- Array van doelen
activities JSONB, -- Array van interventies
based_on_intake_id UUID,
based_on_anamneses UUID[],
based_on_examinations UUID[],
based_on_risk_assessments UUID[],
care_team_ids UUID[],
author_id UUID,
period_start DATE,
period_end DATE,
created_at TIMESTAMP,
updated_at TIMESTAMP
)
-- Conditions (diagnoses - input voor AI)
conditions (
id UUID,
patient_id UUID,
code TEXT, -- DSM-5 code
code_system TEXT,
display_text TEXT,
category TEXT,
clinical_status TEXT,
severity TEXT, -- laag | middel | hoog
encounter_id UUID
)
-- Intakes (bron voor AI context)
intakes (
id UUID,
patient_id UUID,
status intake_status,
treatment_advice JSONB,
kindcheck_data JSONB,
notes TEXT
)
4.2 Nieuwe Velden / Migratie
-- Migratie: Leefgebieden toevoegen aan intakes
ALTER TABLE intakes
ADD COLUMN life_domains JSONB;
-- Migratie: Behandelplan specifieke velden aan care_plans
ALTER TABLE care_plans
ADD COLUMN version INTEGER DEFAULT 1,
ADD COLUMN published_at TIMESTAMP,
ADD COLUMN behandelstructuur JSONB,
ADD COLUMN evaluatiemomenten JSONB,
ADD COLUMN veiligheidsplan JSONB;
-- Constraint voor versie-beheer
ALTER TABLE care_plans
ADD CONSTRAINT unique_patient_version UNIQUE (patient_id, version);
4.3 JSONB Structuren
life_domains (in intakes):
interface LifeDomainScore {
domain: 'dlv' | 'wonen' | 'werk' | 'sociaal' | 'vrijetijd' | 'financien' | 'gezondheid'
baseline: number // 1-5
current: number // 1-5
target: number // 1-5
notes: string
priority: 'laag' | 'middel' | 'hoog'
}
// life_domains: LifeDomainScore[]
goals (in care_plans):
interface SmartGoal {
id: string
title: string
description: string
clientVersion: string // B1-taal
lifeDomain: LifeDomain
priority: 'hoog' | 'middel' | 'laag'
measurability: string
timelineWeeks: number
status: 'niet_gestart' | 'bezig' | 'gehaald' | 'bijgesteld'
progress: number // 0-100
}
activities (in care_plans):
interface Intervention {
id: string
name: string
description: string
rationale: string
linkedGoalIds: string[]
}
behandelstructuur:
interface Behandelstructuur {
duur: string // "8 weken"
frequentie: string // "Wekelijks"
aantalSessies: number // 8
vorm: string // "Individueel"
}
evaluatiemomenten:
interface Evaluatiemoment {
id: string
type: 'tussentijds' | 'eind' | 'crisis'
weekNumber: number
plannedDate: string
actualDate?: string
status: 'gepland' | 'afgerond' | 'overgeslagen'
outcome?: string
lifeDomainUpdates?: LifeDomainScore[]
}
4.4 ERD
patients ─1:N─ intakes ─1:1─ life_domains (JSONB)
│ │
│ └────────── anamneses ─────────┐
│ └────────── examinations ──────┤
│ └────────── risk_assessments ──┤
│ │
└─1:N─ care_plans ────────────────────────────┘
│ (based_on_*)
├── goals (JSONB)
├── activities (JSONB)
├── behandelstructuur (JSONB)
├── evaluatiemomenten (JSONB)
└── veiligheidsplan (JSONB)
└─1:N─ conditions (diagnoses - input voor AI)
5. API Ontwerp
5.1 Endpoints Overzicht
| Endpoint | Method | Input | Output | Auth |
|---|---|---|---|---|
/api/behandelplan/generate |
POST | GenerateInput | GeneratedPlan | Required |
/api/behandelplan/regenerate-section |
POST | RegenerateInput | RegeneratedSection | Required |
5.2 POST /api/behandelplan/generate
Request:
interface GenerateInput {
patientId: string // UUID
intakeId: string // UUID
conditionId?: string // UUID (optioneel, haalt anders laatste op)
extraInstructions?: string // Optionele aanvullende instructies
}
Response:
interface GeneratedPlan {
behandelstructuur: Behandelstructuur
doelen: SmartGoal[]
interventies: Intervention[]
evaluatiemomenten: Evaluatiemoment[]
veiligheidsplan?: Veiligheidsplan // Alleen bij severity "Hoog"
}
Error Responses:
400: Validation error (missing fields, invalid UUIDs)401: Unauthorized404: Patient/Intake/Condition not found422: Insufficient data for generation (no intake notes, no diagnosis)500: AI API error503: AI service unavailable
5.3 POST /api/behandelplan/regenerate-section
Request:
interface RegenerateInput {
patientId: string
carePlanId: string
sectionType: 'goal' | 'intervention'
sectionId: string
instruction?: string // Extra instructie voor AI
currentPlan: GeneratedPlan // Context van huidige plan
}
Response:
interface RegeneratedSection {
type: 'goal' | 'intervention'
original: SmartGoal | Intervention
regenerated: SmartGoal | Intervention
}
5.4 Zod Validation Schemas
// lib/types/behandelplan.ts
export const GenerateInputSchema = z.object({
patientId: z.string().uuid(),
intakeId: z.string().uuid(),
conditionId: z.string().uuid().optional(),
extraInstructions: z.string().max(500).optional(),
});
export const RegenerateInputSchema = z.object({
patientId: z.string().uuid(),
carePlanId: z.string().uuid(),
sectionType: z.enum(['goal', 'intervention']),
sectionId: z.string().uuid(),
instruction: z.string().max(200).optional(),
currentPlan: GeneratedPlanSchema,
});
6. Security & Compliance
6.1 Security Checklist
- Authentication: Supabase Auth (bestaand)
- Authorization: Row Level Security op care_plans
- Data Encryption: At rest (PostgreSQL), in transit (HTTPS)
- Input Validation: Zod schemas op alle endpoints
- Rate Limiting: Toe te voegen op AI endpoints (10 req/min)
- CORS: Restrictive origins (bestaand)
- Secrets: Environment variables (ANTHROPIC_API_KEY)
6.2 RLS Policies voor care_plans
-- Bestaande RLS policy uitbreiden
ALTER TABLE care_plans ENABLE ROW LEVEL SECURITY;
-- Behandelaars kunnen alleen eigen patiënten zien
CREATE POLICY "Users can view care plans for their patients"
ON care_plans FOR SELECT
USING (
auth.uid() IN (
SELECT practitioner_id FROM patient_practitioners
WHERE patient_id = care_plans.patient_id
)
);
-- Behandelaars kunnen care plans maken voor eigen patiënten
CREATE POLICY "Users can create care plans for their patients"
ON care_plans FOR INSERT
WITH CHECK (
auth.uid() = author_id
);
-- Behandelaars kunnen eigen care plans updaten
CREATE POLICY "Users can update their care plans"
ON care_plans FOR UPDATE
USING (auth.uid() = author_id);
6.3 AVG/GDPR Overwegingen
- Data minimalisatie: Alleen noodzakelijke velden in AI prompt
- Geen BSN/identificerende data naar AI: Alleen intake notities en scores
- Audit trail: Bestaande
ai_eventstabel loggen van AI calls - Consent: AI-gebruik gedekt onder behandelrelatie
7. AI/LLM Integratie
7.1 AI Stack
| Component | Waarde |
|---|---|
| Provider | Anthropic |
| Model | claude-3-5-sonnet-20240620 (of claude-sonnet-4) |
| Library | Native fetch (geen SDK nodig) |
| Caching | Geen (elke generatie is uniek) |
| Fallback | Error message + manual mode optie |
7.2 Prompt Template
// lib/ai/behandelplan-prompt.ts
export const BEHANDELPLAN_SYSTEM_PROMPT = `
Je bent een ervaren GGZ-behandelaar die behandelplannen opstelt.
Je maakt SMART doelen die recovery-gericht en evidence-based zijn.
INSTRUCTIES:
1. Genereer 2-4 SMART doelen gebaseerd op de intake en diagnose
2. Focus op leefgebieden met prioriteit "Hoog"
3. Verdeel doelen over minimaal 2 verschillende leefgebieden
4. Maak concrete, meetbare doelen (geen vage termen)
5. Genereer voor elk doel een B1-taal versie (cliënt-vriendelijk)
6. Kies evidence-based interventies passend bij de DSM-categorie
7. Plan 8-12 sessies afhankelijk van severity
8. Voeg veiligheidsplan toe alleen bij severity "Hoog"
OUTPUT FORMAT:
Retourneer ALLEEN valide JSON volgens het volgende schema:
{
"behandelstructuur": {
"duur": "8 weken",
"frequentie": "Wekelijks",
"aantalSessies": 8,
"vorm": "Individueel"
},
"doelen": [...],
"interventies": [...],
"evaluatiemomenten": [...],
"veiligheidsplan": null | {...}
}
`;
export function buildUserPrompt(context: PlanContext): string {
return `
CLIËNT CONTEXT:
- Intake notities: ${context.intakeNotes}
- DSM-categorie: ${context.dsmCategory}
- Severity: ${context.severity}
LEEFGEBIEDEN SCORES:
${context.lifeDomains.map(d =>
`- ${d.domain}: ${d.baseline}/5 (prioriteit: ${d.priority})`
).join('\n')}
${context.extraInstructions ? `EXTRA INSTRUCTIES:\n${context.extraInstructions}` : ''}
Genereer nu een behandelplan.
`;
}
7.3 Evidence-Based Mapping
// lib/ai/intervention-mapping.ts
export const INTERVENTION_MAPPING: Record<string, InterventionSuggestion[]> = {
'angststoornissen': [
{ name: 'CGT', sessions: { laag: 8, middel: 10, hoog: 14 } },
{ name: 'Exposure therapie', sessions: { laag: 6, middel: 8, hoog: 12 } },
{ name: 'ACT', sessions: { laag: 8, middel: 10, hoog: 12 } },
],
'stemmingsklachten': [
{ name: 'CGT', sessions: { laag: 8, middel: 10, hoog: 14 } },
{ name: 'IPT', sessions: { laag: 8, middel: 12, hoog: 16 } },
{ name: 'Gedragsactivatie', sessions: { laag: 6, middel: 8, hoog: 10 } },
],
'trauma_ptss': [
{ name: 'EMDR', sessions: { laag: 6, middel: 10, hoog: 16 } },
{ name: 'Narratieve therapie', sessions: { laag: 8, middel: 12, hoog: 16 } },
],
'persoonlijkheid': [
{ name: 'Schematherapie', sessions: { laag: 16, middel: 24, hoog: 40 } },
{ name: 'MBT', sessions: { laag: 16, middel: 24, hoog: 40 } },
],
};
7.4 API Call Implementatie
// app/api/behandelplan/generate/route.ts
export async function POST(request: NextRequest) {
// 1. Validate input
const body = await request.json();
const input = GenerateInputSchema.parse(body);
// 2. Load context from database
const context = await loadPlanContext(input);
// 3. Build prompt
const messages = [
{ role: 'system', content: BEHANDELPLAN_SYSTEM_PROMPT },
{ role: 'user', content: buildUserPrompt(context) },
];
// 4. Call Claude API
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ANTHROPIC_API_KEY!,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20240620',
max_tokens: 4096,
temperature: 0.3,
messages,
}),
});
// 5. Parse and validate response
const result = await response.json();
const plan = GeneratedPlanSchema.parse(
JSON.parse(result.content[0].text)
);
// 6. Log to ai_events
await logAIEvent('behandelplan_generate', input, plan);
return NextResponse.json(plan);
}
8. Performance & Scalability
8.1 Performance Targets
| Metric | Target | Huidige Baseline |
|---|---|---|
| Page load (FCP) | < 1.5s | ~1s (andere pagina's) |
| API response | < 500ms | ~300ms (intakes) |
| AI generation | < 8s | N/A (nieuw) |
| Auto-save | < 500ms | ~300ms (reports) |
8.2 Optimalisaties
Frontend:
- Server Components voor initial load (geen client JS voor data)
- Skeleton loaders tijdens AI generatie
- Optimistic updates voor status wijzigingen
Backend:
- Parallel database queries voor context loading
- Geen caching van AI responses (elke generatie uniek)
- Connection pooling via Supabase (bestaand)
AI:
- Max tokens: 4096 (voldoende voor plan JSON)
- Temperature: 0.3 (consistent maar niet robotisch)
- Retry logic: 2x met exponential backoff
9. Deployment & CI/CD
9.1 Omgevingen (bestaand)
| Omgeving | URL | Database |
|---|---|---|
| Development | localhost:3000 | Local Supabase |
| Preview | Vercel preview | Supabase preview branch |
| Production | [main domain] | Supabase production |
9.2 Migratie Workflow (Cloud-only)
# Geen lokale Supabase instantie - direct naar cloud
# Optie 1: Via Supabase MCP tool
mcp__supabase__apply_migration(name, query)
# Optie 2: Via Supabase CLI
npx supabase db push --linked
# Types genereren na migratie
mcp__supabase__generate_typescript_types
Let op: Geen supabase db reset mogelijk - migraties zijn direct productie.
9.3 Deployment Checklist
- Environment variables in Vercel dashboard
- Database migraties toegepast
- TypeScript types gegenereerd (
supabase gen types typescript) - Build succesvol (
pnpm build) - Smoke test op preview environment
10. Monitoring & Logging
10.1 AI Event Logging (bestaand)
-- Bestaande ai_events tabel
ai_events (
id UUID,
kind TEXT, -- 'behandelplan_generate' | 'behandelplan_regenerate'
request JSONB, -- Input parameters
response JSONB, -- Generated plan
duration_ms INTEGER,
created_at TIMESTAMP
)
10.2 Metrics te Tracken
| Metric | Doel | Actie bij Overschrijding |
|---|---|---|
| AI success rate | > 95% | Check prompts, input validation |
| AI response time p95 | < 8s | Optimize prompt size |
| Error rate | < 2% | Alert + investigate |
11. Risico's & Technische Mitigatie
| Risico | Impact | Kans | Mitigatie |
|---|---|---|---|
| AI genereert invalide JSON | Hoog | Middel | Zod validation, retry logic, fallback |
| AI API down/rate limited | Hoog | Laag | Error message, manual mode optie |
| Grote intake teksten (token limit) | Middel | Middel | Truncate/summarize intake eerst |
| Inconsistente B1-taal kwaliteit | Middel | Middel | Post-processing, behandelaar review |
| Performance bij grote plannen | Laag | Laag | Pagination, lazy loading |
12. Implementatie Volgorde
Stap 1: Types & Database (2-3 uur)
Bestanden:
lib/types/
├── behandelplan.ts # Nieuwe types + Zod schemas
└── leefgebieden.ts # Life domain types
supabase/migrations/
└── xxx_add_behandelplan_fields.sql
Stap 2: Leefgebieden Componenten (3-4 uur)
Bestanden:
components/behandelplan/
├── leefgebieden-form.tsx # Intake formulier (7 sliders)
├── leefgebieden-scores.tsx # Progress bar weergave
└── leefgebieden-badge.tsx # Domain tag/badge
Stap 3: AI Generatie (3-4 uur)
Bestanden:
lib/ai/
├── behandelplan-prompt.ts # System + user prompts
└── intervention-mapping.ts # Evidence-based mapping
app/api/behandelplan/
├── generate/route.ts # POST endpoint
└── regenerate-section/route.ts # Micro-regeneratie
Stap 4: Behandelplan UI (6-8 uur)
Bestanden:
app/epd/patients/[id]/behandelplan/
├── page.tsx # Server component (vervang placeholder)
├── actions.ts # Server actions (CRUD)
└── components/
├── behandelplan-view.tsx
├── goals-section.tsx
├── goal-card.tsx
├── interventions-section.tsx
└── generate-button.tsx
13. Bijlagen & Referenties
Projectdocumenten
Tech Documentatie
- Next.js: https://nextjs.org/docs
- Supabase: https://supabase.com/docs
- Anthropic Claude: https://docs.anthropic.com/claude/reference
Bestaande Code Referenties
- API pattern:
/app/api/reports/classify/route.ts - Server actions:
/app/epd/patients/[id]/intakes/[intakeId]/actions.ts - Types pattern:
/lib/types/report.ts - AI integration:
/app/api/docs/chat/route.ts
Document Status: v1.0 Draft Volgende Review: Na implementatie Stap 1-2 Eigenaar: Colin van Zeeland