| 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

View File

@@ -0,0 +1,14 @@
/**
* Behandelplan Components
*
* Export all behandelplan-related components
*/
// Leefgebieden (Life Domains)
export { LeefgebiedenBadge, LeefgebiedenBadgeGroup } from './leefgebieden-badge';
export {
LeefgebiedenScores,
LeefgebiedenScoresCard,
LeefgebiedenScoreBar,
} from './leefgebieden-scores';
export { LeefgebiedenForm, LeefgebiedenQuickForm } from './leefgebieden-form';

View File

@@ -0,0 +1,77 @@
'use client';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import { type LifeDomain, LIFE_DOMAIN_META } from '@/lib/types/leefgebieden';
interface LeefgebiedenBadgeProps {
domain: LifeDomain;
showEmoji?: boolean;
size?: 'sm' | 'md' | 'lg';
className?: string;
}
/**
* Colored badge for a life domain
* Uses the domain's specific color from the meta definition
*/
export function LeefgebiedenBadge({
domain,
showEmoji = true,
size = 'md',
className,
}: LeefgebiedenBadgeProps) {
const meta = LIFE_DOMAIN_META[domain];
const sizeClasses = {
sm: 'text-xs px-1.5 py-0.5',
md: 'text-sm px-2 py-0.5',
lg: 'text-base px-3 py-1',
};
return (
<Badge
className={cn(
'font-medium border-0 text-white',
sizeClasses[size],
className
)}
style={{ backgroundColor: meta.color }}
>
{showEmoji && <span className="mr-1">{meta.emoji}</span>}
{meta.shortLabel}
</Badge>
);
}
interface LeefgebiedenBadgeGroupProps {
domains: LifeDomain[];
showEmoji?: boolean;
size?: 'sm' | 'md' | 'lg';
className?: string;
}
/**
* Group of life domain badges
*/
export function LeefgebiedenBadgeGroup({
domains,
showEmoji = true,
size = 'sm',
className,
}: LeefgebiedenBadgeGroupProps) {
if (domains.length === 0) return null;
return (
<div className={cn('flex flex-wrap gap-1', className)}>
{domains.map((domain) => (
<LeefgebiedenBadge
key={domain}
domain={domain}
showEmoji={showEmoji}
size={size}
/>
))}
</div>
);
}

View File

@@ -0,0 +1,368 @@
'use client';
import { useState, useCallback } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Slider } from '@/components/ui/slider';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import {
type LifeDomainScore,
type LifeDomain,
type Priority,
LIFE_DOMAIN_META,
LIFE_DOMAINS,
createDefaultLifeDomainScores,
getScoreColor,
} from '@/lib/types/leefgebieden';
interface DomainFormRowProps {
score: LifeDomainScore;
onChange: (score: LifeDomainScore) => void;
expanded?: boolean;
onToggleExpand?: () => void;
}
const SCORE_LABELS: Record<number, string> = {
1: 'Zeer laag',
2: 'Laag',
3: 'Gemiddeld',
4: 'Goed',
5: 'Uitstekend',
};
const PRIORITY_OPTIONS: { value: Priority; label: string; color: string }[] = [
{ value: 'laag', label: 'Laag', color: 'bg-gray-200 text-gray-700' },
{ value: 'middel', label: 'Middel', color: 'bg-blue-100 text-blue-700' },
{ value: 'hoog', label: 'Hoog', color: 'bg-orange-100 text-orange-700' },
];
/**
* Single domain row in the form
*/
function DomainFormRow({
score,
onChange,
expanded = false,
onToggleExpand,
}: DomainFormRowProps) {
const meta = LIFE_DOMAIN_META[score.domain];
const handleBaselineChange = (values: number[]) => {
onChange({ ...score, baseline: values[0], current: values[0] });
};
const handleTargetChange = (values: number[]) => {
onChange({ ...score, target: values[0] });
};
const handlePriorityChange = (priority: Priority) => {
onChange({ ...score, priority });
};
const handleNotesChange = (notes: string) => {
onChange({ ...score, notes });
};
return (
<div
className={cn(
'border rounded-lg p-4 transition-all',
expanded ? 'bg-muted/50' : 'hover:bg-muted/30',
score.priority === 'hoog' && 'border-orange-300'
)}
>
{/* Header Row */}
<div
className="flex items-center justify-between cursor-pointer"
onClick={onToggleExpand}
>
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-lg flex items-center justify-center text-xl"
style={{ backgroundColor: `${meta.color}20` }}
>
{meta.emoji}
</div>
<div>
<h4 className="font-medium">{meta.label}</h4>
<p className="text-sm text-muted-foreground">{meta.description}</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<div className="flex items-center gap-2">
<span
className="text-lg font-bold"
style={{ color: getScoreColor(score.baseline) }}
>
{score.baseline}
</span>
<span className="text-muted-foreground"></span>
<span className="text-lg font-bold text-foreground">
{score.target}
</span>
</div>
<span className="text-xs text-muted-foreground">
{SCORE_LABELS[score.baseline]}
</span>
</div>
<svg
className={cn(
'w-5 h-5 text-muted-foreground transition-transform',
expanded && 'rotate-180'
)}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
</div>
{/* Expanded Content */}
{expanded && (
<div className="mt-4 space-y-4 pt-4 border-t">
{/* Baseline Score Slider */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label>Huidige score (baseline)</Label>
<span className="text-sm font-medium">{score.baseline}</span>
</div>
<Slider
value={[score.baseline]}
onValueChange={handleBaselineChange}
min={1}
max={5}
step={1}
className="w-full"
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>Zeer laag</span>
<span>Uitstekend</span>
</div>
</div>
{/* Target Score Slider */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label>Doelscore</Label>
<span className="text-sm font-medium">{score.target}</span>
</div>
<Slider
value={[score.target]}
onValueChange={handleTargetChange}
min={1}
max={5}
step={1}
className="w-full"
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>Zeer laag</span>
<span>Uitstekend</span>
</div>
</div>
{/* Priority Selection */}
<div className="space-y-2">
<Label>Prioriteit voor behandeling</Label>
<div className="flex gap-2">
{PRIORITY_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
className={cn(
'px-3 py-1.5 rounded-md text-sm font-medium transition-all',
score.priority === option.value
? option.color
: 'bg-muted text-muted-foreground hover:bg-muted/80'
)}
onClick={() => handlePriorityChange(option.value)}
>
{option.label}
</button>
))}
</div>
</div>
{/* Notes */}
<div className="space-y-2">
<Label>Toelichting (optioneel)</Label>
<Textarea
value={score.notes}
onChange={(e) => handleNotesChange(e.target.value)}
placeholder={`Opmerkingen over ${meta.shortLabel.toLowerCase()}...`}
className="resize-none"
rows={2}
/>
</div>
</div>
)}
</div>
);
}
interface LeefgebiedenFormProps {
initialScores?: LifeDomainScore[];
onSave: (scores: LifeDomainScore[]) => void;
onCancel?: () => void;
isSaving?: boolean;
}
/**
* Complete form for entering life domain scores during intake
*/
export function LeefgebiedenForm({
initialScores,
onSave,
onCancel,
isSaving = false,
}: LeefgebiedenFormProps) {
const [scores, setScores] = useState<LifeDomainScore[]>(
initialScores || createDefaultLifeDomainScores()
);
const [expandedDomain, setExpandedDomain] = useState<LifeDomain | null>(null);
const handleScoreChange = useCallback((updatedScore: LifeDomainScore) => {
setScores((prev) =>
prev.map((s) => (s.domain === updatedScore.domain ? updatedScore : s))
);
}, []);
const handleToggleExpand = useCallback((domain: LifeDomain) => {
setExpandedDomain((prev) => (prev === domain ? null : domain));
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave(scores);
};
const highPriorityCount = scores.filter((s) => s.priority === 'hoog').length;
return (
<form onSubmit={handleSubmit}>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<span>📊</span>
Leefgebieden Assessment
</CardTitle>
<CardDescription>
Beoordeel de 7 leefgebieden van de cliënt. Klik op een gebied om scores
en prioriteiten aan te passen.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{/* Summary Stats */}
<div className="flex items-center justify-between text-sm text-muted-foreground mb-4">
<span>
Klik op een leefgebied om de scores aan te passen
</span>
{highPriorityCount > 0 && (
<span className="text-orange-600 font-medium">
{highPriorityCount} hoge prioriteit{highPriorityCount > 1 ? 'en' : ''}
</span>
)}
</div>
{/* Domain Rows */}
<div className="space-y-2">
{LIFE_DOMAINS.map((domain) => {
const score = scores.find((s) => s.domain === domain)!;
return (
<DomainFormRow
key={domain}
score={score}
onChange={handleScoreChange}
expanded={expandedDomain === domain}
onToggleExpand={() => handleToggleExpand(domain)}
/>
);
})}
</div>
{/* Actions */}
<div className="flex justify-end gap-2 pt-4 border-t">
{onCancel && (
<Button type="button" variant="outline" onClick={onCancel}>
Annuleren
</Button>
)}
<Button type="submit" disabled={isSaving}>
{isSaving ? 'Opslaan...' : 'Leefgebieden opslaan'}
</Button>
</div>
</CardContent>
</Card>
</form>
);
}
interface LeefgebiedenQuickFormProps {
initialScores?: LifeDomainScore[];
onChange: (scores: LifeDomainScore[]) => void;
}
/**
* Compact version of the form for inline editing
*/
export function LeefgebiedenQuickForm({
initialScores,
onChange,
}: LeefgebiedenQuickFormProps) {
const [scores, setScores] = useState<LifeDomainScore[]>(
initialScores || createDefaultLifeDomainScores()
);
const handleScoreChange = useCallback(
(domain: LifeDomain, value: number) => {
const updated = scores.map((s) =>
s.domain === domain ? { ...s, baseline: value, current: value } : s
);
setScores(updated);
onChange(updated);
},
[scores, onChange]
);
return (
<div className="space-y-3">
{LIFE_DOMAINS.map((domain) => {
const score = scores.find((s) => s.domain === domain)!;
const meta = LIFE_DOMAIN_META[domain];
return (
<div key={domain} className="flex items-center gap-3">
<div className="w-8 text-center text-lg">{meta.emoji}</div>
<div className="flex-1">
<div className="flex justify-between text-sm mb-1">
<span>{meta.shortLabel}</span>
<span
className="font-medium"
style={{ color: getScoreColor(score.baseline) }}
>
{score.baseline}
</span>
</div>
<Slider
value={[score.baseline]}
onValueChange={(v) => handleScoreChange(domain, v[0])}
min={1}
max={5}
step={1}
className="w-full"
/>
</div>
</div>
);
})}
</div>
);
}

View File

@@ -0,0 +1,186 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import {
type LifeDomainScore,
type LifeDomain,
LIFE_DOMAIN_META,
LIFE_DOMAINS,
getScoreColor,
getAverageScore,
} from '@/lib/types/leefgebieden';
import { LeefgebiedenBadge } from './leefgebieden-badge';
interface LeefgebiedenScoreBarProps {
score: LifeDomainScore;
showTarget?: boolean;
compact?: boolean;
}
/**
* Single life domain score as a progress bar
*/
export function LeefgebiedenScoreBar({
score,
showTarget = true,
compact = false,
}: LeefgebiedenScoreBarProps) {
const meta = LIFE_DOMAIN_META[score.domain];
const percentage = (score.current / 5) * 100;
const targetPercentage = (score.target / 5) * 100;
return (
<div className={cn('space-y-1', compact ? 'py-1' : 'py-2')}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-lg">{meta.emoji}</span>
<span className={cn('font-medium', compact ? 'text-sm' : 'text-base')}>
{meta.shortLabel}
</span>
{score.priority === 'hoog' && (
<span className="text-xs text-orange-500 font-medium">
Prioriteit
</span>
)}
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="font-medium" style={{ color: getScoreColor(score.current) }}>
{score.current}
</span>
{showTarget && (
<>
<span></span>
<span className="font-medium text-foreground">{score.target}</span>
</>
)}
</div>
</div>
<div className="relative">
{/* Custom progress bar with domain-specific color */}
<div className="relative h-2 w-full overflow-hidden rounded-full bg-primary/20">
<div
className="h-full transition-all duration-300"
style={{
width: `${percentage}%`,
backgroundColor: meta.color,
}}
/>
</div>
{showTarget && (
<div
className="absolute top-0 h-2 w-0.5 bg-foreground/50"
style={{ left: `${targetPercentage}%` }}
title={`Doel: ${score.target}`}
/>
)}
</div>
</div>
);
}
interface LeefgebiedenScoresProps {
scores: LifeDomainScore[];
showTarget?: boolean;
compact?: boolean;
showSummary?: boolean;
className?: string;
}
/**
* Display all 7 life domain scores
*/
export function LeefgebiedenScores({
scores,
showTarget = true,
compact = false,
showSummary = true,
className,
}: LeefgebiedenScoresProps) {
// Ensure we have all 7 domains in the correct order
const orderedScores = LIFE_DOMAINS.map((domain) => {
const found = scores.find((s) => s.domain === domain);
return (
found || {
domain,
baseline: 3,
current: 3,
target: 4,
notes: '',
priority: 'middel' as const,
}
);
});
const avgCurrent = getAverageScore(orderedScores, 'current');
const avgTarget = getAverageScore(orderedScores, 'target');
const highPriority = orderedScores.filter((s) => s.priority === 'hoog');
return (
<div className={cn('space-y-4', className)}>
{showSummary && (
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-4">
<span className="text-muted-foreground">
Gemiddelde score:{' '}
<span className="font-medium text-foreground">{avgCurrent}</span>
{showTarget && (
<span className="text-muted-foreground"> {avgTarget}</span>
)}
</span>
</div>
{highPriority.length > 0 && (
<div className="flex items-center gap-1">
<span className="text-muted-foreground">Prioriteiten:</span>
{highPriority.map((s) => (
<LeefgebiedenBadge key={s.domain} domain={s.domain} size="sm" />
))}
</div>
)}
</div>
)}
<div className="space-y-1">
{orderedScores.map((score) => (
<LeefgebiedenScoreBar
key={score.domain}
score={score}
showTarget={showTarget}
compact={compact}
/>
))}
</div>
</div>
);
}
interface LeefgebiedenScoresCardProps {
scores: LifeDomainScore[];
title?: string;
showTarget?: boolean;
className?: string;
}
/**
* Life domain scores in a Card wrapper
*/
export function LeefgebiedenScoresCard({
scores,
title = 'Leefgebieden',
showTarget = true,
className,
}: LeefgebiedenScoresCardProps) {
return (
<Card className={className}>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<span>📊</span>
{title}
</CardTitle>
</CardHeader>
<CardContent>
<LeefgebiedenScores scores={scores} showTarget={showTarget} />
</CardContent>
</Card>
);
}

36
components/ui/badge.tsx Normal file
View File

@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

76
components/ui/card.tsx Normal file
View File

@@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

26
components/ui/label.tsx Normal file
View File

@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

28
components/ui/slider.tsx Normal file
View File

@@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }

55
components/ui/tabs.tsx Normal file
View File

@@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }

View File

@@ -0,0 +1,438 @@
# Mission Control — Bouwplan Behandelplan Module
**Projectnaam:** Mini-EPD Prototype - Behandelplan Module
**Versie:** v1.0
**Datum:** 03-12-2024
**Auteur:** Colin Lit
---
## 1. Doel en context
**Doel:**
Een werkende MVP Behandelplan module bouwen die demonstreert hoe AI:
- **Tijdsbesparing** realiseert: van 30+ minuten naar 2-5 minuten
- **Kwaliteitsverbetering** biedt: SMART-doelen, evidence-based interventies
- **Transparantie** creëert: cliënt kan eigen plan begrijpen (B1-taal)
- **Praktische workflow** ondersteunt: intake → diagnose → behandelplan
**Context:**
Deze module is onderdeel van de AI Speedrun LinkedIn Serie. Het prototype demonstreert AI-toegevoegde waarde voor zorgprofessionals, product owners en developers.
**Gerelateerde documenten:**
- [PRD Behandelplan v2.0](./prd-behandelplan-v2-final.md)
- [FO Behandelplan v1.0](./fo-behandelplan-v1.md)
- [TO Behandelplan v1.0](./to-behandelplan-v1.md)
---
## 2. Uitgangspunten
### 2.1 Technische Stack
| Component | Technologie | Status |
|-----------|-------------|--------|
| **Frontend** | Next.js 14.2 + TailwindCSS + shadcn/ui | ✅ Bestaand |
| **Backend** | Next.js API Routes + Server Actions | ✅ Bestaand |
| **Database** | Supabase PostgreSQL + RLS | ✅ Bestaand |
| **AI/ML** | Claude 3.5 Sonnet (Anthropic) | ✅ Bestaand |
| **Hosting** | Vercel | ✅ Bestaand |
| **Auth** | Supabase Auth | ✅ Bestaand |
| **Icons** | Lucide React | ✅ Bestaand |
| **Editor** | TipTap | ✅ Bestaand |
| **Validation** | Zod | ✅ Bestaand |
| **Charts** | Recharts | ⏳ Stretch goal |
### 2.2 Projectkaders
| Kader | Waarde |
|-------|--------|
| **Tijd** | 14-19 uur bouwtijd voor MVP |
| **Budget** | €0 extra (bestaande API keys) |
| **Team** | 1 developer + AI assistentie |
| **Data** | Fictieve demo-data (geen productiegegevens) |
| **Doel** | Werkende demo voor LinkedIn serie |
### 2.3 Gekozen Aanpak
- **Foundation first**: Types en database migraties eerst, dan UI
- **Simpele visualisatie**: Progress bars i.p.v. radar chart (stretch)
- **Simple JSON**: Geen streaming, enkele API call met loading state
### 2.4 Programmeer Uitgangspunten
**Code Quality Principles:**
- **DRY (Don't Repeat Yourself)**
- Herbruikbare leefgebieden componenten
- Centrale types voor behandelplan structuur
- Utility functions voor score berekeningen
- **KISS (Keep It Simple, Stupid)**
- Simple JSON API (geen streaming complexity)
- Progress bars i.p.v. radar chart
- Bestaande UI patterns hergebruiken
- **SOC (Separation of Concerns)**
- Types in `/lib/types/behandelplan.ts`
- AI prompts in `/lib/ai/behandelplan-prompt.ts`
- Server actions in `/app/epd/patients/[id]/behandelplan/actions.ts`
- UI components in `/components/behandelplan/`
- **YAGNI (You Aren't Gonna Need It)**
- Geen versie-diff view (stretch)
- Geen real-time collaboration
- Geen notificaties/reminders
**Development Practices:**
- **Error Handling**
- Try-catch op alle AI calls
- Fallback naar manual mode bij AI failure
- User-friendly foutmeldingen
- **Security**
- ANTHROPIC_API_KEY in environment
- RLS policies op care_plans
- Zod validation op alle inputs
- **Performance**
- AI response < 8 seconden
- Skeleton loaders tijdens generatie
- Optimistic updates voor status
---
## 3. Epics & Stories Overzicht
| 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 |
| E3 | Behandelplan UI | Pagina + componenten | ⏳ To Do | 5 | 6-8 uur |
| E4 | Stretch | Micro-regeneratie, radar chart | ⏳ Optioneel | 3 | 3-5 uur |
**Totaal MVP (E0-E3):** 14-19 uur
**Totaal met Stretch:** 17-24 uur
---
## 4. Epics & Stories (Uitwerking)
### Epic 0 — Foundation (Types & Database)
**Epic Doel:** Solide basis met TypeScript types en database schema uitbreiding.
| 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 |
**Technical Notes:**
```typescript
// lib/types/leefgebieden.ts
export type LifeDomain = 'dlv' | 'wonen' | 'werk' | 'sociaal' | 'vrijetijd' | 'financien' | 'gezondheid'
export interface LifeDomainScore {
domain: LifeDomain
baseline: number // 1-5
current: number // 1-5
target: number // 1-5
notes: string
priority: 'laag' | 'middel' | 'hoog'
}
```
**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`
---
### Epic 1 — Leefgebieden Componenten
**Epic Doel:** Formulier voor intake + visuele weergave van scores.
| 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 |
**Technical Notes:**
```typescript
// Kleuren per leefgebied
const DOMAIN_COLORS = {
dlv: '#8b5cf6', // paars
wonen: '#ec4899', // roze
werk: '#f59e0b', // oranje
sociaal: '#3b82f6', // blauw
vrijetijd: '#10b981', // groen
financien: '#eab308', // geel
gezondheid: '#ef4444', // rood
}
```
**Deliverables:**
- [ ] `components/behandelplan/leefgebieden-form.tsx`
- [ ] `components/behandelplan/leefgebieden-scores.tsx`
- [ ] `components/behandelplan/leefgebieden-badge.tsx`
---
### Epic 2 — AI Generatie
**Epic Doel:** Claude API endpoint voor behandelplan generatie.
| 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 |
**Technical Notes:**
```typescript
// Prompt structuur
const messages = [
{ role: 'system', content: BEHANDELPLAN_SYSTEM_PROMPT },
{ role: 'user', content: buildUserPrompt(context) },
];
// AI settings
const settings = {
model: 'claude-3-5-sonnet-20240620',
max_tokens: 4096,
temperature: 0.3, // Consistent maar niet robotisch
};
```
**Deliverables:**
- [ ] `lib/ai/behandelplan-prompt.ts`
- [ ] `lib/ai/intervention-mapping.ts`
- [ ] `app/api/behandelplan/generate/route.ts`
---
### Epic 3 — Behandelplan UI
**Epic Doel:** Werkende behandelplan pagina met alle componenten.
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP |
|----------|--------------|---------------------|--------|------------------|----|
| E3.S1 | Behandelplan pagina | Placeholder vervangen, data loading, status weergave | ⏳ | E2.S2 | 2 |
| E3.S2 | Generate button + flow | Button triggert AI, loading state, resultaat weergave | ⏳ | E3.S1 | 2 |
| E3.S3 | SMART doelen sectie | Goal cards met progress, status, leefgebied badge | ⏳ | E3.S1, E1.S3 | 3 |
| E3.S4 | Interventies sectie | Intervention cards met gekoppelde doelen | ⏳ | E3.S3 | 2 |
| E3.S5 | Server actions | Create, update, delete, publish behandelplan | ⏳ | E3.S1 | 2 |
**Technical Notes:**
```
/app/epd/patients/[id]/behandelplan/
├── page.tsx # Server component
├── actions.ts # Server actions
└── components/
├── behandelplan-view.tsx
├── generate-button.tsx
├── goals-section.tsx
├── goal-card.tsx
└── interventions-section.tsx
```
**Deliverables:**
- [ ] `app/epd/patients/[id]/behandelplan/page.tsx` (vervangen)
- [ ] `app/epd/patients/[id]/behandelplan/actions.ts`
- [ ] `components/behandelplan/behandelplan-view.tsx`
- [ ] `components/behandelplan/generate-button.tsx`
- [ ] `components/behandelplan/goals-section.tsx`
- [ ] `components/behandelplan/goal-card.tsx`
- [ ] `components/behandelplan/interventions-section.tsx`
---
### Epic 4 — Stretch Goals (Optioneel)
**Epic Doel:** Extra features indien tijd beschikbaar.
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | SP |
|----------|--------------|---------------------|--------|------------------|----|
| E4.S1 | Micro-regeneratie | Per doel [↻] knop, modal met instructie, nieuw voorstel | ⏳ | E3.S3 | 3 |
| E4.S2 | Radar chart | Recharts installeren, 3-lijn spindiagram | ⏳ | E1.S2 | 3 |
| E4.S3 | Sessie-planning | Tabel met 8-12 sessies, status per sessie | ⏳ | E3.S1 | 2 |
**Technical Notes:**
- Recharts: `pnpm add recharts`
- Regenerate endpoint: `POST /api/behandelplan/regenerate-section`
---
## 5. Kwaliteit & Testplan
### Test Types
| Test Type | Scope | Tools | Status |
|-----------|-------|-------|--------|
| Unit Tests | Types, utilities | Vitest | ⏳ Nice to have |
| Integration | API endpoints | Manual + curl | ✅ Required |
| Smoke Tests | Happy flow | Manual checklist | ✅ Required |
| Performance | AI response time | Network tab | ✅ Required |
### Manual Test Checklist (Demo)
**Pre-conditions:**
- [ ] Patient bestaat met intake data
- [ ] Diagnose/probleemprofiel is ingevuld
- [ ] Leefgebieden scores zijn ingevuld
**Happy Flow:**
- [ ] Behandelplan pagina laadt zonder errors
- [ ] "Genereer Behandelplan" knop is zichtbaar
- [ ] AI genereert plan binnen 8 seconden
- [ ] 2-4 SMART doelen worden getoond
- [ ] Doelen hebben leefgebied badges
- [ ] Interventies zijn gekoppeld aan doelen
- [ ] B1-taal versie is beschikbaar per doel
- [ ] Plan kan worden opgeslagen
- [ ] Status kan worden gewijzigd (concept → actief)
**Error Scenarios:**
- [ ] Geen intake data → "Vul eerst intake in" melding
- [ ] AI API error → "AI niet beschikbaar" melding + retry optie
- [ ] Validatie error → Inline error messages
---
## 6. Demo & Presentatieplan
### Demo Scenario
**Duur:** 5-7 minuten
**Doelgroep:** LinkedIn audience (product owners, developers, zorgprofessionals)
**Flow:**
1. **Context** (30 sec)
- "Behandelplan schrijven kost 30+ minuten"
- "AI kan dit reduceren naar 2-5 minuten"
2. **Leefgebieden invullen** (1 min)
- Toon 7 domeinen met sliders
- Prioriteiten instellen
- Opslaan
3. **AI Generatie** (2 min)
- Klik op "Genereer Behandelplan"
- Toon loading state met timer
- Resultaat verschijnt (< 5 sec)
4. **Resultaat bekijken** (2 min)
- SMART doelen met leefgebied tags
- B1-taal versie voor cliënt
- Evidence-based interventies
- Behandelstructuur
5. **Aanpassen** (1 min)
- Doel bewerken
- Status wijzigen
- Publiceren
**Key Highlights:**
- Tijdsbesparing: 30 min → 5 min
- Kwaliteit: SMART-criteria automatisch
- Transparantie: B1-taal voor cliënt
---
## 7. Risico's & Mitigatie
| Risico | Kans | Impact | Mitigatie | Owner |
|--------|------|--------|-----------|-------|
| AI genereert invalide JSON | Middel | Hoog | Zod validation, retry (2x), fallback message | Dev |
| AI response > 8 seconden | Laag | Middel | Timeout handling, loading feedback | Dev |
| Leefgebieden UI te complex | Middel | Middel | Simpele sliders, geen radar chart (MVP) | Dev |
| Database migratie faalt | Laag | Hoog | Test lokaal eerst, rollback script | Dev |
| Prompt geeft slechte output | Middel | Hoog | Itereren op prompt, few-shot examples | Dev |
| Scope creep | Hoog | Middel | Strikte MVP scope, stretch als optioneel | Dev |
---
## 8. Implementatie Volgorde
```
Week 1 (14-19 uur totaal)
├── Dag 1: E0 - Foundation (2-3 uur)
│ ├── E0.S1: Leefgebieden types
│ ├── E0.S2: Behandelplan types
│ └── E0.S3: Database migratie
├── Dag 2: E1 - Leefgebieden (3-4 uur)
│ ├── E1.S1: Formulier component
│ ├── E1.S2: Scores weergave
│ └── E1.S3: Badge component
├── Dag 3: E2 - AI Generatie (3-4 uur)
│ ├── E2.S1: Prompt engineering
│ ├── E2.S2: Generate endpoint
│ └── E2.S3: Event logging
└── Dag 4-5: E3 - UI (6-8 uur)
├── E3.S1: Behandelplan pagina
├── E3.S2: Generate button
├── E3.S3: Doelen sectie
├── E3.S4: Interventies sectie
└── E3.S5: Server actions
Optioneel (indien tijd):
└── E4 - Stretch Goals
├── E4.S1: Micro-regeneratie
├── E4.S2: Radar chart
└── E4.S3: Sessie-planning
```
---
## 9. Referenties
### Mission Control Documents
- **PRD** — [prd-behandelplan-v2-final.md](./prd-behandelplan-v2-final.md)
- **FO** — [fo-behandelplan-v1.md](./fo-behandelplan-v1.md)
- **TO** — [to-behandelplan-v1.md](./to-behandelplan-v1.md)
- **UX/UI** — [ux-stylesheet.md](../ux-stylesheet.md)
### Bestaande Code
- API pattern: `/app/api/reports/classify/route.ts`
- Server actions: `/app/epd/patients/[id]/intakes/[intakeId]/actions.ts`
- Types pattern: `/lib/types/report.ts`
- AI integration: `/app/api/docs/chat/route.ts`
### External Resources
- Repository: `github.com/[org]/15-mini-epd-prototype`
- Anthropic Docs: https://docs.anthropic.com/claude/reference
- Supabase Docs: https://supabase.com/docs
---
## 10. Glossary & Abbreviations
| Term | Betekenis |
|------|-----------|
| SMART | Specifiek, Meetbaar, Acceptabel, Realistisch, Tijdgebonden |
| B1-taal | Taalniveau begrijpelijk voor algemeen publiek |
| Leefgebieden | 7 levensdomeinen volgens herstelgerichte zorg |
| DSM | Diagnostic and Statistical Manual (diagnose classificatie) |
| RLS | Row Level Security (Supabase) |
| FHIR | Fast Healthcare Interoperability Resources |
| Care Plan | FHIR resource voor behandelplan |
| SP | Story Points |
| MVP | Minimum Viable Product |
---
**Versiehistorie:**
| Versie | Datum | Auteur | Wijziging |
|--------|-------|--------|-----------|
| v1.0 | 03-12-2024 | Colin Lit | Initiële versie |

View File

@@ -0,0 +1,566 @@
# Functioneel Ontwerp (FO) — Behandelplan Module
**Projectnaam:** Mini-EPD Prototype - AI Speedrun
**Versie:** v1.0
**Datum:** 03-12-2024
**Auteur:** Colin Lit
---
## 1. Doel en relatie met het PRD
**Doel van dit document:**
Dit Functioneel Ontwerp beschrijft **hoe** de Behandelplan module uit het PRD functioneel werkt — wat de behandelaar ziet, doet en ervaart bij het genereren en beheren van behandelplannen met AI-ondersteuning.
**Relatie met PRD:**
- PRD: `prd-behandelplan-v2-final.md` — beschrijft *wat* en *waarom*
- FO (dit document): beschrijft *hoe* dit in de praktijk werkt
**Scope:**
- MVP-functionaliteit (Fase 1-4 uit implementatieplan)
- Foundation first aanpak (types → componenten → AI → UI)
- Simpele leefgebieden visualisatie (progress bars, geen radar chart)
- Simple JSON API (geen streaming)
---
## 2. Overzicht van de belangrijkste onderdelen
De Behandelplan module bestaat uit de volgende onderdelen:
| # | Onderdeel | Beschrijving |
|---|-----------|--------------|
| 1 | **Leefgebieden Intake** | Formulier voor 7 levensdomeinen met scores en prioriteiten |
| 2 | **AI Generatie** | Knop om behandelplan te laten genereren op basis van intake + diagnose |
| 3 | **Behandelplan Overzicht** | Hoofdpagina met structuur, doelen, interventies |
| 4 | **SMART Doelen** | Lijst van 2-4 behandeldoelen met voortgang |
| 5 | **Interventies** | Evidence-based interventies gekoppeld aan doelen |
| 6 | **Sessie-planning** | Tabel met geplande sessies (stretch) |
| 7 | **Evaluatiemomenten** | Tussentijdse en eindevaluatie (stretch) |
---
## 3. User Stories
### Primaire User Stories (MVP)
| ID | Rol | Doel / Actie | Verwachte waarde | Prioriteit |
|----|-----|--------------|------------------|------------|
| US-01 | Behandelaar | Leefgebieden scores invullen bij intake | Gestructureerd beeld van cliëntsituatie | Hoog |
| US-02 | Behandelaar | AI behandelplan laten genereren | Van 30 min naar 2-5 min tijdsbesparing | Hoog |
| US-03 | Behandelaar | SMART doelen bekijken en aanpassen | Kwaliteitsverbetering, passend bij cliënt | Hoog |
| US-04 | Behandelaar | Interventies koppelen aan doelen | Evidence-based behandeling | Hoog |
| US-05 | Behandelaar | Specifiek doel laten regenereren | Fijnafstelling zonder alles opnieuw | Middel |
| US-06 | Behandelaar | Plan publiceren (concept → actief) | Cliënt kan plan inzien | Middel |
### Secundaire User Stories (Stretch)
| ID | Rol | Doel / Actie | Verwachte waarde | Prioriteit |
|----|-----|--------------|------------------|------------|
| US-07 | Behandelaar | Sessie-planning invullen | Overzicht behandeltraject | Laag |
| US-08 | Behandelaar | Evaluatiemoment vastleggen | Voortgang meten en bijsturen | Laag |
| US-09 | Cliënt | Eigen behandelplan bekijken (B1-taal) | Transparantie en begrip | Laag |
---
## 4. Functionele werking per onderdeel
### 4.1 Leefgebieden Intake
**Locatie:** Onderdeel van intake-flow of aparte tab binnen cliëntdossier
**Functionaliteit:**
- Formulier met 7 levensdomeinen (leefgebieden)
- Per domein:
- **Score slider:** 1-5 (1 = zeer problematisch, 5 = goed)
- **Toelichting:** Vrij tekstveld voor context
- **Prioriteit:** Dropdown (Laag / Middel / Hoog)
**De 7 Leefgebieden:**
| # | Domein | Emoji | Kleur | Voorbeeldvragen |
|---|--------|-------|-------|-----------------|
| 1 | Dagelijkse Levensverrichtingen (DLV) | 🏠 | `#8b5cf6` | Zelfzorg, structuur, dagritme |
| 2 | Wonen | 🏡 | `#ec4899` | Woonsituatie, veiligheid thuis |
| 3 | Werk/Dagbesteding | 💼 | `#f59e0b` | Baan, opleiding, vrijwilligerswerk |
| 4 | Sociaal netwerk | 👥 | `#3b82f6` | Familie, vrienden, relaties |
| 5 | Vrijetijd/Zingeving | 🎯 | `#10b981` | Hobby's, levensdoel, spiritualiteit |
| 6 | Financiën | 💰 | `#eab308` | Schulden, inkomen, budgettering |
| 7 | Lichamelijke gezondheid | 🏃 | `#ef4444` | Slaap, beweging, voeding |
**Gedrag:**
- Opslaan: Data wordt opgeslagen als JSONB in intake/care_plan record
- Validatie: Alle 7 domeinen moeten een score hebben
- Weergave: Na opslaan worden scores getoond als progress bars met kleuren
**States:**
- **Leeg:** "Vul de leefgebieden in om een compleet beeld te krijgen"
- **Gedeeltelijk:** Waarschuwing bij minder dan 7 domeinen
- **Compleet:** Groen vinkje, klaar voor behandelplan generatie
---
### 4.2 AI Behandelplan Generatie
**Locatie:** Behandelplan tab binnen cliëntdossier
**Trigger:** Knop `[⚡ Genereer Behandelplan]`
**Voorwaarden:**
- Intake notities aanwezig (uit rich text editor)
- Diagnose/probleemprofiel ingevuld (DSM-categorie + severity)
- Leefgebieden scores ingevuld (7 domeinen)
**Input naar AI:**
```
- Intake tekst (samenvatting of volledige notities)
- DSM-categorie (bijv. "Angststoornissen")
- Severity niveau (Laag / Middel / Hoog)
- Leefgebieden scores met prioriteiten
- Optioneel: extra instructies van behandelaar
```
**AI Processing:**
- Model: Claude 3.5 Sonnet
- Response tijd: < 5 seconden
- Output: Gestructureerde JSON
**Output van AI:**
1. **Behandelstructuur:** Duur, frequentie, aantal sessies, vorm
2. **SMART Doelen:** 2-4 doelen verdeeld over leefgebieden
3. **Interventies:** Evidence-based, gekoppeld aan doelen
4. **Sessie-planning:** Grove indeling (8-12 sessies)
5. **Evaluatiemomenten:** Tussentijds + eind
6. **Veiligheidsplan:** Alleen bij severity "Hoog"
**UI tijdens generatie:**
```
┌─────────────────────────────────────┐
│ ⚡ Behandelplan wordt gegenereerd...│
│ │
│ [████████████░░░░░░░] 75% │
│ │
│ Even geduld, dit duurt ~5 seconden │
└─────────────────────────────────────┘
```
**Na generatie:**
- Plan verschijnt in bewerkbare vorm
- Status: "Concept" (niet gepubliceerd)
- Behandelaar kan reviewen en aanpassen
---
### 4.3 Behandelplan Overzicht (Hoofdpagina)
**Locatie:** `/epd/patients/[id]/behandelplan`
**Layout:**
```
┌─────────────────────────────────────────────────────────────┐
│ HEADER │
│ Behandelplan v1 Status: ● Concept │
│ [Bewerken] [Publiceer] [Nieuwe Versie] │
├─────────────────────────────────────────────────────────────┤
│ │
│ 📋 BEHANDELSTRUCTUUR │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Duur: 8 weken | Frequentie: Wekelijks | Sessies: 8 ││
│ │ Vorm: Individueel ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ 🌐 LEEFGEBIEDEN OVERZICHT │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ DLV ████████░░ 4/5 Baseline: 3 ││
│ │ Wonen ████████░░ 4/5 Baseline: 4 ││
│ │ Werk ⚠️ ████░░░░░░ 2/5 Baseline: 2 [Prioriteit] ││
│ │ Sociaal ⚠️ ████░░░░░░ 2/5 Baseline: 2 [Prioriteit] ││
│ │ Vrijetijd ██████░░░░ 3/5 Baseline: 3 ││
│ │ Financiën ██████░░░░ 3/5 Baseline: 3 ││
│ │ Gezondheid ████████░░ 4/5 Baseline: 4 ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ 🎯 SMART DOELEN (3) │
│ [Doel cards - zie 4.4] │
│ │
│ 💡 INTERVENTIES (2) │
│ [Interventie cards - zie 4.5] │
│ │
└─────────────────────────────────────────────────────────────┘
```
**Acties:**
- `[Bewerken]`: Opent inline editing modus
- `[Publiceer]`: Wijzigt status naar "Actief", zichtbaar voor cliënt
- `[Nieuwe Versie]`: Maakt v2 aan op basis van huidige versie
**Status indicatoren:**
- 🔵 Concept - Bewerkbaar, niet zichtbaar voor cliënt
- 🟢 Actief - Gepubliceerd, zichtbaar voor cliënt
- 🟡 In evaluatie - Evaluatiemoment gepland
- ⚫ Afgerond - Behandeling afgerond
---
### 4.4 SMART Doelen
**Weergave per doel:**
```
┌───────────────────────────────────────────────────────────┐
│ 💼 Werk Prioriteit│
│ [Hoog] │
│ Terugkeer naar 4 werkdagen per week │
│ │
│ "Ik werk weer 4 dagen zonder paniek te krijgen" │
│ (cliënt-versie) │
│ │
│ Voortgang: ██████░░░░ 60% │
│ Status: Bezig | Deadline: 8 weken │
│ │
│ Meetbaarheid: Aantal werkdagen per week bijhouden │
│ │
│ [Bewerk] [↻ Regenereer] [Details ▼] │
└───────────────────────────────────────────────────────────┘
```
**Velden per doel:**
| Veld | Type | Beschrijving |
|------|------|--------------|
| Titel | Tekst | Korte beschrijving (1 zin) |
| Beschrijving | Tekst | SMART-uitwerking (2-3 zinnen) |
| Cliënt-versie | Tekst | B1-taal versie voor cliënt |
| Leefgebied | Tag | DLV/Wonen/Werk/Sociaal/etc. |
| Prioriteit | Dropdown | Hoog/Middel/Laag |
| Meetbaarheid | Tekst | Hoe meten we vooruitgang? |
| Tijdslijn | Getal | Binnen X weken |
| Status | Dropdown | Niet gestart/Bezig/Gehaald/Bijgesteld |
| Voortgang | Slider | 0-100% |
**Acties:**
- `[Bewerk]`: Inline editing van alle velden
- `[↻ Regenereer]`: AI genereert alternatief doel (zie 4.6)
- `[Details ▼]`: Uitklappen voor SMART-details
- `[+]`: Handmatig doel toevoegen
- `[🗑️]`: Doel verwijderen
**AI-gedrag bij generatie:**
- Focust op leefgebieden met prioriteit "Hoog"
- Verdeelt doelen over minimaal 2 verschillende domeinen
- Maakt concrete, meetbare doelen (geen vage termen)
- Genereert automatisch B1-taal cliënt-versie
---
### 4.5 Interventies
**Weergave per interventie:**
```
┌───────────────────────────────────────────────────────────┐
│ 🧠 Cognitieve Gedragstherapie (CGT) │
│ │
│ Beschrijving: │
│ Identificeren en uitdagen van negatieve gedachtenpatronen │
│ die angst en vermijding in stand houden. │
│ │
│ Rationale: │
│ CGT is de eerste keuze behandeling bij angststoornissen │
│ met sterke evidentie voor effectiviteit. │
│ │
│ Gekoppeld aan: [💼 Doel 1] [👥 Doel 2] │
│ │
│ [Bewerk] [Details ▼] │
└───────────────────────────────────────────────────────────┘
```
**Velden per interventie:**
| Veld | Type | Beschrijving |
|------|------|--------------|
| Naam | Tekst | CGT, Exposure, EMDR, ACT, etc. |
| Beschrijving | Tekst | Uitleg van de interventie |
| Rationale | Tekst | Waarom past dit bij deze cliënt? |
| Gekoppelde doelen | Multi-select | Welke doelen worden benaderd? |
**AI-mapping (evidence-based):**
| DSM-Categorie | Primaire Interventies | Sessies bij Hoog |
|---------------|----------------------|------------------|
| Angststoornissen | CGT, Exposure, ACT | 12-16 sessies |
| Stemmingsklachten | CGT, IPT, Gedragsactivatie | 8-12 sessies |
| Trauma/PTSS | EMDR, Narratieve therapie | 12+ sessies |
| Persoonlijkheid | Schematherapie, MBT | 20+ sessies |
---
### 4.6 Micro-regeneratie (Stretch)
**Trigger:** Klik op `[↻ Regenereer]` bij specifiek doel of interventie
**Flow:**
```
┌─────────────────────────────────────────────────────────────┐
│ ↻ Doel regenereren [X] │
├─────────────────────────────────────────────────────────────┤
│ │
│ Huidige doel: │
│ "Terugkeer naar 4 werkdagen per week" │
│ │
│ Extra instructie voor AI (optioneel): │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Maak meer gefocust op geleidelijke opbouw ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ [Annuleren] [↻ Regenereer] │
└─────────────────────────────────────────────────────────────┘
```
**Na regeneratie:**
```
┌─────────────────────────────────────────────────────────────┐
│ Nieuw voorstel: │
│ │
│ "Stapsgewijze opbouw naar 4 werkdagen via 2→3→4 schema" │
│ │
│ [Behoud origineel] [✓ Accepteer nieuw voorstel] │
└─────────────────────────────────────────────────────────────┘
```
**Gedrag:**
- AI behoudt context van rest van plan
- Alleen het specifieke onderdeel wordt vervangen
- Toast notification bij succes: "Doel bijgewerkt"
---
### 4.7 Publicatie Workflow
**Statussen:**
```
Concept ──→ Actief ──→ In evaluatie ──→ Afgerond
│ │
└──→ Gearchiveerd ←──────────┘
(bij nieuwe versie)
```
**Validatie voor publicatie:**
- ✓ Minimaal 1 doel ingevuld
- ✓ Minimaal 1 interventie gekoppeld
- ✓ Behandelstructuur compleet (duur, frequentie)
- ✓ Evaluatiemomenten gepland (tussentijds + eind)
**Publicatie actie:**
1. Behandelaar klikt `[Publiceer]`
2. Systeem valideert compleetheid
3. Bij succes: status → "Actief", publicatiedatum vastgelegd
4. Toast: "Behandelplan gepubliceerd"
5. Plan zichtbaar in cliëntportaal
**Versie-beheer:**
- Nummering: v1, v2, v3, etc.
- Bij "Nieuwe Versie": huidige → "Gearchiveerd", nieuwe kopie → "Concept"
- Oude versies blijven zichtbaar (read-only)
---
## 5. UI-overzicht (visuele structuur)
### Behandelplan Pagina Layout
```
┌─────────────────────────────────────────────────────────────────┐
│ HEADER │
│ Mini-EPD Logo [Cliëntnaam ▼] [Zoek...] [Profiel] │
├─────────────────┬───────────────────────────────────────────────┤
│ SIDEBAR │ MAIN CONTENT │
│ │ │
│ ← Cliënten │ ┌───────────────────────────────────────────┐ │
│ ───────── │ │ Behandelplan v1 Status: Concept │ │
│ □ Dashboard │ │ [Bewerken] [Publiceer] [Print] │ │
│ □ Intake │ └───────────────────────────────────────────┘ │
│ □ Diagnose │ │
│ ■ Behandelplan │ ┌── Behandelstructuur ──────────────────────┐ │
│ □ Rapportage │ │ Duur: 8 weken | Freq: Wekelijks | 8 sess │ │
│ □ Agenda │ └───────────────────────────────────────────┘ │
│ │ │
│ │ ┌── Leefgebieden ────────────────────────────┐ │
│ │ │ [Progress bars met scores per domein] │ │
│ │ └───────────────────────────────────────────┘ │
│ │ │
│ │ ┌── SMART Doelen ────────────────────────────┐ │
│ │ │ [Doel 1 - Werk] │ │
│ │ │ [Doel 2 - Sociaal] │ │
│ │ │ [Doel 3 - DLV] │ │
│ │ │ [+ Doel toevoegen] │ │
│ │ └───────────────────────────────────────────┘ │
│ │ │
│ │ ┌── Interventies ────────────────────────────┐ │
│ │ │ [CGT] [Exposure] │ │
│ │ └───────────────────────────────────────────┘ │
│ │ │
├─────────────────┴───────────────────────────────────────────────┤
│ FOOTER: Auto-saved 2 sec ago │
└─────────────────────────────────────────────────────────────────┘
```
### Responsive (Tablet)
```
┌─────────────────────────────────────────────┐
│ [☰] Mini-EPD Cliëntnaam [Zoek] │
├─────────────────────────────────────────────┤
│ │
│ Behandelplan v1 │
│ Status: ● Concept │
│ [Bewerken] [Publiceer] │
│ │
│ ┌── Behandelstructuur ────────────────────┐│
│ │ Duur: 8 weken | Wekelijks | 8 sessies ││
│ └─────────────────────────────────────────┘│
│ │
│ ┌── Leefgebieden ─────────────────────────┐│
│ │ [Compacte progress bars] ││
│ └─────────────────────────────────────────┘│
│ │
│ ┌── Doelen ───────────────────────────────┐│
│ │ [Gestapelde doel cards] ││
│ └─────────────────────────────────────────┘│
│ │
└─────────────────────────────────────────────┘
```
---
## 6. Interacties met AI (functionele beschrijving)
| Locatie | AI-actie | Trigger | Input | Output |
|---------|----------|---------|-------|--------|
| Behandelplan tab | Genereer plan | Klik `[⚡ Genereer]` | Intake + diagnose + leefgebieden | Compleet behandelplan (JSON) |
| Doel card | Regenereer doel | Klik `[↻ Regenereer]` | Context plan + instructie | Alternatief doel |
| Interventie card | Regenereer interventie | Klik `[↻ Regenereer]` | Context plan + instructie | Alternatieve interventie |
| Doel card | Genereer cliënt-versie | Automatisch bij nieuw doel | Behandelaar-tekst | B1-taal versie |
### AI Response Format
```typescript
interface AIGeneratedPlan {
behandelstructuur: {
duur: string // "8 weken"
frequentie: string // "Wekelijks"
aantalSessies: number // 8
vorm: string // "Individueel"
}
doelen: Array<{
id: string
title: string
description: string // SMART uitwerking
clientVersion: string // B1-taal
lifeDomain: string // "werk" | "sociaal" | etc.
priority: string // "hoog" | "middel" | "laag"
measurability: string
timelineWeeks: number
}>
interventies: Array<{
name: string
description: string
rationale: string
linkedGoalIds: string[]
}>
evaluatiemomenten: Array<{
type: string // "tussentijds" | "eind"
weekNumber: number
}>
veiligheidsplan?: { // Alleen bij severity "Hoog"
waarschuwingssignalen: string[]
copingStrategieen: string[]
contacten: string[]
}
}
```
---
## 7. Gebruikersrollen en rechten
| Rol | Toegang tot | Acties | Beperkingen |
|-----|------------|--------|-------------|
| Behandelaar | Eigen cliëntdossiers | Volledig CRUD, AI generatie | Alleen eigen cliënten |
| Behandelaar (collega) | Gedeelde cliënten | Lezen, commentaar | Geen bewerken |
| Cliënt | Eigen behandelplan | Alleen lezen | Ziet B1-versie, geen edit |
| Demo-user | Alle fictieve data | Lezen + AI testen | Geen opslaan |
---
## 8. States en Foutafhandeling
### Empty States
**Geen behandelplan:**
```
┌───────────────────────────────────────────┐
│ 📋 │
│ │
│ Nog geen behandelplan │
│ │
│ Vul eerst de intake en diagnose in, │
│ dan kan AI een behandelplan genereren. │
│ │
│ [Naar Intake] [Naar Diagnose] │
└───────────────────────────────────────────┘
```
**Incomplete voorwaarden:**
```
┌───────────────────────────────────────────┐
│ ⚠️ Nog niet klaar voor behandelplan │
│ │
│ □ Intake notities ✓ │
│ □ Diagnose/probleemprofiel ✗ │
│ □ Leefgebieden scores ✗ │
│ │
│ Vul de ontbrekende onderdelen in. │
└───────────────────────────────────────────┘
```
### Error States
| Situatie | Bericht | Actie |
|----------|---------|-------|
| AI niet beschikbaar | "AI tijdelijk niet beschikbaar" | Retry knop, handmatig alternatief |
| Validatie fout | Inline error onder veld | Focus op fout veld |
| Netwerk error | Toast: "Verbinding verloren" | Auto-retry, lokale opslag |
| Rate limit | "Even wachten..." | Countdown timer |
### Loading States
**AI generatie:**
```
⚡ Behandelplan wordt gegenereerd...
[████████████░░░░░░░] 75%
Even geduld, dit duurt ~5 seconden
```
**Auto-save:**
- Tijdens typen: "Opslaan..."
- Na succes: "✓ Opgeslagen 2 sec geleden"
---
## 9. Bijlagen & Referenties
### Interne Documenten
- [PRD Behandelplan v2.0](./prd-behandelplan-v2-final.md) — Requirements
- [Implementatieplan](~/.claude/plans/) — Technische aanpak
- [UX Stylesheet](../ux-stylesheet.md) — Kleuren, typography
### Technische Specificaties
- Database: `treatment_plans` tabel met JSONB structuur
- API: `/api/behandelplan/generate` (POST, JSON response)
- AI Model: Claude 3.5 Sonnet
### Externe Bronnen
- [GGZ Richtlijnen](https://www.ggzrichtlijnen.nl/) — Evidence-based interventies
- [WCAG 2.1 AA](https://www.w3.org/WAI/WCAG21/quickref/) — Accessibility
---
**Document Status:** v1.0 Draft
**Volgende Review:** Na implementatie Fase 1-2
**Eigenaar:** Colin Lit

View File

@@ -3,7 +3,7 @@
**Projectnaam:** Mini-ECD Prototype - AI Speedrun **Projectnaam:** Mini-ECD Prototype - AI Speedrun
**Versie:** v2.0 (volgens template, incl. Leefgebieden) **Versie:** v2.0 (volgens template, incl. Leefgebieden)
**Datum:** 2 december 2024 **Datum:** 2 december 2024
**Auteur:** Colin van Zeeland **Auteur:** Colin Lit
**Changelog:** **Changelog:**
- v2.0: Herstructurering volgens PRD template, duidelijke MVP/post-MVP scheiding, expliciete UX sectie - v2.0: Herstructurering volgens PRD template, duidelijke MVP/post-MVP scheiding, expliciete UX sectie

View File

@@ -0,0 +1,719 @@
# Technisch Ontwerp (TO) — Behandelplan Module
**Projectnaam:** Mini-EPD Prototype - AI Speedrun
**Versie:** v1.0
**Datum:** 03-12-2024
**Auteur:** Colin Lit
---
## 1. Doel en relatie met PRD en FO
**Doel van dit document:**
Dit Technisch Ontwerp beschrijft **hoe** de Behandelplan module technisch wordt gebouwd. Het PRD beschrijft het *wat*, het FO het *hoe functioneel*, en dit TO de *technische implementatie*.
**Gerelateerde documenten:**
- PRD: `prd-behandelplan-v2-final.md`
- FO: `fo-behandelplan-v1.md`
- Implementatieplan: `~/.claude/plans/effervescent-toasting-beaver.md`
**Scope:**
- Foundation first: Types → Database → Components → AI → UI
- Simple JSON API (geen streaming)
- Simpele leefgebieden visualisatie (progress bars, geen radar chart)
---
## 2. Technische Architectuur Overzicht
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ FRONTEND (Next.js 14) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────────┐ │
│ │ Behandelplan │ │ Leefgebieden │ │ SMART Doelen │ │
│ │ Page │ │ Components │ │ Components │ │
│ │ (Server Comp) │ │ (Client Comp) │ │ (Client Comp) │ │
│ └────────┬────────┘ └────────┬────────┘ └──────────────┬──────────────┘ │
│ │ │ │ │
│ └────────────────────┴──────────────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ Server Actions │ │
│ │ (behandelplan/ │ │
│ │ actions.ts) │ │
│ └──────────┬──────────┘ │
└────────────────────────────────────┼────────────────────────────────────────┘
┌────────────────────────────────────┼────────────────────────────────────────┐
│ API ROUTES │
│ ┌─────────────────────────────────▼─────────────────────────────────────┐ │
│ │ /api/behandelplan/generate │ │
│ │ (POST - AI Generation) │ │
│ └─────────────────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────▼─────────────────────────────────────┐ │
│ │ /api/behandelplan/regenerate-section │ │
│ │ (POST - Micro-regeneration) │ │
│ └─────────────────────────────────┬─────────────────────────────────────┘ │
└────────────────────────────────────┼────────────────────────────────────────┘
┌────────────────────────────────────┼────────────────────────────────────────┐
│ EXTERNAL SERVICES │
│ ┌─────────────────┐ │ ┌─────────────────────┐ │
│ │ Supabase │◄─────────────┴──────────────► Claude API │ │
│ │ (PostgreSQL) │ │ (Anthropic) │ │
│ │ - care_plans │ │ - claude-sonnet │ │
│ │ - patients │ │ - JSON response │ │
│ │ - intakes │ │ │ │
│ └─────────────────┘ └─────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘
```
---
## 3. Techstack Selectie
### Bestaande Stack (hergebruiken)
| Component | Technologie | Status | Argumentatie |
|-----------|-------------|--------|--------------|
| Frontend | Next.js 14.2.18 | ✅ Bestaand | React framework, SSR, App Router |
| Backend | Next.js API Routes | ✅ Bestaand | Co-located, TypeScript |
| Database | Supabase PostgreSQL | ✅ Bestaand | RLS, FHIR-compliant schema |
| AI | Claude Sonnet | ✅ Bestaand | API key geconfigureerd |
| Styling | TailwindCSS 3.4 | ✅ Bestaand | Utility-first, shadcn/ui |
| Icons | Lucide React | ✅ Bestaand | Consistent icon set |
| Editor | TipTap | ✅ Bestaand | Rich text editor |
| Validation | Zod | ✅ Bestaand | Schema validation |
### Nieuwe Dependencies
| Component | Technologie | Nodig voor | Alternatief |
|-----------|-------------|------------|-------------|
| Charts | Recharts 2.x | Radar chart (stretch) | ❌ Later toevoegen |
**Conclusie:** Geen nieuwe dependencies nodig voor MVP. Recharts alleen bij stretch goal.
---
## 4. Datamodel
### 4.1 Bestaande Tabellen (hergebruiken)
```sql
-- FHIR CarePlan (hoofdtabel voor behandelplannen)
care_plans (
id UUID PRIMARY KEY,
patient_id UUID REFERENCES patients(id),
title TEXT,
status careplan_status, -- draft | active | completed | revoked
intent TEXT,
goals JSONB, -- Array van doelen
activities JSONB, -- Array van interventies
based_on_intake_id UUID,
based_on_anamneses UUID[],
based_on_examinations UUID[],
based_on_risk_assessments UUID[],
care_team_ids UUID[],
author_id UUID,
period_start DATE,
period_end DATE,
created_at TIMESTAMP,
updated_at TIMESTAMP
)
-- Conditions (diagnoses - input voor AI)
conditions (
id UUID,
patient_id UUID,
code TEXT, -- DSM-5 code
code_system TEXT,
display_text TEXT,
category TEXT,
clinical_status TEXT,
severity TEXT, -- laag | middel | hoog
encounter_id UUID
)
-- Intakes (bron voor AI context)
intakes (
id UUID,
patient_id UUID,
status intake_status,
treatment_advice JSONB,
kindcheck_data JSONB,
notes TEXT
)
```
### 4.2 Nieuwe Velden / Migratie
```sql
-- Migratie: Leefgebieden toevoegen aan intakes
ALTER TABLE intakes
ADD COLUMN life_domains JSONB;
-- Migratie: Behandelplan specifieke velden aan care_plans
ALTER TABLE care_plans
ADD COLUMN version INTEGER DEFAULT 1,
ADD COLUMN published_at TIMESTAMP,
ADD COLUMN behandelstructuur JSONB,
ADD COLUMN evaluatiemomenten JSONB,
ADD COLUMN veiligheidsplan JSONB;
-- Constraint voor versie-beheer
ALTER TABLE care_plans
ADD CONSTRAINT unique_patient_version UNIQUE (patient_id, version);
```
### 4.3 JSONB Structuren
**life_domains (in intakes):**
```typescript
interface LifeDomainScore {
domain: 'dlv' | 'wonen' | 'werk' | 'sociaal' | 'vrijetijd' | 'financien' | 'gezondheid'
baseline: number // 1-5
current: number // 1-5
target: number // 1-5
notes: string
priority: 'laag' | 'middel' | 'hoog'
}
// life_domains: LifeDomainScore[]
```
**goals (in care_plans):**
```typescript
interface SmartGoal {
id: string
title: string
description: string
clientVersion: string // B1-taal
lifeDomain: LifeDomain
priority: 'hoog' | 'middel' | 'laag'
measurability: string
timelineWeeks: number
status: 'niet_gestart' | 'bezig' | 'gehaald' | 'bijgesteld'
progress: number // 0-100
}
```
**activities (in care_plans):**
```typescript
interface Intervention {
id: string
name: string
description: string
rationale: string
linkedGoalIds: string[]
}
```
**behandelstructuur:**
```typescript
interface Behandelstructuur {
duur: string // "8 weken"
frequentie: string // "Wekelijks"
aantalSessies: number // 8
vorm: string // "Individueel"
}
```
**evaluatiemomenten:**
```typescript
interface Evaluatiemoment {
id: string
type: 'tussentijds' | 'eind' | 'crisis'
weekNumber: number
plannedDate: string
actualDate?: string
status: 'gepland' | 'afgerond' | 'overgeslagen'
outcome?: string
lifeDomainUpdates?: LifeDomainScore[]
}
```
### 4.4 ERD
```
patients ─1:N─ intakes ─1:1─ life_domains (JSONB)
│ │
│ └────────── anamneses ─────────┐
│ └────────── examinations ──────┤
│ └────────── risk_assessments ──┤
│ │
└─1:N─ care_plans ────────────────────────────┘
│ (based_on_*)
├── goals (JSONB)
├── activities (JSONB)
├── behandelstructuur (JSONB)
├── evaluatiemomenten (JSONB)
└── veiligheidsplan (JSONB)
└─1:N─ conditions (diagnoses - input voor AI)
```
---
## 5. API Ontwerp
### 5.1 Endpoints Overzicht
| Endpoint | Method | Input | Output | Auth |
|----------|--------|-------|--------|------|
| `/api/behandelplan/generate` | POST | GenerateInput | GeneratedPlan | Required |
| `/api/behandelplan/regenerate-section` | POST | RegenerateInput | RegeneratedSection | Required |
### 5.2 POST /api/behandelplan/generate
**Request:**
```typescript
interface GenerateInput {
patientId: string // UUID
intakeId: string // UUID
conditionId?: string // UUID (optioneel, haalt anders laatste op)
extraInstructions?: string // Optionele aanvullende instructies
}
```
**Response:**
```typescript
interface GeneratedPlan {
behandelstructuur: Behandelstructuur
doelen: SmartGoal[]
interventies: Intervention[]
evaluatiemomenten: Evaluatiemoment[]
veiligheidsplan?: Veiligheidsplan // Alleen bij severity "Hoog"
}
```
**Error Responses:**
- `400`: Validation error (missing fields, invalid UUIDs)
- `401`: Unauthorized
- `404`: Patient/Intake/Condition not found
- `422`: Insufficient data for generation (no intake notes, no diagnosis)
- `500`: AI API error
- `503`: AI service unavailable
### 5.3 POST /api/behandelplan/regenerate-section
**Request:**
```typescript
interface RegenerateInput {
patientId: string
carePlanId: string
sectionType: 'goal' | 'intervention'
sectionId: string
instruction?: string // Extra instructie voor AI
currentPlan: GeneratedPlan // Context van huidige plan
}
```
**Response:**
```typescript
interface RegeneratedSection {
type: 'goal' | 'intervention'
original: SmartGoal | Intervention
regenerated: SmartGoal | Intervention
}
```
### 5.4 Zod Validation Schemas
```typescript
// lib/types/behandelplan.ts
export const GenerateInputSchema = z.object({
patientId: z.string().uuid(),
intakeId: z.string().uuid(),
conditionId: z.string().uuid().optional(),
extraInstructions: z.string().max(500).optional(),
});
export const RegenerateInputSchema = z.object({
patientId: z.string().uuid(),
carePlanId: z.string().uuid(),
sectionType: z.enum(['goal', 'intervention']),
sectionId: z.string().uuid(),
instruction: z.string().max(200).optional(),
currentPlan: GeneratedPlanSchema,
});
```
---
## 6. Security & Compliance
### 6.1 Security Checklist
- [x] **Authentication:** Supabase Auth (bestaand)
- [x] **Authorization:** Row Level Security op care_plans
- [x] **Data Encryption:** At rest (PostgreSQL), in transit (HTTPS)
- [x] **Input Validation:** Zod schemas op alle endpoints
- [ ] **Rate Limiting:** Toe te voegen op AI endpoints (10 req/min)
- [x] **CORS:** Restrictive origins (bestaand)
- [x] **Secrets:** Environment variables (ANTHROPIC_API_KEY)
### 6.2 RLS Policies voor care_plans
```sql
-- Bestaande RLS policy uitbreiden
ALTER TABLE care_plans ENABLE ROW LEVEL SECURITY;
-- Behandelaars kunnen alleen eigen patiënten zien
CREATE POLICY "Users can view care plans for their patients"
ON care_plans FOR SELECT
USING (
auth.uid() IN (
SELECT practitioner_id FROM patient_practitioners
WHERE patient_id = care_plans.patient_id
)
);
-- Behandelaars kunnen care plans maken voor eigen patiënten
CREATE POLICY "Users can create care plans for their patients"
ON care_plans FOR INSERT
WITH CHECK (
auth.uid() = author_id
);
-- Behandelaars kunnen eigen care plans updaten
CREATE POLICY "Users can update their care plans"
ON care_plans FOR UPDATE
USING (auth.uid() = author_id);
```
### 6.3 AVG/GDPR Overwegingen
- **Data minimalisatie:** Alleen noodzakelijke velden in AI prompt
- **Geen BSN/identificerende data naar AI:** Alleen intake notities en scores
- **Audit trail:** Bestaande `ai_events` tabel loggen van AI calls
- **Consent:** AI-gebruik gedekt onder behandelrelatie
---
## 7. AI/LLM Integratie
### 7.1 AI Stack
| Component | Waarde |
|-----------|--------|
| Provider | Anthropic |
| Model | claude-3-5-sonnet-20240620 (of claude-sonnet-4) |
| Library | Native fetch (geen SDK nodig) |
| Caching | Geen (elke generatie is uniek) |
| Fallback | Error message + manual mode optie |
### 7.2 Prompt Template
```typescript
// lib/ai/behandelplan-prompt.ts
export const BEHANDELPLAN_SYSTEM_PROMPT = `
Je bent een ervaren GGZ-behandelaar die behandelplannen opstelt.
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)
5. Genereer voor elk doel een B1-taal versie (cliënt-vriendelijk)
6. Kies evidence-based interventies passend bij de DSM-categorie
7. Plan 8-12 sessies afhankelijk van severity
8. Voeg veiligheidsplan toe alleen bij severity "Hoog"
OUTPUT FORMAT:
Retourneer ALLEEN valide JSON volgens het volgende schema:
{
"behandelstructuur": {
"duur": "8 weken",
"frequentie": "Wekelijks",
"aantalSessies": 8,
"vorm": "Individueel"
},
"doelen": [...],
"interventies": [...],
"evaluatiemomenten": [...],
"veiligheidsplan": null | {...}
}
`;
export function buildUserPrompt(context: PlanContext): string {
return `
CLIËNT CONTEXT:
- Intake notities: ${context.intakeNotes}
- DSM-categorie: ${context.dsmCategory}
- Severity: ${context.severity}
LEEFGEBIEDEN SCORES:
${context.lifeDomains.map(d =>
`- ${d.domain}: ${d.baseline}/5 (prioriteit: ${d.priority})`
).join('\n')}
${context.extraInstructions ? `EXTRA INSTRUCTIES:\n${context.extraInstructions}` : ''}
Genereer nu een behandelplan.
`;
}
```
### 7.3 Evidence-Based Mapping
```typescript
// lib/ai/intervention-mapping.ts
export const INTERVENTION_MAPPING: Record<string, InterventionSuggestion[]> = {
'angststoornissen': [
{ name: 'CGT', sessions: { laag: 8, middel: 10, hoog: 14 } },
{ name: 'Exposure therapie', sessions: { laag: 6, middel: 8, hoog: 12 } },
{ name: 'ACT', sessions: { laag: 8, middel: 10, hoog: 12 } },
],
'stemmingsklachten': [
{ name: 'CGT', sessions: { laag: 8, middel: 10, hoog: 14 } },
{ name: 'IPT', sessions: { laag: 8, middel: 12, hoog: 16 } },
{ name: 'Gedragsactivatie', sessions: { laag: 6, middel: 8, hoog: 10 } },
],
'trauma_ptss': [
{ name: 'EMDR', sessions: { laag: 6, middel: 10, hoog: 16 } },
{ name: 'Narratieve therapie', sessions: { laag: 8, middel: 12, hoog: 16 } },
],
'persoonlijkheid': [
{ name: 'Schematherapie', sessions: { laag: 16, middel: 24, hoog: 40 } },
{ name: 'MBT', sessions: { laag: 16, middel: 24, hoog: 40 } },
],
};
```
### 7.4 API Call Implementatie
```typescript
// app/api/behandelplan/generate/route.ts
export async function POST(request: NextRequest) {
// 1. Validate input
const body = await request.json();
const input = GenerateInputSchema.parse(body);
// 2. Load context from database
const context = await loadPlanContext(input);
// 3. Build prompt
const messages = [
{ role: 'system', content: BEHANDELPLAN_SYSTEM_PROMPT },
{ role: 'user', content: buildUserPrompt(context) },
];
// 4. Call Claude API
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ANTHROPIC_API_KEY!,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20240620',
max_tokens: 4096,
temperature: 0.3,
messages,
}),
});
// 5. Parse and validate response
const result = await response.json();
const plan = GeneratedPlanSchema.parse(
JSON.parse(result.content[0].text)
);
// 6. Log to ai_events
await logAIEvent('behandelplan_generate', input, plan);
return NextResponse.json(plan);
}
```
---
## 8. Performance & Scalability
### 8.1 Performance Targets
| Metric | Target | Huidige Baseline |
|--------|--------|------------------|
| Page load (FCP) | < 1.5s | ~1s (andere pagina's) |
| API response | < 500ms | ~300ms (intakes) |
| AI generation | < 8s | N/A (nieuw) |
| Auto-save | < 500ms | ~300ms (reports) |
### 8.2 Optimalisaties
**Frontend:**
- Server Components voor initial load (geen client JS voor data)
- Skeleton loaders tijdens AI generatie
- Optimistic updates voor status wijzigingen
**Backend:**
- Parallel database queries voor context loading
- Geen caching van AI responses (elke generatie uniek)
- Connection pooling via Supabase (bestaand)
**AI:**
- Max tokens: 4096 (voldoende voor plan JSON)
- Temperature: 0.3 (consistent maar niet robotisch)
- Retry logic: 2x met exponential backoff
---
## 9. Deployment & CI/CD
### 9.1 Omgevingen (bestaand)
| Omgeving | URL | Database |
|----------|-----|----------|
| Development | localhost:3000 | Local Supabase |
| Preview | Vercel preview | Supabase preview branch |
| Production | [main domain] | Supabase production |
### 9.2 Migratie Workflow (Cloud-only)
```bash
# Geen lokale Supabase instantie - direct naar cloud
# Optie 1: Via Supabase MCP tool
mcp__supabase__apply_migration(name, query)
# Optie 2: Via Supabase CLI
npx supabase db push --linked
# Types genereren na migratie
mcp__supabase__generate_typescript_types
```
**Let op:** Geen `supabase db reset` mogelijk - migraties zijn direct productie.
### 9.3 Deployment Checklist
- [ ] Environment variables in Vercel dashboard
- [ ] Database migraties toegepast
- [ ] TypeScript types gegenereerd (`supabase gen types typescript`)
- [ ] Build succesvol (`pnpm build`)
- [ ] Smoke test op preview environment
---
## 10. Monitoring & Logging
### 10.1 AI Event Logging (bestaand)
```sql
-- Bestaande ai_events tabel
ai_events (
id UUID,
kind TEXT, -- 'behandelplan_generate' | 'behandelplan_regenerate'
request JSONB, -- Input parameters
response JSONB, -- Generated plan
duration_ms INTEGER,
created_at TIMESTAMP
)
```
### 10.2 Metrics te Tracken
| Metric | Doel | Actie bij Overschrijding |
|--------|------|--------------------------|
| AI success rate | > 95% | Check prompts, input validation |
| AI response time p95 | < 8s | Optimize prompt size |
| Error rate | < 2% | Alert + investigate |
---
## 11. Risico's & Technische Mitigatie
| Risico | Impact | Kans | Mitigatie |
|--------|--------|------|-----------|
| AI genereert invalide JSON | Hoog | Middel | Zod validation, retry logic, fallback |
| AI API down/rate limited | Hoog | Laag | Error message, manual mode optie |
| Grote intake teksten (token limit) | Middel | Middel | Truncate/summarize intake eerst |
| Inconsistente B1-taal kwaliteit | Middel | Middel | Post-processing, behandelaar review |
| Performance bij grote plannen | Laag | Laag | Pagination, lazy loading |
---
## 12. Implementatie Volgorde
### Stap 1: Types & Database (2-3 uur)
**Bestanden:**
```
lib/types/
├── behandelplan.ts # Nieuwe types + Zod schemas
└── leefgebieden.ts # Life domain types
supabase/migrations/
└── xxx_add_behandelplan_fields.sql
```
### Stap 2: Leefgebieden Componenten (3-4 uur)
**Bestanden:**
```
components/behandelplan/
├── leefgebieden-form.tsx # Intake formulier (7 sliders)
├── leefgebieden-scores.tsx # Progress bar weergave
└── leefgebieden-badge.tsx # Domain tag/badge
```
### Stap 3: AI Generatie (3-4 uur)
**Bestanden:**
```
lib/ai/
├── behandelplan-prompt.ts # System + user prompts
└── intervention-mapping.ts # Evidence-based mapping
app/api/behandelplan/
├── generate/route.ts # POST endpoint
└── regenerate-section/route.ts # Micro-regeneratie
```
### Stap 4: Behandelplan UI (6-8 uur)
**Bestanden:**
```
app/epd/patients/[id]/behandelplan/
├── page.tsx # Server component (vervang placeholder)
├── actions.ts # Server actions (CRUD)
└── components/
├── behandelplan-view.tsx
├── goals-section.tsx
├── goal-card.tsx
├── interventions-section.tsx
└── generate-button.tsx
```
---
## 13. Bijlagen & Referenties
### Projectdocumenten
- [PRD Behandelplan v2.0](./prd-behandelplan-v2-final.md)
- [FO Behandelplan v1.0](./fo-behandelplan-v1.md)
- [UX Stylesheet](../ux-stylesheet.md)
### Tech Documentatie
- Next.js: https://nextjs.org/docs
- Supabase: https://supabase.com/docs
- Anthropic Claude: https://docs.anthropic.com/claude/reference
### Bestaande Code Referenties
- API pattern: `/app/api/reports/classify/route.ts`
- Server actions: `/app/epd/patients/[id]/intakes/[intakeId]/actions.ts`
- Types pattern: `/lib/types/report.ts`
- AI integration: `/app/api/docs/chat/route.ts`
---
**Document Status:** v1.0 Draft
**Volgende Review:** Na implementatie Stap 1-2
**Eigenaar:** Colin van Zeeland

View File

@@ -9,7 +9,7 @@ Afhankelijk van de **complexiteit van je software** bepaal je zelf hoe gedetaill
**Projectnaam:** _[vul in]_ **Projectnaam:** _[vul in]_
**Versie:** _v1.0_ **Versie:** _v1.0_
**Datum:** _[dd-mm-jjjj]_ **Datum:** _[dd-mm-jjjj]_
**Auteur:** _[naam]_ **Auteur:** _[Colin Lit]_
--- ---
@@ -155,6 +155,8 @@ export const loadData = async () => {
| E3 | AI-integratie | AI endpoints en prompt engineering | ⏳ To Do | 3 | Test met Gemini model | | E3 | AI-integratie | AI endpoints en prompt engineering | ⏳ To Do | 3 | Test met Gemini model |
| E4 | Testing & Deploy | QA, demo prep en deployment | ⏳ To Do | 3 | | | 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.
--- ---
## 4. Epics & Stories (Uitwerking) ## 4. Epics & Stories (Uitwerking)

View File

@@ -3,7 +3,7 @@
**Projectnaam:** _[vul in]_ **Projectnaam:** _[vul in]_
**Versie:** _v1.0_ **Versie:** _v1.0_
**Datum:** _[dd-mm-jjjj]_ **Datum:** _[dd-mm-jjjj]_
**Auteur:** _[naam]_ **Auteur:** Colin Lit
--- ---

View File

@@ -3,7 +3,7 @@
**Projectnaam:** _[vul in]_ **Projectnaam:** _[vul in]_
**Versie:** _v1.0_ **Versie:** _v1.0_
**Datum:** _[dd-mm-jjjj]_ **Datum:** _[dd-mm-jjjj]_
**Auteur:** _[naam]_ **Auteur:** Colin Lit
--- ---

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
}

View File

@@ -23,7 +23,11 @@
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-toast": "^1.2.15",
"@react-three/drei": "^10.7.7", "@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.4.0", "@react-three/fiber": "^9.4.0",

191
pnpm-lock.yaml generated
View File

@@ -41,9 +41,21 @@ importers:
'@radix-ui/react-icons': '@radix-ui/react-icons':
specifier: ^1.3.2 specifier: ^1.3.2
version: 1.3.2(react@18.3.1) version: 1.3.2(react@18.3.1)
'@radix-ui/react-label':
specifier: ^2.1.8
version: 2.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-progress':
specifier: ^1.1.8
version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slider':
specifier: ^1.3.6
version: 1.3.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': '@radix-ui/react-slot':
specifier: ^1.2.4 specifier: ^1.2.4
version: 1.2.4(@types/react@18.3.27)(react@18.3.1) version: 1.2.4(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-tabs':
specifier: ^1.1.13
version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-toast': '@radix-ui/react-toast':
specifier: ^1.2.15 specifier: ^1.2.15
version: 1.2.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 1.2.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -553,6 +565,9 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
'@radix-ui/primitive@1.1.3': '@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
@@ -613,6 +628,15 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-context@1.1.3':
resolution: {integrity: sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dialog@1.1.15': '@radix-ui/react-dialog@1.1.15':
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
peerDependencies: peerDependencies:
@@ -697,6 +721,19 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-label@2.1.8':
resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-menu@2.1.16': '@radix-ui/react-menu@2.1.16':
resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==}
peerDependencies: peerDependencies:
@@ -762,6 +799,32 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-primitive@2.1.4':
resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-progress@1.1.8':
resolution: {integrity: sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-roving-focus@1.1.11': '@radix-ui/react-roving-focus@1.1.11':
resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
peerDependencies: peerDependencies:
@@ -775,6 +838,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-slider@1.3.6':
resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.2.3': '@radix-ui/react-slot@1.2.3':
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
peerDependencies: peerDependencies:
@@ -793,6 +869,19 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-tabs@1.1.13':
resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-toast@1.2.15': '@radix-ui/react-toast@1.2.15':
resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==} resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==}
peerDependencies: peerDependencies:
@@ -851,6 +940,15 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-use-previous@1.1.1':
resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-rect@1.1.1': '@radix-ui/react-use-rect@1.1.1':
resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
peerDependencies: peerDependencies:
@@ -4104,6 +4202,8 @@ snapshots:
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
'@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.1.3': {} '@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
@@ -4153,6 +4253,12 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.27 '@types/react': 18.3.27
'@radix-ui/react-context@1.1.3(@types/react@18.3.27)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.27
'@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/primitive': 1.1.3 '@radix-ui/primitive': 1.1.3
@@ -4237,6 +4343,15 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.27 '@types/react': 18.3.27
'@radix-ui/react-label@2.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.27
'@types/react-dom': 18.3.7(@types/react@18.3.27)
'@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/primitive': 1.1.3 '@radix-ui/primitive': 1.1.3
@@ -4310,6 +4425,25 @@ snapshots:
'@types/react': 18.3.27 '@types/react': 18.3.27
'@types/react-dom': 18.3.7(@types/react@18.3.27) '@types/react-dom': 18.3.7(@types/react@18.3.27)
'@radix-ui/react-primitive@2.1.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-slot': 1.2.4(@types/react@18.3.27)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.27
'@types/react-dom': 18.3.7(@types/react@18.3.27)
'@radix-ui/react-progress@1.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-context': 1.1.3(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.27
'@types/react-dom': 18.3.7(@types/react@18.3.27)
'@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/primitive': 1.1.3 '@radix-ui/primitive': 1.1.3
@@ -4327,6 +4461,25 @@ snapshots:
'@types/react': 18.3.27 '@types/react': 18.3.27
'@types/react-dom': 18.3.7(@types/react@18.3.27) '@types/react-dom': 18.3.7(@types/react@18.3.27)
'@radix-ui/react-slider@1.3.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/number': 1.1.1
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-use-size': 1.1.1(@types/react@18.3.27)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.27
'@types/react-dom': 18.3.7(@types/react@18.3.27)
'@radix-ui/react-slot@1.2.3(@types/react@18.3.27)(react@18.3.1)': '@radix-ui/react-slot@1.2.3(@types/react@18.3.27)(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1) '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1)
@@ -4341,6 +4494,22 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.27 '@types/react': 18.3.27
'@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.27
'@types/react-dom': 18.3.7(@types/react@18.3.27)
'@radix-ui/react-toast@1.2.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@radix-ui/react-toast@1.2.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/primitive': 1.1.3 '@radix-ui/primitive': 1.1.3
@@ -4395,6 +4564,12 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.27 '@types/react': 18.3.27
'@radix-ui/react-use-previous@1.1.1(@types/react@18.3.27)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.27
'@radix-ui/react-use-rect@1.1.1(@types/react@18.3.27)(react@18.3.1)': '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.27)(react@18.3.1)':
dependencies: dependencies:
'@radix-ui/rect': 1.1.1 '@radix-ui/rect': 1.1.1
@@ -5580,8 +5755,8 @@ snapshots:
'@typescript-eslint/parser': 8.47.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': 8.47.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1)
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
@@ -5600,7 +5775,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
dependencies: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.3 debug: 4.4.3
@@ -5611,22 +5786,22 @@ snapshots:
tinyglobby: 0.2.15 tinyglobby: 0.2.15
unrs-resolver: 1.11.1 unrs-resolver: 1.11.1
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): eslint-module-utils@2.12.1(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.47.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': 8.47.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
dependencies: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.9 array-includes: 3.1.9
@@ -5637,7 +5812,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
hasown: 2.0.2 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3