Files
triqura-ecd/lib/types/behandelplan.ts
colinislit 265d3a971f 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>
2025-12-12 13:29:59 +01:00

621 lines
18 KiB
TypeScript

/**
* Behandelplan (Treatment Plan) Types
*
* Types voor AI-gegenereerde behandelplannen
* Gebaseerd op FHIR CarePlan met GGZ-specifieke uitbreidingen
*/
import { z } from 'zod';
import { type LifeDomain, type LifeDomainScore, LIFE_DOMAINS, PRIORITIES } from './leefgebieden';
// =============================================================================
// STATUS TYPES
// =============================================================================
/**
* Status van een behandelplan
*/
export const PLAN_STATUSES = ['concept', 'actief', 'in_evaluatie', 'afgerond', 'gearchiveerd'] as const;
export type PlanStatus = typeof PLAN_STATUSES[number];
/**
* Status van een doel
*/
export const GOAL_STATUSES = ['niet_gestart', 'bezig', 'gehaald', 'bijgesteld'] as const;
export type GoalStatus = typeof GOAL_STATUSES[number];
/**
* Type evaluatiemoment
*/
export const EVALUATION_TYPES = ['tussentijds', 'eind', 'crisis'] as const;
export type EvaluationType = typeof EVALUATION_TYPES[number];
/**
* Status evaluatiemoment
*/
export const EVALUATION_STATUSES = ['gepland', 'afgerond', 'overgeslagen'] as const;
export type EvaluationStatus = typeof EVALUATION_STATUSES[number];
// =============================================================================
// CORE TYPES
// =============================================================================
/**
* Behandelstructuur - algemene parameters van het plan
*/
export interface Behandelstructuur {
duur: string; // bijv. "8 weken"
frequentie: string; // bijv. "Wekelijks"
aantalSessies: number; // bijv. 8
vorm: string; // bijv. "Individueel"
}
/**
* SMART Doel
*/
export interface SmartGoal {
id: string;
title: string; // Korte beschrijving (1 zin)
description: string; // SMART-uitwerking (2-3 zinnen)
clientVersion: string; // B1-taal versie voor cliënt
lifeDomain: LifeDomain; // Gekoppeld leefgebied
priority: 'hoog' | 'middel' | 'laag';
measurability: string; // Hoe meten we vooruitgang?
timelineWeeks: number; // Binnen X weken
status: GoalStatus;
progress: number; // 0-100
}
/**
* Evidence-based Interventie
*/
export interface Intervention {
id: string;
name: string; // bijv. "CGT", "EMDR", "ACT"
description: string; // Uitleg van de interventie
rationale: string; // Waarom past dit bij deze cliënt?
linkedGoalIds: string[]; // Welke doelen worden hiermee benaderd?
}
/**
* Evaluatiemoment
*/
export interface Evaluatiemoment {
id: string;
type: EvaluationType;
weekNumber: number;
plannedDate: string; // ISO date string
actualDate?: string; // Ingevuld na uitvoering
status: EvaluationStatus;
outcome?: string; // Vrije tekst resultaat
lifeDomainUpdates?: LifeDomainScore[]; // Nieuwe scores
}
/**
* Veiligheidsplan (alleen bij severity "Hoog")
*/
export interface Veiligheidsplan {
waarschuwingssignalen: string[]; // 3-5 items
copingStrategieen: string[]; // 3-5 items
contacten: {
naam: string;
rol: string;
telefoon: string;
}[];
restricties?: string[]; // bijv. "Geen alcohol tijdens behandeling"
}
/**
* Sessie in de planning
*/
export interface Sessie {
id: string;
nummer: number;
focus: string;
datum?: string; // ISO date string
status: 'gepland' | 'afgerond' | 'no_show' | 'verzet' | 'geannuleerd';
gekoppeldeDoelIds: string[];
notities?: string;
}
// =============================================================================
// GENERATED PLAN (AI OUTPUT)
// =============================================================================
/**
* Volledig door AI gegenereerd behandelplan
*/
export interface GeneratedPlan {
behandelstructuur: Behandelstructuur;
doelen: SmartGoal[];
interventies: Intervention[];
sessiePlanning: Sessie[];
evaluatiemomenten: Evaluatiemoment[];
veiligheidsplan?: Veiligheidsplan; // Alleen bij severity "Hoog"
}
// =============================================================================
// API INPUT/OUTPUT TYPES
// =============================================================================
/**
* Input voor behandelplan generatie
*/
export interface GenerateBehandelplanInput {
patientId: string;
intakeId: string;
conditionId?: string; // Optioneel, haalt anders laatste op
extraInstructions?: string; // Optionele aanvullende instructies
}
/**
* Input voor micro-regeneratie
*/
export interface RegenerateSectionInput {
patientId: string;
carePlanId: string;
sectionType: 'goal' | 'intervention';
sectionId: string;
instruction?: string; // Extra instructie voor AI
currentPlan: GeneratedPlan; // Context van huidige plan
}
/**
* Output van micro-regeneratie
*/
export interface RegeneratedSection {
type: 'goal' | 'intervention';
original: SmartGoal | Intervention;
regenerated: SmartGoal | Intervention;
}
// =============================================================================
// ZOD SCHEMAS
// =============================================================================
export const BehandelstructuurSchema = z.object({
duur: z.string(),
frequentie: z.string(),
aantalSessies: z.number().min(1).max(52),
vorm: z.string(),
});
export const SmartGoalSchema = z.object({
id: z.string(),
title: z.string().min(5).max(200),
description: z.string().min(10).max(500),
clientVersion: z.string().min(5).max(300),
lifeDomain: z.enum(LIFE_DOMAINS),
priority: z.enum(PRIORITIES),
measurability: z.string().min(5).max(200),
timelineWeeks: z.number().min(1).max(52),
status: z.enum(GOAL_STATUSES),
progress: z.number().min(0).max(100),
});
export const InterventionSchema = z.object({
id: z.string(),
name: z.string().min(2).max(100),
description: z.string().min(10).max(500),
rationale: z.string().min(10).max(500),
linkedGoalIds: z.array(z.string()),
});
export const EvaluatiemomentSchema = z.object({
id: z.string(),
type: z.enum(EVALUATION_TYPES),
weekNumber: z.number().min(1).max(52),
plannedDate: z.string(),
actualDate: z.string().optional(),
status: z.enum(EVALUATION_STATUSES),
outcome: z.string().optional(),
lifeDomainUpdates: z.array(z.any()).optional(), // Simplified for now
});
export const VeiligheidsplanSchema = z.object({
waarschuwingssignalen: z.array(z.string()).min(1).max(10),
copingStrategieen: z.array(z.string()).min(1).max(10),
contacten: z.array(z.object({
naam: z.string(),
rol: z.string(),
telefoon: z.string(),
})),
restricties: z.array(z.string()).optional(),
});
export const SessieSchema = z.object({
id: z.string(),
nummer: z.number().min(1),
focus: z.string(),
datum: z.string().optional(),
status: z.enum(['gepland', 'afgerond', 'no_show', 'verzet', 'geannuleerd']),
gekoppeldeDoelIds: z.array(z.string()),
notities: z.string().optional(),
});
export const GeneratedPlanSchema = z.object({
behandelstructuur: BehandelstructuurSchema,
doelen: z.array(SmartGoalSchema).min(1).max(6),
interventies: z.array(InterventionSchema).min(1).max(5),
sessiePlanning: z.array(SessieSchema),
evaluatiemomenten: z.array(EvaluatiemomentSchema).min(1),
veiligheidsplan: VeiligheidsplanSchema.optional(),
});
export const GenerateBehandelplanInputSchema = z.object({
patientId: z.string().uuid(),
intakeId: z.string().uuid(),
conditionId: z.string().uuid().optional(),
extraInstructions: z.string().max(500).optional(),
});
export const RegenerateSectionInputSchema = z.object({
patientId: z.string().uuid(),
carePlanId: z.string().uuid(),
sectionType: z.enum(['goal', 'intervention']),
sectionId: z.string(),
instruction: z.string().max(200).optional(),
currentPlan: GeneratedPlanSchema,
});
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Genereer een nieuw UUID-achtig ID
*/
export function generateId(): string {
return crypto.randomUUID();
}
/**
* Maak een nieuw leeg SMART doel
*/
export function createEmptyGoal(lifeDomain: LifeDomain = 'dlv'): SmartGoal {
return {
id: generateId(),
title: '',
description: '',
clientVersion: '',
lifeDomain,
priority: 'middel',
measurability: '',
timelineWeeks: 8,
status: 'niet_gestart',
progress: 0,
};
}
/**
* Maak een nieuwe lege interventie
*/
export function createEmptyIntervention(): Intervention {
return {
id: generateId(),
name: '',
description: '',
rationale: '',
linkedGoalIds: [],
};
}
/**
* Bereken totale voortgang van alle doelen
*/
export function calculateTotalProgress(goals: SmartGoal[]): number {
if (goals.length === 0) return 0;
const sum = goals.reduce((acc, goal) => acc + goal.progress, 0);
return Math.round(sum / goals.length);
}
/**
* Krijg doelen per leefgebied
*/
export function getGoalsByDomain(goals: SmartGoal[]): Record<LifeDomain, SmartGoal[]> {
const result = {} as Record<LifeDomain, SmartGoal[]>;
for (const domain of LIFE_DOMAINS) {
result[domain] = goals.filter((g) => g.lifeDomain === domain);
}
return result;
}
/**
* Check of plan klaar is voor publicatie
*/
export function canPublish(plan: GeneratedPlan): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (plan.doelen.length === 0) {
errors.push('Minimaal 1 doel is vereist');
}
if (plan.interventies.length === 0) {
errors.push('Minimaal 1 interventie is vereist');
}
if (!plan.behandelstructuur.duur || !plan.behandelstructuur.frequentie) {
errors.push('Behandelstructuur moet compleet zijn');
}
if (plan.evaluatiemomenten.length < 2) {
errors.push('Minimaal 2 evaluatiemomenten zijn vereist');
}
return {
valid: errors.length === 0,
errors,
};
}
/**
* Status label voor UI (oude Nederlandse keys - deprecated)
*/
export const PLAN_STATUS_LABELS: Record<PlanStatus, { label: string; color: string }> = {
concept: { label: 'Concept', color: '#60a5fa' }, // blauw
actief: { label: 'Actief', color: '#10b981' }, // groen
in_evaluatie: { label: 'In evaluatie', color: '#f59e0b' }, // oranje
afgerond: { label: 'Afgerond', color: '#6b7280' }, // grijs
gearchiveerd: { label: 'Gearchiveerd', color: '#9ca3af' }, // lichtgrijs
};
/**
* FHIR status naar Nederlandse UI labels
* Database gebruikt FHIR statussen, UI toont Nederlands
*/
export type FhirCarePlanStatus = 'draft' | 'active' | 'on-hold' | 'completed' | 'revoked' | 'entered-in-error' | 'unknown';
export const FHIR_STATUS_LABELS: Record<FhirCarePlanStatus, { label: string; color: string }> = {
draft: { label: 'Concept', color: '#60a5fa' }, // blauw
active: { label: 'Actueel', color: '#10b981' }, // groen
'on-hold': { label: 'Gepauzeerd', color: '#f59e0b' }, // oranje
completed: { label: 'Definitief', color: '#6b7280' }, // grijs
revoked: { label: 'Archief', color: '#9ca3af' }, // lichtgrijs
'entered-in-error': { label: 'Verwijderd', color: '#ef4444' }, // rood
unknown: { label: 'Onbekend', color: '#9ca3af' }, // lichtgrijs
};
/**
* Goal status label voor UI
*/
export const GOAL_STATUS_LABELS: Record<GoalStatus, { label: string; color: string }> = {
niet_gestart: { label: 'Niet gestart', color: '#9ca3af' },
bezig: { label: 'Bezig', color: '#3b82f6' },
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,
};
}