| 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:
14
components/behandelplan/index.ts
Normal file
14
components/behandelplan/index.ts
Normal 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';
|
||||
77
components/behandelplan/leefgebieden-badge.tsx
Normal file
77
components/behandelplan/leefgebieden-badge.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
368
components/behandelplan/leefgebieden-form.tsx
Normal file
368
components/behandelplan/leefgebieden-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
186
components/behandelplan/leefgebieden-scores.tsx
Normal file
186
components/behandelplan/leefgebieden-scores.tsx
Normal 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
36
components/ui/badge.tsx
Normal 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
76
components/ui/card.tsx
Normal 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
26
components/ui/label.tsx
Normal 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 }
|
||||
28
components/ui/progress.tsx
Normal file
28
components/ui/progress.tsx
Normal 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
28
components/ui/slider.tsx
Normal 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
55
components/ui/tabs.tsx
Normal 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 }
|
||||
22
components/ui/textarea.tsx
Normal file
22
components/ui/textarea.tsx
Normal 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 }
|
||||
438
docs/specs/behandelplan/bouwplan-behandelplan-v1.md
Normal file
438
docs/specs/behandelplan/bouwplan-behandelplan-v1.md
Normal 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 |
|
||||
566
docs/specs/behandelplan/fo-behandelplan-v1.md
Normal file
566
docs/specs/behandelplan/fo-behandelplan-v1.md
Normal 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
|
||||
@@ -3,7 +3,7 @@
|
||||
**Projectnaam:** Mini-ECD Prototype - AI Speedrun
|
||||
**Versie:** v2.0 (volgens template, incl. Leefgebieden)
|
||||
**Datum:** 2 december 2024
|
||||
**Auteur:** Colin van Zeeland
|
||||
**Auteur:** Colin Lit
|
||||
|
||||
**Changelog:**
|
||||
- v2.0: Herstructurering volgens PRD template, duidelijke MVP/post-MVP scheiding, expliciete UX sectie
|
||||
|
||||
719
docs/specs/behandelplan/to-behandelplan-v1.md
Normal file
719
docs/specs/behandelplan/to-behandelplan-v1.md
Normal 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
|
||||
4
docs/templates/bouwplan_template.md
vendored
4
docs/templates/bouwplan_template.md
vendored
@@ -9,7 +9,7 @@ Afhankelijk van de **complexiteit van je software** bepaal je zelf hoe gedetaill
|
||||
**Projectnaam:** _[vul in]_
|
||||
**Versie:** _v1.0_
|
||||
**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 |
|
||||
| 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)
|
||||
|
||||
2
docs/templates/fo_template.md
vendored
2
docs/templates/fo_template.md
vendored
@@ -3,7 +3,7 @@
|
||||
**Projectnaam:** _[vul in]_
|
||||
**Versie:** _v1.0_
|
||||
**Datum:** _[dd-mm-jjjj]_
|
||||
**Auteur:** _[naam]_
|
||||
**Auteur:** Colin Lit
|
||||
|
||||
---
|
||||
|
||||
|
||||
2
docs/templates/prd_template.md
vendored
2
docs/templates/prd_template.md
vendored
@@ -3,7 +3,7 @@
|
||||
**Projectnaam:** _[vul in]_
|
||||
**Versie:** _v1.0_
|
||||
**Datum:** _[dd-mm-jjjj]_
|
||||
**Auteur:** _[naam]_
|
||||
**Auteur:** Colin Lit
|
||||
|
||||
---
|
||||
|
||||
|
||||
370
lib/types/behandelplan.ts
Normal file
370
lib/types/behandelplan.ts
Normal 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
167
lib/types/leefgebieden.ts
Normal 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
|
||||
}
|
||||
@@ -23,7 +23,11 @@
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@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-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^9.4.0",
|
||||
|
||||
191
pnpm-lock.yaml
generated
191
pnpm-lock.yaml
generated
@@ -41,9 +41,21 @@ importers:
|
||||
'@radix-ui/react-icons':
|
||||
specifier: ^1.3.2
|
||||
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':
|
||||
specifier: ^1.2.4
|
||||
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':
|
||||
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)
|
||||
@@ -553,6 +565,9 @@ packages:
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@radix-ui/number@1.1.1':
|
||||
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
|
||||
|
||||
'@radix-ui/primitive@1.1.3':
|
||||
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
||||
|
||||
@@ -613,6 +628,15 @@ packages:
|
||||
'@types/react':
|
||||
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':
|
||||
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
|
||||
peerDependencies:
|
||||
@@ -697,6 +721,19 @@ packages:
|
||||
'@types/react':
|
||||
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':
|
||||
resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==}
|
||||
peerDependencies:
|
||||
@@ -762,6 +799,32 @@ packages:
|
||||
'@types/react-dom':
|
||||
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':
|
||||
resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
|
||||
peerDependencies:
|
||||
@@ -775,6 +838,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
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':
|
||||
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
||||
peerDependencies:
|
||||
@@ -793,6 +869,19 @@ packages:
|
||||
'@types/react':
|
||||
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':
|
||||
resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==}
|
||||
peerDependencies:
|
||||
@@ -851,6 +940,15 @@ packages:
|
||||
'@types/react':
|
||||
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':
|
||||
resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
|
||||
peerDependencies:
|
||||
@@ -4104,6 +4202,8 @@ snapshots:
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/number@1.1.1': {}
|
||||
|
||||
'@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)':
|
||||
@@ -4153,6 +4253,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@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)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
@@ -4237,6 +4343,15 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@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)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
@@ -4310,6 +4425,25 @@ snapshots:
|
||||
'@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)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
@@ -4327,6 +4461,25 @@ snapshots:
|
||||
'@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)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1)
|
||||
@@ -4341,6 +4494,22 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@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)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
@@ -4395,6 +4564,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@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)':
|
||||
dependencies:
|
||||
'@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)
|
||||
eslint: 8.57.1
|
||||
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-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-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-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-react: 7.37.5(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
|
||||
@@ -5600,7 +5775,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
@@ -5611,22 +5786,22 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
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:
|
||||
- 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:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.47.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
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:
|
||||
- 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:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
@@ -5637,7 +5812,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.1
|
||||
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
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
|
||||
Reference in New Issue
Block a user