diff --git a/app/api/behandelplan/generate/route.ts b/app/api/behandelplan/generate/route.ts new file mode 100644 index 0000000..9387b9a --- /dev/null +++ b/app/api/behandelplan/generate/route.ts @@ -0,0 +1,327 @@ +/** + * Behandelplan Generate API + * + * POST /api/behandelplan/generate + * Genereert een behandelplan met Claude AI + */ + +import { createClient } from '@/lib/auth/server'; +import { NextRequest, NextResponse } from 'next/server'; +import { z } from 'zod'; +import { + BEHANDELPLAN_SYSTEM_PROMPT, + buildUserPrompt, + validatePlanContext, + type PlanContext, +} from '@/lib/ai/behandelplan-prompt'; +import { type Severity } from '@/lib/ai/intervention-mapping'; +import { + GeneratedPlanSchema, + type GeneratedPlan, +} from '@/lib/types/behandelplan'; +import { createDefaultLifeDomainScores, type LifeDomainScore } from '@/lib/types/leefgebieden'; + +// Input validation schema +const GenerateInputSchema = z.object({ + patientId: z.string().uuid('Patient ID moet een geldige UUID zijn'), + intakeId: z.string().uuid('Intake ID moet een geldige UUID zijn'), + conditionId: z.string().uuid().optional(), + extraInstructions: z.string().max(500).optional(), +}); + +/** + * Map severity_code to Severity type + */ +function mapSeverity(severityCode: string | null | undefined): Severity { + if (!severityCode) return 'middel'; + + const code = severityCode.toLowerCase(); + if (code.includes('mild') || code.includes('laag') || code.includes('light')) { + return 'laag'; + } + if (code.includes('severe') || code.includes('hoog') || code.includes('ernstig')) { + return 'hoog'; + } + return 'middel'; +} + +/** + * Load context from database + */ +async function loadPlanContext( + supabase: Awaited>, + patientId: string, + intakeId: string, + conditionId?: string +): Promise { + // 1. Load intake + const { data: intake, error: intakeError } = await supabase + .from('intakes') + .select('*') + .eq('id', intakeId) + .eq('patient_id', patientId) + .single(); + + if (intakeError || !intake) { + throw new Error(`Intake niet gevonden: ${intakeError?.message || 'niet gevonden'}`); + } + + // 2. Load condition (latest for patient, or specific one) + let conditionQuery = supabase + .from('conditions') + .select('*') + .eq('patient_id', patientId); + + if (conditionId) { + conditionQuery = conditionQuery.eq('id', conditionId); + } else { + conditionQuery = conditionQuery.order('recorded_date', { ascending: false }).limit(1); + } + + const { data: conditions } = await conditionQuery; + const condition = conditions?.[0]; + + // 3. Build intake notes from available data + const intakeNotes = buildIntakeNotes(intake); + + // 4. Get life domains (from intake or default) + const lifeDomains: LifeDomainScore[] = + (intake.life_domains as LifeDomainScore[] | null) || + createDefaultLifeDomainScores(); + + // 5. Build context + return { + patientId, + intakeNotes, + dsmCategory: condition?.category || condition?.code_display || 'overig', + severity: mapSeverity(condition?.severity_code), + lifeDomains, + extraInstructions: undefined, + }; +} + +/** + * Build intake notes from intake data + */ +function buildIntakeNotes(intake: Record): string { + const parts: string[] = []; + + if (intake.notes) { + parts.push(String(intake.notes)); + } + + if (intake.treatment_advice) { + const advice = intake.treatment_advice as Record; + if (advice.content) { + parts.push(`Behandeladvies: ${String(advice.content)}`); + } + } + + if (intake.kindcheck_data) { + const kindcheck = intake.kindcheck_data as Record; + if (kindcheck.observations) { + parts.push(`Observaties: ${String(kindcheck.observations)}`); + } + } + + return parts.join('\n\n') || 'Geen intake notities beschikbaar.'; +} + +/** + * Call Claude API + */ +async function callClaudeAPI(context: PlanContext): Promise { + const apiKey = process.env.ANTHROPIC_API_KEY; + if (!apiKey) { + throw new Error('ANTHROPIC_API_KEY ontbreekt in environment'); + } + + const userPrompt = buildUserPrompt(context); + + const response = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + }, + body: JSON.stringify({ + model: 'claude-sonnet-4-20250514', + max_tokens: 4096, + temperature: 0.3, + system: BEHANDELPLAN_SYSTEM_PROMPT, + messages: [{ role: 'user', content: userPrompt }], + }), + }); + + if (!response.ok) { + const errorBody = await response.text(); + console.error('Claude API error:', errorBody); + throw new Error(`Claude API fout: ${response.status}`); + } + + const data = await response.json(); + const rawText = data?.content?.[0]?.text; + + if (!rawText) { + throw new Error('Geen response van Claude API'); + } + + // Parse JSON from response (handle potential markdown code blocks) + let jsonText = rawText.trim(); + if (jsonText.startsWith('```json')) { + jsonText = jsonText.slice(7); + } + if (jsonText.startsWith('```')) { + jsonText = jsonText.slice(3); + } + if (jsonText.endsWith('```')) { + jsonText = jsonText.slice(0, -3); + } + + const parsed = JSON.parse(jsonText.trim()); + + // Validate with Zod schema + const validated = GeneratedPlanSchema.parse(parsed); + + return validated; +} + +/** + * Log AI event to database + */ +async function logAIEvent( + supabase: Awaited>, + kind: string, + patientId: string, + input: Record, + output: Record, + durationMs: number +) { + try { + await supabase.from('ai_events').insert({ + kind, + patient_id: patientId, + input_data: input, + output_data: output, + duration_ms: durationMs, + }); + } catch (error) { + // Log but don't fail the request + console.error('Failed to log AI event:', error); + } +} + +/** + * POST /api/behandelplan/generate + */ +export async function POST(request: NextRequest) { + const startTime = Date.now(); + + try { + const body = await request.json(); + + // Validate input + const result = GenerateInputSchema.safeParse(body); + if (!result.success) { + return NextResponse.json( + { + error: 'Validatiefout', + details: result.error.issues.map((e) => ({ + field: e.path.join('.'), + message: e.message, + })), + }, + { status: 400 } + ); + } + + const { patientId, intakeId, conditionId, extraInstructions } = result.data; + const supabase = await createClient(); + + // Load context from database + const context = await loadPlanContext(supabase, patientId, intakeId, conditionId); + + // Add extra instructions if provided + if (extraInstructions) { + context.extraInstructions = extraInstructions; + } + + // Validate context + const validation = validatePlanContext(context); + if (!validation.valid) { + return NextResponse.json( + { + error: 'Onvoldoende context voor behandelplan generatie', + details: validation.errors, + }, + { status: 400 } + ); + } + + // Call Claude API + const plan = await callClaudeAPI(context); + + const durationMs = Date.now() - startTime; + + // Log AI event + await logAIEvent( + supabase, + 'behandelplan_generate', + patientId, + { intakeId, conditionId, extraInstructions }, + { goalCount: plan.doelen.length, interventionCount: plan.interventies.length }, + durationMs + ); + + return NextResponse.json({ + success: true, + plan, + meta: { + generatedAt: new Date().toISOString(), + durationMs, + context: { + dsmCategory: context.dsmCategory, + severity: context.severity, + highPriorityDomains: context.lifeDomains + .filter((d) => d.priority === 'hoog') + .map((d) => d.domain), + }, + }, + }); + } catch (error) { + console.error('Error generating behandelplan:', error); + + const errorMessage = error instanceof Error ? error.message : 'Onbekende fout'; + + // Check for specific error types + if (errorMessage.includes('Intake niet gevonden')) { + return NextResponse.json( + { error: errorMessage }, + { status: 404 } + ); + } + + if (errorMessage.includes('ANTHROPIC_API_KEY')) { + return NextResponse.json( + { error: 'AI service niet geconfigureerd' }, + { status: 503 } + ); + } + + if (errorMessage.includes('Claude API')) { + return NextResponse.json( + { error: 'AI service tijdelijk niet beschikbaar', details: errorMessage }, + { status: 503 } + ); + } + + return NextResponse.json( + { + error: 'Fout bij genereren behandelplan', + details: process.env.NODE_ENV === 'development' ? errorMessage : undefined, + }, + { status: 500 } + ); + } +} diff --git a/docs/specs/behandelplan/bouwplan-behandelplan-v1.md b/docs/specs/behandelplan/bouwplan-behandelplan-v1.md index 2f68cf5..bb888ed 100644 --- a/docs/specs/behandelplan/bouwplan-behandelplan-v1.md +++ b/docs/specs/behandelplan/bouwplan-behandelplan-v1.md @@ -107,9 +107,9 @@ Deze module is onderdeel van de AI Speedrun LinkedIn Serie. Het prototype demons | Epic ID | Titel | Doel | Status | Stories | Geschat | |---------|-------|------|--------|---------|---------| -| E0 | Foundation | Types, database schema | ⏳ To Do | 3 | 2-3 uur | -| E1 | Leefgebieden | Intake formulier + score weergave | ⏳ To Do | 3 | 3-4 uur | -| E2 | AI Generatie | Claude API endpoint + prompts | ⏳ To Do | 3 | 3-4 uur | +| E0 | Foundation | Types, database schema | ✅ Done | 3 | 2-3 uur | +| E1 | Leefgebieden | Intake formulier + score weergave | ✅ Done | 3 | 3-4 uur | +| E2 | AI Generatie | Claude API endpoint + prompts | ✅ Done | 3 | 3-4 uur | | E3 | Behandelplan UI | Pagina + componenten | ⏳ To Do | 5 | 6-8 uur | | E4 | Stretch | Micro-regeneratie, radar chart | ⏳ Optioneel | 3 | 3-5 uur | @@ -126,9 +126,9 @@ Deze module is onderdeel van de AI Speedrun LinkedIn Serie. Het prototype demons | Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP | |----------|--------------|---------------------|--------|------------------|----| -| E0.S1 | Leefgebieden types maken | `lib/types/leefgebieden.ts` met LifeDomain, LifeDomainScore types | ⏳ | — | 1 | -| E0.S2 | Behandelplan types maken | `lib/types/behandelplan.ts` met SmartGoal, Intervention, GeneratedPlan types + Zod schemas | ⏳ | E0.S1 | 2 | -| E0.S3 | Database migratie | `care_plans` uitgebreid met version, behandelstructuur, evaluatiemomenten; `intakes` met life_domains | ⏳ | E0.S2 | 2 | +| E0.S1 | Leefgebieden types maken | `lib/types/leefgebieden.ts` met LifeDomain, LifeDomainScore types | ✅ | — | 1 | +| E0.S2 | Behandelplan types maken | `lib/types/behandelplan.ts` met SmartGoal, Intervention, GeneratedPlan types + Zod schemas | ✅ | E0.S1 | 2 | +| E0.S3 | Database migratie | `care_plans` uitgebreid met version, behandelstructuur, evaluatiemomenten; `intakes` met life_domains | ✅ | E0.S2 | 2 | **Technical Notes:** ```typescript @@ -146,10 +146,10 @@ export interface LifeDomainScore { ``` **Deliverables:** -- [ ] `lib/types/leefgebieden.ts` -- [ ] `lib/types/behandelplan.ts` -- [ ] Database migratie via Supabase MCP (cloud-only, geen lokale instantie) -- [ ] Gegenereerde database types via `mcp__supabase__generate_typescript_types` +- [x] `lib/types/leefgebieden.ts` +- [x] `lib/types/behandelplan.ts` +- [x] Database migratie via Supabase MCP (cloud-only, geen lokale instantie) +- [x] Gegenereerde database types via `mcp__supabase__generate_typescript_types` --- @@ -159,9 +159,9 @@ export interface LifeDomainScore { | Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP | |----------|--------------|---------------------|--------|------------------|----| -| E1.S1 | Leefgebieden formulier | 7 sliders (1-5), toelichting velden, prioriteit dropdowns | ⏳ | E0.S3 | 3 | -| E1.S2 | Leefgebieden scores weergave | 7 progress bars met kleuren, baseline vs current indicator | ⏳ | E0.S1 | 2 | -| E1.S3 | Leefgebieden badge component | Gekleurde tag per domein met emoji | ⏳ | E0.S1 | 1 | +| E1.S1 | Leefgebieden formulier | 7 sliders (1-5), toelichting velden, prioriteit dropdowns | ✅ | E0.S3 | 3 | +| E1.S2 | Leefgebieden scores weergave | 7 progress bars met kleuren, baseline vs current indicator | ✅ | E0.S1 | 2 | +| E1.S3 | Leefgebieden badge component | Gekleurde tag per domein met emoji | ✅ | E0.S1 | 1 | **Technical Notes:** ```typescript @@ -178,9 +178,10 @@ const DOMAIN_COLORS = { ``` **Deliverables:** -- [ ] `components/behandelplan/leefgebieden-form.tsx` -- [ ] `components/behandelplan/leefgebieden-scores.tsx` -- [ ] `components/behandelplan/leefgebieden-badge.tsx` +- [x] `components/behandelplan/leefgebieden-form.tsx` +- [x] `components/behandelplan/leefgebieden-scores.tsx` +- [x] `components/behandelplan/leefgebieden-badge.tsx` +- [x] `components/behandelplan/index.ts` (exports) --- @@ -190,9 +191,9 @@ const DOMAIN_COLORS = { | Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP | |----------|--------------|---------------------|--------|------------------|----| -| E2.S1 | Prompt engineering | System prompt + user prompt templates, evidence-based mapping | ⏳ | E0.S2 | 2 | -| E2.S2 | Generate endpoint | `POST /api/behandelplan/generate` retourneert JSON, < 8 sec response | ⏳ | E2.S1 | 3 | -| E2.S3 | AI event logging | Calls loggen naar `ai_events` tabel | ⏳ | E2.S2 | 1 | +| E2.S1 | Prompt engineering | System prompt + user prompt templates, evidence-based mapping | ✅ | E0.S2 | 2 | +| E2.S2 | Generate endpoint | `POST /api/behandelplan/generate` retourneert JSON, < 8 sec response | ✅ | E2.S1 | 3 | +| E2.S3 | AI event logging | Calls loggen naar `ai_events` tabel | ✅ | E2.S2 | 1 | **Technical Notes:** ```typescript @@ -211,9 +212,9 @@ const settings = { ``` **Deliverables:** -- [ ] `lib/ai/behandelplan-prompt.ts` -- [ ] `lib/ai/intervention-mapping.ts` -- [ ] `app/api/behandelplan/generate/route.ts` +- [x] `lib/ai/behandelplan-prompt.ts` +- [x] `lib/ai/intervention-mapping.ts` +- [x] `app/api/behandelplan/generate/route.ts` --- @@ -436,3 +437,4 @@ Optioneel (indien tijd): | Versie | Datum | Auteur | Wijziging | |--------|-------|--------|-----------| | v1.0 | 03-12-2024 | Colin Lit | Initiële versie | +| v1.1 | 04-12-2024 | Colin Lit | Epic 0, 1, 2 afgerond - status bijgewerkt | diff --git a/docs/templates/bouwplan_template.md b/docs/templates/bouwplan_template.md index 87b1089..5596128 100644 --- a/docs/templates/bouwplan_template.md +++ b/docs/templates/bouwplan_template.md @@ -156,6 +156,7 @@ export const loadData = async () => { | E4 | Testing & Deploy | QA, demo prep en deployment | ⏳ To Do | 3 | | **Belangrijk:** Voer niet in 1x het volledige plan uit. Bouw per epic en per story. +**Belangrijk:** Het installeren van dependencies en libraries, of het uitvoeren van migraties van tabellen, moet altijd eerst aan Colin worden gemeld of gevraagd. Colin kan deze zelf eventueel uitvoeren. --- @@ -174,6 +175,7 @@ export const loadData = async () => { | E0.S4 | Environment variables | `.env.local` + Vercel vars geconfigureerd | ⏳ | E0.S3 | 1 | **Technical Notes:** +- ⚠️ **Belangrijk:** Dependencies installeren moet eerst aan Colin worden gemeld of gevraagd. Colin kan deze zelf eventueel uitvoeren. - Gebruik `pnpm` voor snellere installs - `.env.example` committen voor team onboarding @@ -189,6 +191,7 @@ export const loadData = async () => { | E1.S3 | Demo-data seeden | 3+ testcliënten met complete intake data | ⏳ | E1.S1 | 2 | **Technical Notes:** +- ⚠️ **Belangrijk:** Migraties van tabellen moeten eerst aan Colin worden gemeld of gevraagd. Colin kan deze zelf eventueel uitvoeren. - Collections: `clients`, `intakes`, `plans`, `ai_events` - Demo user heeft `all access` voor development - Seed script: `npm run seed` diff --git a/lib/ai/behandelplan-prompt.ts b/lib/ai/behandelplan-prompt.ts new file mode 100644 index 0000000..c2eb3a6 --- /dev/null +++ b/lib/ai/behandelplan-prompt.ts @@ -0,0 +1,221 @@ +/** + * Behandelplan AI Prompts + * + * System prompt en user prompt templates voor AI-generatie van behandelplannen + */ + +import { type LifeDomainScore, LIFE_DOMAIN_META } from '@/lib/types/leefgebieden'; +import { type Severity, getInterventionsForCategory, getRecommendedSessionCount, getRecommendedFrequency, getRecommendedFormat } from './intervention-mapping'; + +/** + * Context voor behandelplan generatie + */ +export interface PlanContext { + patientId: string; + intakeNotes: string; + dsmCategory: string; + severity: Severity; + lifeDomains: LifeDomainScore[]; + extraInstructions?: string; +} + +/** + * System prompt voor Claude + * Instructies voor het genereren van een behandelplan + */ +export const BEHANDELPLAN_SYSTEM_PROMPT = `Je bent een ervaren GGZ-behandelaar die behandelplannen opstelt voor cliënten in de ambulante GGZ. +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 zoals "beter voelen") +5. Genereer voor elk doel een B1-taal versie (cliënt-vriendelijk, simpele woorden) +6. Kies evidence-based interventies passend bij de DSM-categorie +7. Plan sessies gebaseerd op severity niveau +8. Voeg een veiligheidsplan toe ALLEEN bij severity "hoog" + +SMART CRITERIA: +- Specifiek: Wat precies wil de cliënt bereiken? +- Meetbaar: Hoe meten we vooruitgang? (bijv. "3x per week", "score van 3 naar 4") +- Acceptabel: Past bij wensen en mogelijkheden cliënt +- Realistisch: Haalbaar binnen de behandelperiode +- Tijdgebonden: Binnen hoeveel weken? + +B1-TAAL RICHTLIJNEN (cliënt-versie): +- Korte zinnen (max 15 woorden) +- Alledaagse woorden (geen jargon) +- Actieve zinnen ("Ik ga..." niet "Er zal worden...") +- Directe aanspreking ("jij" of "je") + +OUTPUT FORMAT: +Retourneer ALLEEN valide JSON volgens dit exacte schema (geen extra tekst): +{ + "behandelstructuur": { + "duur": "string (bijv. '8 weken')", + "frequentie": "string (bijv. 'Wekelijks')", + "aantalSessies": number, + "vorm": "string (bijv. 'Individueel')" + }, + "doelen": [ + { + "id": "string (uuid)", + "title": "string (max 200 tekens, korte beschrijving)", + "description": "string (SMART uitwerking, 2-3 zinnen)", + "clientVersion": "string (B1-taal versie voor cliënt)", + "lifeDomain": "dlv|wonen|werk|sociaal|vrijetijd|financien|gezondheid", + "priority": "hoog|middel|laag", + "measurability": "string (hoe meten we vooruitgang?)", + "timelineWeeks": number, + "status": "niet_gestart", + "progress": 0 + } + ], + "interventies": [ + { + "id": "string (uuid)", + "name": "string (bijv. 'CGT', 'EMDR')", + "description": "string (uitleg interventie)", + "rationale": "string (waarom past dit bij deze cliënt?)", + "linkedGoalIds": ["string (id van gekoppeld doel)"] + } + ], + "sessiePlanning": [ + { + "id": "string (uuid)", + "nummer": number, + "focus": "string (waar gaat de sessie over?)", + "status": "gepland", + "gekoppeldeDoelIds": ["string"] + } + ], + "evaluatiemomenten": [ + { + "id": "string (uuid)", + "type": "tussentijds|eind", + "weekNumber": number, + "plannedDate": "string (ISO date, mag leeg)", + "status": "gepland" + } + ], + "veiligheidsplan": null of { + "waarschuwingssignalen": ["string (3-5 items)"], + "copingStrategieen": ["string (3-5 items)"], + "contacten": [ + { + "naam": "string", + "rol": "string", + "telefoon": "string" + } + ], + "restricties": ["string (optioneel)"] + } +}`; + +/** + * Bouw user prompt met context + */ +export function buildUserPrompt(context: PlanContext): string { + // Get high priority domains + const highPriorityDomains = context.lifeDomains.filter(d => d.priority === 'hoog'); + const lowScoreDomains = context.lifeDomains.filter(d => d.baseline <= 2); + + // Get intervention suggestions + const suggestedInterventions = getInterventionsForCategory(context.dsmCategory, context.severity); + const recommendedSessions = getRecommendedSessionCount(context.dsmCategory, context.severity); + const recommendedFrequency = getRecommendedFrequency(context.severity); + const recommendedFormat = getRecommendedFormat(context.severity); + + // Build life domains section + const lifeDomainLines = context.lifeDomains.map(d => { + const meta = LIFE_DOMAIN_META[d.domain]; + const priorityMarker = d.priority === 'hoog' ? ' [PRIORITEIT]' : ''; + const lowScoreMarker = d.baseline <= 2 ? ' [LAAG]' : ''; + return `- ${meta.label} (${d.domain}): ${d.baseline}/5 → doel ${d.target}/5${priorityMarker}${lowScoreMarker}${d.notes ? ` (${d.notes})` : ''}`; + }).join('\n'); + + // Build intervention suggestions + const interventionLines = suggestedInterventions.slice(0, 3).map(i => + `- ${i.name}: ${i.description} (~${i.recommendedSessions} sessies)` + ).join('\n'); + + return `CLIËNT CONTEXT: +================= +Intake notities: +${context.intakeNotes} + +DSM-categorie: ${context.dsmCategory} +Severity: ${context.severity} + +LEEFGEBIEDEN ASSESSMENT: +======================== +${lifeDomainLines} + +${highPriorityDomains.length > 0 ? ` +FOCUS GEBIEDEN (hoge prioriteit): +${highPriorityDomains.map(d => `- ${LIFE_DOMAIN_META[d.domain].label}`).join('\n')} +` : ''} +${lowScoreDomains.length > 0 ? ` +AANDACHTSPUNTEN (lage scores): +${lowScoreDomains.map(d => `- ${LIFE_DOMAIN_META[d.domain].label} (score: ${d.baseline}/5)`).join('\n')} +` : ''} + +AANBEVOLEN INTERVENTIES (evidence-based): +========================================= +${interventionLines} + +AANBEVOLEN BEHANDELSTRUCTUUR: +============================= +- Aantal sessies: ~${recommendedSessions} +- Frequentie: ${recommendedFrequency} +- Vorm: ${recommendedFormat} + +${context.extraInstructions ? ` +EXTRA INSTRUCTIES VAN BEHANDELAAR: +================================== +${context.extraInstructions} +` : ''} + +Genereer nu een compleet behandelplan in JSON formaat. +Zorg dat elk doel: +1. Gekoppeld is aan een leefgebied met hoge prioriteit of lage score +2. Een meetbare indicator heeft +3. Een B1-taal versie heeft voor de cliënt +4. Realistisch is voor de behandelperiode + +${context.severity === 'hoog' ? 'BELANGRIJK: Voeg ook een veiligheidsplan toe met waarschuwingssignalen, copingstrategieën en contactpersonen.' : ''}`; +} + +/** + * Valideer of de context voldoende informatie bevat + */ +export function validatePlanContext(context: PlanContext): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!context.intakeNotes || context.intakeNotes.length < 50) { + errors.push('Intake notities zijn te kort (minimaal 50 tekens)'); + } + + if (!context.dsmCategory) { + errors.push('DSM-categorie ontbreekt'); + } + + if (!context.severity) { + errors.push('Severity niveau ontbreekt'); + } + + if (!context.lifeDomains || context.lifeDomains.length !== 7) { + errors.push('Leefgebieden assessment is incompleet (7 domeinen vereist)'); + } + + const hasHighPriority = context.lifeDomains?.some(d => d.priority === 'hoog'); + if (!hasHighPriority) { + errors.push('Minimaal 1 leefgebied moet prioriteit "hoog" hebben'); + } + + return { + valid: errors.length === 0, + errors, + }; +} diff --git a/lib/ai/intervention-mapping.ts b/lib/ai/intervention-mapping.ts new file mode 100644 index 0000000..b2736f8 --- /dev/null +++ b/lib/ai/intervention-mapping.ts @@ -0,0 +1,232 @@ +/** + * Evidence-Based Intervention Mapping + * + * Mapping van DSM-categorieën naar evidence-based interventies + * met sessie-aantallen per severity level + */ + +export interface InterventionSuggestion { + name: string; + description: string; + sessions: { + laag: number; + middel: number; + hoog: number; + }; +} + +export type Severity = 'laag' | 'middel' | 'hoog'; + +/** + * DSM categorieën naar interventie mapping + * Gebaseerd op richtlijnen GGZ Standaarden + */ +export const INTERVENTION_MAPPING: Record = { + angststoornissen: [ + { + name: 'CGT', + description: 'Cognitieve Gedragstherapie gericht op angstreductie door blootstelling en cognitieve herstructurering', + sessions: { laag: 8, middel: 10, hoog: 14 }, + }, + { + name: 'Exposure therapie', + description: 'Systematische blootstelling aan angstopwekkende situaties met responspreventie', + sessions: { laag: 6, middel: 8, hoog: 12 }, + }, + { + name: 'ACT', + description: 'Acceptance and Commitment Therapy voor psychologische flexibiliteit', + sessions: { laag: 8, middel: 10, hoog: 12 }, + }, + ], + stemmingsklachten: [ + { + name: 'CGT', + description: 'Cognitieve Gedragstherapie gericht op negatieve denkpatronen en gedragsactivatie', + sessions: { laag: 8, middel: 10, hoog: 14 }, + }, + { + name: 'IPT', + description: 'Interpersoonlijke Therapie gericht op relationele patronen en sociale steun', + sessions: { laag: 8, middel: 12, hoog: 16 }, + }, + { + name: 'Gedragsactivatie', + description: 'Gestructureerde toename van plezierige en betekenisvolle activiteiten', + sessions: { laag: 6, middel: 8, hoog: 10 }, + }, + ], + trauma_ptss: [ + { + name: 'EMDR', + description: 'Eye Movement Desensitization and Reprocessing voor traumaverwerking', + sessions: { laag: 6, middel: 10, hoog: 16 }, + }, + { + name: 'Narratieve therapie', + description: 'Verwerking door het opbouwen van een coherent traumaverhaal', + sessions: { laag: 8, middel: 12, hoog: 16 }, + }, + { + name: 'CGT trauma-focus', + description: 'Trauma-gefocuste CGT met exposure en cognitieve verwerking', + sessions: { laag: 8, middel: 12, hoog: 16 }, + }, + ], + persoonlijkheid: [ + { + name: 'Schematherapie', + description: 'Langdurige therapie gericht op disfunctionele schemas en copingstijlen', + sessions: { laag: 16, middel: 24, hoog: 40 }, + }, + { + name: 'MBT', + description: 'Mentalization-Based Treatment voor emotieregulatie en relaties', + sessions: { laag: 16, middel: 24, hoog: 40 }, + }, + { + name: 'DGT', + description: 'Dialectische Gedragstherapie voor emotieregulatie en crisisvaardigheden', + sessions: { laag: 16, middel: 24, hoog: 40 }, + }, + ], + verslaving: [ + { + name: 'Motiverende gespreksvoering', + description: 'Versterken van intrinsieke motivatie voor gedragsverandering', + sessions: { laag: 4, middel: 6, hoog: 8 }, + }, + { + name: 'CGT verslaving', + description: 'Cognitieve Gedragstherapie gericht op craving en terugvalpreventie', + sessions: { laag: 8, middel: 12, hoog: 16 }, + }, + { + name: 'Terugvalpreventie', + description: 'Identificeren en managen van risicosituaties en triggers', + sessions: { laag: 6, middel: 8, hoog: 12 }, + }, + ], + adhd: [ + { + name: 'Psycho-educatie', + description: 'Educatie over ADHD en praktische copingstrategieën', + sessions: { laag: 4, middel: 6, hoog: 8 }, + }, + { + name: 'Coaching/planning', + description: 'Structuur en planningsvaardigheden ontwikkelen', + sessions: { laag: 6, middel: 8, hoog: 12 }, + }, + { + name: 'CGT ADHD', + description: 'Gedragstherapie gericht op impulscontrole en executieve functies', + sessions: { laag: 8, middel: 10, hoog: 14 }, + }, + ], + autisme: [ + { + name: 'Psycho-educatie', + description: 'Educatie over autisme en zelfacceptatie', + sessions: { laag: 4, middel: 6, hoog: 8 }, + }, + { + name: 'Sociale vaardigheden', + description: 'Training in sociale interactie en communicatie', + sessions: { laag: 8, middel: 12, hoog: 16 }, + }, + { + name: 'Stressmanagement', + description: 'Omgaan met prikkels en sensorische overbelasting', + sessions: { laag: 6, middel: 8, hoog: 12 }, + }, + ], + overig: [ + { + name: 'Ondersteunende gesprekken', + description: 'Steunende begeleiding en reflectie', + sessions: { laag: 4, middel: 6, hoog: 8 }, + }, + { + name: 'Psycho-educatie', + description: 'Educatie over klachten en copingstrategieën', + sessions: { laag: 4, middel: 6, hoog: 8 }, + }, + ], +}; + +/** + * Haal interventies op voor een DSM categorie + */ +export function getInterventionsForCategory( + dsmCategory: string, + severity: Severity +): { name: string; description: string; recommendedSessions: number }[] { + // Normalize category + const normalizedCategory = dsmCategory + .toLowerCase() + .replace(/[^a-z]/g, '_') + .replace(/_+/g, '_'); + + // Find matching category + const interventions = + INTERVENTION_MAPPING[normalizedCategory] || + INTERVENTION_MAPPING['overig']; + + return interventions.map((intervention) => ({ + name: intervention.name, + description: intervention.description, + recommendedSessions: intervention.sessions[severity], + })); +} + +/** + * Bereken aanbevolen aantal sessies + */ +export function getRecommendedSessionCount( + dsmCategory: string, + severity: Severity +): number { + const interventions = getInterventionsForCategory(dsmCategory, severity); + if (interventions.length === 0) return 8; + + // Neem gemiddelde van eerste 2 interventies + const topInterventions = interventions.slice(0, 2); + const avg = + topInterventions.reduce((sum, i) => sum + i.recommendedSessions, 0) / + topInterventions.length; + + return Math.round(avg); +} + +/** + * Bepaal behandelvorm op basis van severity + */ +export function getRecommendedFormat(severity: Severity): string { + switch (severity) { + case 'laag': + return 'Individueel'; + case 'middel': + return 'Individueel'; + case 'hoog': + return 'Individueel + groep (optioneel)'; + default: + return 'Individueel'; + } +} + +/** + * Bepaal behandelfrequentie op basis van severity + */ +export function getRecommendedFrequency(severity: Severity): string { + switch (severity) { + case 'laag': + return 'Wekelijks tot tweewekelijks'; + case 'middel': + return 'Wekelijks'; + case 'hoog': + return 'Wekelijks tot 2x per week'; + default: + return 'Wekelijks'; + } +} diff --git a/lib/supabase/database.types.ts b/lib/supabase/database.types.ts index e89a130..dc84375 100644 --- a/lib/supabase/database.types.ts +++ b/lib/supabase/database.types.ts @@ -655,6 +655,7 @@ export type Database = { end_date: string | null id: string kindcheck_data: Json | null + life_domains: Json | null notes: string | null patient_id: string psychologist_id: string | null @@ -670,6 +671,7 @@ export type Database = { end_date?: string | null id?: string kindcheck_data?: Json | null + life_domains?: Json | null notes?: string | null patient_id: string psychologist_id?: string | null @@ -685,6 +687,7 @@ export type Database = { end_date?: string | null id?: string kindcheck_data?: Json | null + life_domains?: Json | null notes?: string | null patient_id?: string psychologist_id?: string | null