/** * 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 { const result = {} as Record; 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 = { 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 = { 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 = { 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; // ============================================================================= // 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(); 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, }; }