feat(diagnose+agenda): Diagnose module met ICD-10 en agenda uitbreidingen

Diagnose Module:
- Diagnose overzicht pagina met alle patiënt diagnoses
- Diagnosis manager met ICD-10 combobox zoekfunctie
- Diagnose kaarten met hoofddiagnose markering
- Modal voor nieuwe/bewerkte diagnoses
- ICD-10 GGZ codes dataset (lib/data/)
- Zod schemas voor diagnose validatie
- TypeScript types voor ICD-10 (lib/types/icd10.ts)
- Complete documentatie (PRD, FO, TO, Bouwplan)

Agenda Uitbreidingen:
- Patient context card in afspraak modal
- Rapportage composer direct in afspraak modal
- Rapportage bewerken vanuit gekoppelde rapportages
- Verbeterde focus styling voor inputs

Behandelplan:
- Flat componenten structuur (behandeldoel-card, form, planning)
- Context header component
- Uitgebreide types (lib/types/behandelplan.ts)
- Actions voor behandelplan beheer

UI Componenten:
- Command component (shadcn/ui) voor combobox
- Popover component (shadcn/ui)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
colinislit
2025-12-12 13:29:59 +01:00
parent 593bef4dcf
commit 265d3a971f
36 changed files with 6663 additions and 236 deletions

View File

@@ -0,0 +1,134 @@
{
"version": "ICD-10-GM 2024",
"source": "WHO (publiek domein)",
"description": "GGZ-relevante ICD-10 codes voor Mini EPD prototype",
"categories": [
{
"name": "Depressieve stoornissen",
"codes": [
{ "code": "F32.0", "display": "Lichte depressieve episode", "keywords": ["depressie", "licht", "somber"] },
{ "code": "F32.1", "display": "Matige depressieve episode", "keywords": ["depressie", "matig", "somber"] },
{ "code": "F32.2", "display": "Ernstige depressieve episode zonder psychotische kenmerken", "keywords": ["depressie", "ernstig", "somber"] },
{ "code": "F32.3", "display": "Ernstige depressieve episode met psychotische kenmerken", "keywords": ["depressie", "ernstig", "psychose", "waan"] },
{ "code": "F33.0", "display": "Recidiverende depressieve stoornis, huidige episode licht", "keywords": ["depressie", "recidiverend", "terugkerend"] },
{ "code": "F33.1", "display": "Recidiverende depressieve stoornis, huidige episode matig", "keywords": ["depressie", "recidiverend", "terugkerend"] },
{ "code": "F33.2", "display": "Recidiverende depressieve stoornis, huidige episode ernstig", "keywords": ["depressie", "recidiverend", "terugkerend", "ernstig"] }
]
},
{
"name": "Angststoornissen",
"codes": [
{ "code": "F40.0", "display": "Agorafobie", "keywords": ["angst", "plein", "open ruimte", "fobie"] },
{ "code": "F40.1", "display": "Sociale fobie", "keywords": ["sociaal", "angst", "vermijding", "fobie"] },
{ "code": "F40.2", "display": "Specifieke fobie", "keywords": ["fobie", "specifiek", "angst"] },
{ "code": "F41.0", "display": "Paniekstoornis", "keywords": ["paniek", "aanval", "angst", "hartkloppingen"] },
{ "code": "F41.1", "display": "Gegeneraliseerde angststoornis", "keywords": ["gad", "piekeren", "angst", "zorgen"] },
{ "code": "F41.2", "display": "Gemengde angststoornis en depressieve stoornis", "keywords": ["angst", "depressie", "gemengd"] },
{ "code": "F41.3", "display": "Andere gemengde angststoornissen", "keywords": ["angst", "gemengd"] },
{ "code": "F41.9", "display": "Angststoornis, niet gespecificeerd", "keywords": ["angst", "nos"] }
]
},
{
"name": "Trauma- en stressorgerelateerde stoornissen",
"codes": [
{ "code": "F43.0", "display": "Acute stressreactie", "keywords": ["stress", "acuut", "trauma"] },
{ "code": "F43.1", "display": "Posttraumatische stressstoornis", "keywords": ["ptss", "trauma", "herbeleven", "nachtmerries"] },
{ "code": "F43.2", "display": "Aanpassingsstoornis", "keywords": ["aanpassing", "stress", "life event"] },
{ "code": "F43.8", "display": "Andere reacties op ernstige stress", "keywords": ["stress", "trauma"] }
]
},
{
"name": "Obsessief-compulsieve stoornissen",
"codes": [
{ "code": "F42.0", "display": "Overwegend obsessieve gedachten of ruminaties", "keywords": ["ocd", "obsessie", "dwang", "gedachten"] },
{ "code": "F42.1", "display": "Overwegend compulsieve handelingen", "keywords": ["ocd", "compulsie", "dwang", "rituelen"] },
{ "code": "F42.2", "display": "Gemengde obsessieve gedachten en handelingen", "keywords": ["ocd", "obsessie", "compulsie", "dwang"] }
]
},
{
"name": "Bipolaire stoornissen",
"codes": [
{ "code": "F31.0", "display": "Bipolaire stoornis, huidige episode hypomaan", "keywords": ["bipolair", "hypomanie", "manisch"] },
{ "code": "F31.1", "display": "Bipolaire stoornis, huidige episode manisch zonder psychose", "keywords": ["bipolair", "manie", "manisch"] },
{ "code": "F31.3", "display": "Bipolaire stoornis, huidige episode licht of matig depressief", "keywords": ["bipolair", "depressie"] },
{ "code": "F31.4", "display": "Bipolaire stoornis, huidige episode ernstig depressief", "keywords": ["bipolair", "depressie", "ernstig"] }
]
},
{
"name": "Persoonlijkheidsstoornissen",
"codes": [
{ "code": "F60.0", "display": "Paranoïde persoonlijkheidsstoornis", "keywords": ["persoonlijkheid", "paranoide", "wantrouwen"] },
{ "code": "F60.1", "display": "Schizoïde persoonlijkheidsstoornis", "keywords": ["persoonlijkheid", "schizoid", "teruggetrokken"] },
{ "code": "F60.2", "display": "Antisociale persoonlijkheidsstoornis", "keywords": ["persoonlijkheid", "antisociaal", "psychopathie"] },
{ "code": "F60.3", "display": "Borderline persoonlijkheidsstoornis", "keywords": ["persoonlijkheid", "borderline", "emotieregulatie", "instabiel"] },
{ "code": "F60.4", "display": "Theatrale persoonlijkheidsstoornis", "keywords": ["persoonlijkheid", "theatraal", "histrionisch"] },
{ "code": "F60.6", "display": "Vermijdende persoonlijkheidsstoornis", "keywords": ["persoonlijkheid", "vermijdend", "angst", "afwijzing"] },
{ "code": "F60.7", "display": "Afhankelijke persoonlijkheidsstoornis", "keywords": ["persoonlijkheid", "afhankelijk", "dependent"] }
]
},
{
"name": "ADHD en gedragsstoornissen",
"codes": [
{ "code": "F90.0", "display": "Aandachtstekortstoornis met hyperactiviteit", "keywords": ["adhd", "add", "concentratie", "hyperactief"] },
{ "code": "F90.1", "display": "Hyperkinetische gedragsstoornis", "keywords": ["adhd", "gedrag", "hyperactief"] }
]
},
{
"name": "Autismespectrumstoornissen",
"codes": [
{ "code": "F84.0", "display": "Autisme", "keywords": ["autisme", "ass", "spectrum"] },
{ "code": "F84.5", "display": "Syndroom van Asperger", "keywords": ["asperger", "autisme", "ass", "spectrum"] }
]
},
{
"name": "Eetstoornissen",
"codes": [
{ "code": "F50.0", "display": "Anorexia nervosa", "keywords": ["eetstoornis", "anorexia", "gewicht", "eten"] },
{ "code": "F50.2", "display": "Boulimia nervosa", "keywords": ["eetstoornis", "boulimia", "eetbuien", "braken"] },
{ "code": "F50.9", "display": "Eetstoornis, niet gespecificeerd", "keywords": ["eetstoornis", "nos"] }
]
},
{
"name": "Schizofrenie en psychotische stoornissen",
"codes": [
{ "code": "F20.0", "display": "Paranoïde schizofrenie", "keywords": ["schizofrenie", "psychose", "waan", "hallucinatie"] },
{ "code": "F20.9", "display": "Schizofrenie, niet gespecificeerd", "keywords": ["schizofrenie", "psychose", "nos"] },
{ "code": "F23.0", "display": "Acute polymorf psychotische stoornis zonder symptomen van schizofrenie", "keywords": ["psychose", "acuut"] }
]
},
{
"name": "Stoornissen door middelengebruik",
"codes": [
{ "code": "F10.1", "display": "Schadelijk gebruik van alcohol", "keywords": ["alcohol", "verslaving", "middelen"] },
{ "code": "F10.2", "display": "Alcoholafhankelijkheid", "keywords": ["alcohol", "verslaving", "afhankelijk"] },
{ "code": "F12.1", "display": "Schadelijk gebruik van cannabis", "keywords": ["cannabis", "wiet", "middelen"] },
{ "code": "F12.2", "display": "Cannabisafhankelijkheid", "keywords": ["cannabis", "wiet", "verslaving"] },
{ "code": "F19.1", "display": "Schadelijk gebruik van meerdere middelen", "keywords": ["middelen", "drugs", "verslaving"] },
{ "code": "F19.2", "display": "Afhankelijkheid van meerdere middelen", "keywords": ["middelen", "drugs", "verslaving", "afhankelijk"] }
]
},
{
"name": "Somatoforme en dissociatieve stoornissen",
"codes": [
{ "code": "F44.0", "display": "Dissociatieve amnesie", "keywords": ["dissociatie", "amnesie", "geheugen"] },
{ "code": "F44.1", "display": "Dissociatieve fugue", "keywords": ["dissociatie", "fugue"] },
{ "code": "F45.0", "display": "Somatisatiestoornis", "keywords": ["somatisch", "lichamelijk", "klachten"] },
{ "code": "F45.2", "display": "Hypochondrische stoornis", "keywords": ["hypochondrie", "ziekte", "angst"] }
]
},
{
"name": "Slaapstoornissen",
"codes": [
{ "code": "F51.0", "display": "Niet-organische insomnie", "keywords": ["slaap", "insomnie", "slapeloosheid"] },
{ "code": "F51.1", "display": "Niet-organische hypersomnie", "keywords": ["slaap", "hypersomnie", "slaperigheid"] }
]
}
],
"frequentCodes": [
"F32.1",
"F41.1",
"F43.1",
"F41.0",
"F60.3"
]
}

89
lib/schemas/diagnosis.ts Normal file
View File

@@ -0,0 +1,89 @@
import { z } from 'zod';
/** Severity options */
export const DIAGNOSIS_SEVERITIES = ['licht', 'matig', 'ernstig'] as const;
/** Diagnosis type options */
export const DIAGNOSIS_TYPES = ['primary', 'secondary'] as const;
/** Clinical status options (moet matchen met database enum condition_clinical_status) */
export const DIAGNOSIS_STATUSES = ['active', 'remission', 'resolved', 'inactive'] as const;
/**
* Zod schema voor diagnose formulier validatie
*/
export const diagnosisSchema = z.object({
/** ICD-10 code (verplicht, format F##.#) */
code: z
.string()
.min(1, 'ICD-10 code is verplicht')
.regex(/^F\d{2}(\.\d{1,2})?$/, 'Ongeldige ICD-10 code (verwacht format: F##.#)'),
/** Beschrijving van de diagnose (verplicht) */
description: z
.string()
.min(1, 'Beschrijving is verplicht')
.max(200, 'Beschrijving mag maximaal 200 tekens zijn'),
/** Ernst classificatie (verplicht) */
severity: z.enum(DIAGNOSIS_SEVERITIES, {
message: 'Selecteer een geldige ernst',
}),
/** Diagnose type: hoofd of nevendiagnose (verplicht) */
diagnosisType: z.enum(DIAGNOSIS_TYPES, {
message: 'Selecteer hoofd- of nevendiagnose',
}),
/** Klinische status (verplicht) */
status: z.enum(DIAGNOSIS_STATUSES, {
message: 'Selecteer een geldige status',
}),
/** DSM-5 referentie (optioneel, vrije tekst) */
dsm5Reference: z
.string()
.max(100, 'DSM-5 referentie mag maximaal 100 tekens zijn')
.optional()
.or(z.literal('')),
/** Onderbouwing / notities (optioneel) */
notes: z
.string()
.max(500, 'Onderbouwing mag maximaal 500 tekens zijn')
.optional()
.or(z.literal('')),
});
/** TypeScript type afgeleid van het schema */
export type DiagnosisFormData = z.infer<typeof diagnosisSchema>;
/** Default waarden voor nieuw diagnose formulier */
export const diagnosisDefaults: DiagnosisFormData = {
code: '',
description: '',
severity: 'matig',
diagnosisType: 'secondary',
status: 'active',
dsm5Reference: '',
notes: '',
};
/**
* Schema voor server-side payload (inclusief patient/intake IDs)
*/
export const diagnosisPayloadSchema = diagnosisSchema.extend({
patientId: z.string().uuid('Ongeldige patient ID'),
intakeId: z.string().uuid('Ongeldige intake ID'),
});
export type DiagnosisPayload = z.infer<typeof diagnosisPayloadSchema>;
/**
* Schema voor update operatie (alle velden optioneel behalve ID)
*/
export const diagnosisUpdateSchema = diagnosisSchema.partial().extend({
id: z.string().uuid('Ongeldige diagnose ID'),
});
export type DiagnosisUpdatePayload = z.infer<typeof diagnosisUpdateSchema>;

View File

@@ -384,3 +384,237 @@ export const GOAL_STATUS_LABELS: Record<GoalStatus, { label: string; color: stri
gehaald: { label: 'Gehaald', color: '#10b981' },
bijgesteld: { label: 'Bijgesteld', color: '#f59e0b' },
};
// =============================================================================
// FLAT BEHANDELPLAN TYPES (Nieuwe platte structuur)
// =============================================================================
/**
* Embedded interventie binnen een behandeldoel
* Simpelere versie zonder linkedGoalIds (want embedded in doel)
*/
export interface EmbeddedInterventie {
id: string;
name: string; // bijv. "CGT", "EMDR", "ACT"
description: string; // Korte beschrijving van de aanpak
}
/**
* Behandeldoel met embedded interventies
* Kerntype voor de "platte" behandelplan structuur
*/
export interface Behandeldoel {
id: string;
// Doel informatie
title: string; // Professionele formulering
clientVersion: string; // B1-taal versie voor cliënt
// Classificatie
lifeDomain: LifeDomain; // Gekoppeld leefgebied
// Embedded interventies (KERNVERANDERING - niet meer apart)
interventies: EmbeddedInterventie[];
// Timeline
startWeek: number; // Start week (1-52)
endWeek: number; // Eind week (1-52)
// Status & voortgang
status: GoalStatus;
progress: number; // 0-100
}
// =============================================================================
// FLAT BEHANDELPLAN ZOD SCHEMAS
// =============================================================================
export const EmbeddedInterventieSchema = z.object({
id: z.string(),
name: z.string().min(1).max(100),
description: z.string().max(500),
});
export const BehandeldoelSchema = z.object({
id: z.string(),
title: z.string().min(5).max(200),
clientVersion: z.string().min(5).max(300),
lifeDomain: z.enum(LIFE_DOMAINS),
interventies: z.array(EmbeddedInterventieSchema),
startWeek: z.number().min(1).max(52),
endWeek: z.number().min(1).max(52),
status: z.enum(GOAL_STATUSES),
progress: z.number().min(0).max(100),
});
/**
* Schema voor AI-gegenereerd flat behandelplan
*/
export const GeneratedPlanFlatSchema = z.object({
behandelstructuur: BehandelstructuurSchema,
behandeldoelen: z.array(BehandeldoelSchema).min(1).max(6),
evaluatiemomenten: z.array(EvaluatiemomentSchema).min(1),
veiligheidsplan: VeiligheidsplanSchema.optional(),
});
export type GeneratedPlanFlat = z.infer<typeof GeneratedPlanFlatSchema>;
// =============================================================================
// TRANSFORMATIE FUNCTIES (Oude <-> Nieuwe structuur)
// =============================================================================
/**
* Transformeer oude structuur (goals + interventions apart) naar flat (behandeldoelen)
* Koppelt interventies aan doelen op basis van linkedGoalIds
*/
export function transformToFlat(
goals: SmartGoal[],
interventions: Intervention[]
): Behandeldoel[] {
return goals.map((goal) => {
// Vind interventies die aan dit doel gekoppeld zijn
const linkedInterventions = interventions
.filter((int) => int.linkedGoalIds.includes(goal.id))
.map((int) => ({
id: int.id,
name: int.name,
description: int.description,
}));
return {
id: goal.id,
title: goal.title,
clientVersion: goal.clientVersion,
lifeDomain: goal.lifeDomain,
interventies: linkedInterventions,
startWeek: 1, // Default, kan later uit goal.timelineWeeks berekend worden
endWeek: goal.timelineWeeks,
status: goal.status,
progress: goal.progress,
};
});
}
/**
* Transformeer flat structuur terug naar oude structuur (voor backwards compatibility)
* Zet embedded interventies om naar aparte array met linkedGoalIds
*/
export function transformFromFlat(behandeldoelen: Behandeldoel[]): {
goals: SmartGoal[];
interventions: Intervention[];
} {
const goals: SmartGoal[] = [];
const interventionMap = new Map<string, Intervention>();
for (const doel of behandeldoelen) {
// Maak SmartGoal van Behandeldoel
goals.push({
id: doel.id,
title: doel.title,
description: '', // Niet meer gebruikt in flat structuur
clientVersion: doel.clientVersion,
lifeDomain: doel.lifeDomain,
priority: 'middel', // Default
measurability: '', // Niet meer gebruikt in flat structuur
timelineWeeks: doel.endWeek,
status: doel.status,
progress: doel.progress,
});
// Verzamel interventies en koppel aan doel
for (const int of doel.interventies) {
const existing = interventionMap.get(int.id);
if (existing) {
// Interventie bestaat al, voeg dit doel toe aan linkedGoalIds
existing.linkedGoalIds.push(doel.id);
} else {
// Nieuwe interventie
interventionMap.set(int.id, {
id: int.id,
name: int.name,
description: int.description,
rationale: '', // Niet meer gebruikt in flat structuur
linkedGoalIds: [doel.id],
});
}
}
}
return {
goals,
interventions: Array.from(interventionMap.values()),
};
}
/**
* Maak een nieuw leeg behandeldoel
*/
export function createEmptyBehandeldoel(lifeDomain: LifeDomain = 'dlv'): Behandeldoel {
return {
id: generateId(),
title: '',
clientVersion: '',
lifeDomain,
interventies: [],
startWeek: 1,
endWeek: 8,
status: 'niet_gestart',
progress: 0,
};
}
/**
* Maak een nieuwe lege embedded interventie
*/
export function createEmptyEmbeddedInterventie(): EmbeddedInterventie {
return {
id: generateId(),
name: '',
description: '',
};
}
/**
* Bereken totale voortgang van behandeldoelen
*/
export function calculateBehandeldoelenProgress(doelen: Behandeldoel[]): number {
if (doelen.length === 0) return 0;
const sum = doelen.reduce((acc, doel) => acc + doel.progress, 0);
return Math.round(sum / doelen.length);
}
/**
* Check of flat plan klaar is voor publicatie
*/
export function canPublishFlat(
behandeldoelen: Behandeldoel[],
behandelstructuur: Behandelstructuur,
evaluatiemomenten: Evaluatiemoment[]
): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (behandeldoelen.length === 0) {
errors.push('Minimaal 1 behandeldoel is vereist');
}
// Check of elk doel minimaal 1 interventie heeft
const doelenZonderInterventie = behandeldoelen.filter(
(d) => d.interventies.length === 0
);
if (doelenZonderInterventie.length > 0) {
errors.push('Elk behandeldoel moet minimaal 1 interventie hebben');
}
if (!behandelstructuur.duur || !behandelstructuur.frequentie) {
errors.push('Behandelstructuur moet compleet zijn');
}
if (evaluatiemomenten.length < 2) {
errors.push('Minimaal 2 evaluatiemomenten zijn vereist');
}
return {
valid: errors.length === 0,
errors,
};
}

110
lib/types/icd10.ts Normal file
View File

@@ -0,0 +1,110 @@
/**
* ICD-10 GGZ Code Types
* Voor gebruik met lib/data/icd10-ggz-codes.json
*/
export interface ICD10Code {
/** ICD-10 code (bijv. "F32.1") */
code: string;
/** Nederlandse beschrijving */
display: string;
/** Zoekwoorden voor filtering */
keywords: string[];
}
export interface ICD10Category {
/** Categorie naam (bijv. "Depressieve stoornissen") */
name: string;
/** Codes binnen deze categorie */
codes: ICD10Code[];
}
export interface ICD10CodeList {
/** Versie van de codelijst */
version: string;
/** Bron (WHO publiek domein) */
source: string;
/** Beschrijving */
description: string;
/** Categorieën met codes */
categories: ICD10Category[];
/** Veelgebruikte codes (top 5) */
frequentCodes: string[];
}
/** Ernst classificatie voor diagnoses */
export type DiagnosisSeverity = 'licht' | 'matig' | 'ernstig';
/** Klinische status (FHIR compatible) */
export type DiagnosisClinicalStatus =
| 'active'
| 'recurrence'
| 'relapse'
| 'inactive'
| 'remission'
| 'resolved';
/** Verificatie status (FHIR compatible) */
export type DiagnosisVerificationStatus =
| 'unconfirmed'
| 'provisional'
| 'differential'
| 'confirmed'
| 'refuted'
| 'entered-in-error';
/** Diagnose type (hoofd/neven) */
export type DiagnosisType = 'primary' | 'secondary';
/** Helper type voor platte lijst van alle codes */
export type FlatICD10Code = ICD10Code & {
category: string;
};
/**
* Flatten ICD-10 categories naar platte lijst
*/
export function flattenICD10Codes(codeList: ICD10CodeList): FlatICD10Code[] {
return codeList.categories.flatMap((category) =>
category.codes.map((code) => ({
...code,
category: category.name,
}))
);
}
/**
* Zoek ICD-10 codes op query (code, display of keywords)
*/
export function searchICD10Codes(
codes: FlatICD10Code[],
query: string,
maxResults = 8
): FlatICD10Code[] {
if (!query.trim()) return [];
const normalizedQuery = query.toLowerCase().trim();
return codes
.filter((code) => {
const matchesCode = code.code.toLowerCase().includes(normalizedQuery);
const matchesDisplay = code.display.toLowerCase().includes(normalizedQuery);
const matchesKeyword = code.keywords.some((kw) =>
kw.toLowerCase().includes(normalizedQuery)
);
return matchesCode || matchesDisplay || matchesKeyword;
})
.slice(0, maxResults);
}
/**
* Haal veelgebruikte codes op
*/
export function getFrequentCodes(
codes: FlatICD10Code[],
frequentCodeIds: string[]
): FlatICD10Code[] {
return frequentCodeIds
.map((id) => codes.find((c) => c.code === id))
.filter((c): c is FlatICD10Code => c !== undefined);
}