| Epic | Status | Wat is gedaan |

|---------------------|---------------|---------------------|
  | 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
This commit is contained in:
colinislit
2025-12-04 09:09:23 +01:00
parent 8e8a8e6f49
commit 621e1c90f9
22 changed files with 3369 additions and 12 deletions

370
lib/types/behandelplan.ts Normal file
View File

@@ -0,0 +1,370 @@
/**
* 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
*/
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
};
/**
* 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' },
};

167
lib/types/leefgebieden.ts Normal file
View File

@@ -0,0 +1,167 @@
/**
* Leefgebieden (Life Domains) Types
*
* 7 levensdomeinen volgens herstelgerichte GGZ-methodiek
* Gebruikt voor intake-assessment en behandelplan doelen
*/
import { z } from 'zod';
/**
* De 7 leefgebieden (levensdomeinen)
*/
export const LIFE_DOMAINS = [
'dlv', // Dagelijkse Levensverrichtingen
'wonen', // Wonen
'werk', // Werk/Dagbesteding
'sociaal', // Sociaal netwerk
'vrijetijd', // Vrijetijd/Zingeving
'financien', // Financiën
'gezondheid', // Lichamelijke gezondheid
] as const;
export type LifeDomain = typeof LIFE_DOMAINS[number];
/**
* Prioriteit niveau
*/
export const PRIORITIES = ['laag', 'middel', 'hoog'] as const;
export type Priority = typeof PRIORITIES[number];
/**
* Leefgebied metadata (labels, kleuren, emoji's)
*/
export const LIFE_DOMAIN_META: Record<LifeDomain, {
label: string;
shortLabel: string;
emoji: string;
color: string;
description: string;
}> = {
dlv: {
label: 'Dagelijkse Levensverrichtingen',
shortLabel: 'DLV',
emoji: '🏠',
color: '#8b5cf6', // paars
description: 'Zelfzorg, structuur, dagritme',
},
wonen: {
label: 'Wonen',
shortLabel: 'Wonen',
emoji: '🏡',
color: '#ec4899', // roze
description: 'Woonsituatie, veiligheid thuis',
},
werk: {
label: 'Werk/Dagbesteding',
shortLabel: 'Werk',
emoji: '💼',
color: '#f59e0b', // oranje
description: 'Baan, opleiding, vrijwilligerswerk',
},
sociaal: {
label: 'Sociaal netwerk',
shortLabel: 'Sociaal',
emoji: '👥',
color: '#3b82f6', // blauw
description: 'Familie, vrienden, relaties',
},
vrijetijd: {
label: 'Vrijetijd/Zingeving',
shortLabel: 'Vrijetijd',
emoji: '🎯',
color: '#10b981', // groen
description: "Hobby's, levensdoel, spiritualiteit",
},
financien: {
label: 'Financiën',
shortLabel: 'Financiën',
emoji: '💰',
color: '#eab308', // geel
description: 'Schulden, inkomen, budgettering',
},
gezondheid: {
label: 'Lichamelijke gezondheid',
shortLabel: 'Gezondheid',
emoji: '🏃',
color: '#ef4444', // rood
description: 'Slaap, beweging, voeding',
},
};
/**
* Score per leefgebied (1-5 schaal)
*/
export interface LifeDomainScore {
domain: LifeDomain;
baseline: number; // 1-5, score bij intake
current: number; // 1-5, huidige score (voor evaluatie)
target: number; // 1-5, doelscore
notes: string; // Toelichting
priority: Priority; // Prioriteit voor behandeling
}
/**
* Zod schema voor validatie
*/
export const LifeDomainScoreSchema = z.object({
domain: z.enum(LIFE_DOMAINS),
baseline: z.number().min(1).max(5),
current: z.number().min(1).max(5),
target: z.number().min(1).max(5),
notes: z.string().default(''),
priority: z.enum(PRIORITIES),
});
export const LifeDomainScoresSchema = z.array(LifeDomainScoreSchema).length(7);
/**
* Type voor het volledige leefgebieden formulier
*/
export type LifeDomainScores = LifeDomainScore[];
/**
* Default waarden voor nieuw formulier
*/
export function createDefaultLifeDomainScores(): LifeDomainScores {
return LIFE_DOMAINS.map((domain) => ({
domain,
baseline: 3,
current: 3,
target: 4,
notes: '',
priority: 'middel' as Priority,
}));
}
/**
* Helper: krijg leefgebieden met hoge prioriteit
*/
export function getHighPriorityDomains(scores: LifeDomainScores): LifeDomainScore[] {
return scores.filter((s) => s.priority === 'hoog');
}
/**
* Helper: krijg leefgebieden met lage scores (problematisch)
*/
export function getLowScoreDomains(scores: LifeDomainScores, threshold = 2): LifeDomainScore[] {
return scores.filter((s) => s.baseline <= threshold);
}
/**
* Helper: bereken gemiddelde score
*/
export function getAverageScore(scores: LifeDomainScores, type: 'baseline' | 'current' | 'target' = 'baseline'): number {
if (scores.length === 0) return 0;
const sum = scores.reduce((acc, s) => acc + s[type], 0);
return Math.round((sum / scores.length) * 10) / 10;
}
/**
* Helper: krijg kleur voor score (groen/oranje/rood)
*/
export function getScoreColor(score: number): string {
if (score >= 4) return '#10b981'; // groen
if (score >= 3) return '#f59e0b'; // oranje
return '#ef4444'; // rood
}