| 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
|
**Projectnaam:** Mini-ECD Prototype - AI Speedrun
|
||||||
**Versie:** v2.0 (volgens template, incl. Leefgebieden)
|
**Versie:** v2.0 (volgens template, incl. Leefgebieden)
|
||||||
**Datum:** 2 december 2024
|
**Datum:** 2 december 2024
|
||||||
**Auteur:** Colin van Zeeland
|
**Auteur:** Colin Lit
|
||||||
|
|
||||||
**Changelog:**
|
**Changelog:**
|
||||||
- v2.0: Herstructurering volgens PRD template, duidelijke MVP/post-MVP scheiding, expliciete UX sectie
|
- v2.0: Herstructurering volgens PRD template, duidelijke MVP/post-MVP scheiding, expliciete UX sectie
|
||||||
|
|||||||
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]_
|
**Projectnaam:** _[vul in]_
|
||||||
**Versie:** _v1.0_
|
**Versie:** _v1.0_
|
||||||
**Datum:** _[dd-mm-jjjj]_
|
**Datum:** _[dd-mm-jjjj]_
|
||||||
**Auteur:** _[naam]_
|
**Auteur:** _[Colin Lit]_
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -155,6 +155,8 @@ export const loadData = async () => {
|
|||||||
| E3 | AI-integratie | AI endpoints en prompt engineering | ⏳ To Do | 3 | Test met Gemini model |
|
| E3 | AI-integratie | AI endpoints en prompt engineering | ⏳ To Do | 3 | Test met Gemini model |
|
||||||
| E4 | Testing & Deploy | QA, demo prep en deployment | ⏳ To Do | 3 | |
|
| E4 | Testing & Deploy | QA, demo prep en deployment | ⏳ To Do | 3 | |
|
||||||
|
|
||||||
|
**Belangrijk:** Voer niet in 1x het volledige plan uit. Bouw per epic en per story.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Epics & Stories (Uitwerking)
|
## 4. Epics & Stories (Uitwerking)
|
||||||
|
|||||||
2
docs/templates/fo_template.md
vendored
2
docs/templates/fo_template.md
vendored
@@ -3,7 +3,7 @@
|
|||||||
**Projectnaam:** _[vul in]_
|
**Projectnaam:** _[vul in]_
|
||||||
**Versie:** _v1.0_
|
**Versie:** _v1.0_
|
||||||
**Datum:** _[dd-mm-jjjj]_
|
**Datum:** _[dd-mm-jjjj]_
|
||||||
**Auteur:** _[naam]_
|
**Auteur:** Colin Lit
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
2
docs/templates/prd_template.md
vendored
2
docs/templates/prd_template.md
vendored
@@ -3,7 +3,7 @@
|
|||||||
**Projectnaam:** _[vul in]_
|
**Projectnaam:** _[vul in]_
|
||||||
**Versie:** _v1.0_
|
**Versie:** _v1.0_
|
||||||
**Datum:** _[dd-mm-jjjj]_
|
**Datum:** _[dd-mm-jjjj]_
|
||||||
**Auteur:** _[naam]_
|
**Auteur:** Colin Lit
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
370
lib/types/behandelplan.ts
Normal file
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-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-toast": "^1.2.15",
|
"@radix-ui/react-toast": "^1.2.15",
|
||||||
"@react-three/drei": "^10.7.7",
|
"@react-three/drei": "^10.7.7",
|
||||||
"@react-three/fiber": "^9.4.0",
|
"@react-three/fiber": "^9.4.0",
|
||||||
|
|||||||
191
pnpm-lock.yaml
generated
191
pnpm-lock.yaml
generated
@@ -41,9 +41,21 @@ importers:
|
|||||||
'@radix-ui/react-icons':
|
'@radix-ui/react-icons':
|
||||||
specifier: ^1.3.2
|
specifier: ^1.3.2
|
||||||
version: 1.3.2(react@18.3.1)
|
version: 1.3.2(react@18.3.1)
|
||||||
|
'@radix-ui/react-label':
|
||||||
|
specifier: ^2.1.8
|
||||||
|
version: 2.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-progress':
|
||||||
|
specifier: ^1.1.8
|
||||||
|
version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-slider':
|
||||||
|
specifier: ^1.3.6
|
||||||
|
version: 1.3.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@radix-ui/react-slot':
|
'@radix-ui/react-slot':
|
||||||
specifier: ^1.2.4
|
specifier: ^1.2.4
|
||||||
version: 1.2.4(@types/react@18.3.27)(react@18.3.1)
|
version: 1.2.4(@types/react@18.3.27)(react@18.3.1)
|
||||||
|
'@radix-ui/react-tabs':
|
||||||
|
specifier: ^1.1.13
|
||||||
|
version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@radix-ui/react-toast':
|
'@radix-ui/react-toast':
|
||||||
specifier: ^1.2.15
|
specifier: ^1.2.15
|
||||||
version: 1.2.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.2.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
@@ -553,6 +565,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
'@radix-ui/number@1.1.1':
|
||||||
|
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
|
||||||
|
|
||||||
'@radix-ui/primitive@1.1.3':
|
'@radix-ui/primitive@1.1.3':
|
||||||
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
||||||
|
|
||||||
@@ -613,6 +628,15 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-context@1.1.3':
|
||||||
|
resolution: {integrity: sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-dialog@1.1.15':
|
'@radix-ui/react-dialog@1.1.15':
|
||||||
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
|
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -697,6 +721,19 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-label@2.1.8':
|
||||||
|
resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-menu@2.1.16':
|
'@radix-ui/react-menu@2.1.16':
|
||||||
resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==}
|
resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -762,6 +799,32 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-primitive@2.1.4':
|
||||||
|
resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-progress@1.1.8':
|
||||||
|
resolution: {integrity: sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-roving-focus@1.1.11':
|
'@radix-ui/react-roving-focus@1.1.11':
|
||||||
resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
|
resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -775,6 +838,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-slider@1.3.6':
|
||||||
|
resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.2.3':
|
'@radix-ui/react-slot@1.2.3':
|
||||||
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -793,6 +869,19 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-tabs@1.1.13':
|
||||||
|
resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-toast@1.2.15':
|
'@radix-ui/react-toast@1.2.15':
|
||||||
resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==}
|
resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -851,6 +940,15 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-use-previous@1.1.1':
|
||||||
|
resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-use-rect@1.1.1':
|
'@radix-ui/react-use-rect@1.1.1':
|
||||||
resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
|
resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4104,6 +4202,8 @@ snapshots:
|
|||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/number@1.1.1': {}
|
||||||
|
|
||||||
'@radix-ui/primitive@1.1.3': {}
|
'@radix-ui/primitive@1.1.3': {}
|
||||||
|
|
||||||
'@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
@@ -4153,6 +4253,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.27
|
'@types/react': 18.3.27
|
||||||
|
|
||||||
|
'@radix-ui/react-context@1.1.3(@types/react@18.3.27)(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.27
|
||||||
|
|
||||||
'@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -4237,6 +4343,15 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.27
|
'@types/react': 18.3.27
|
||||||
|
|
||||||
|
'@radix-ui/react-label@2.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.27
|
||||||
|
'@types/react-dom': 18.3.7(@types/react@18.3.27)
|
||||||
|
|
||||||
'@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -4310,6 +4425,25 @@ snapshots:
|
|||||||
'@types/react': 18.3.27
|
'@types/react': 18.3.27
|
||||||
'@types/react-dom': 18.3.7(@types/react@18.3.27)
|
'@types/react-dom': 18.3.7(@types/react@18.3.27)
|
||||||
|
|
||||||
|
'@radix-ui/react-primitive@2.1.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-slot': 1.2.4(@types/react@18.3.27)(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.27
|
||||||
|
'@types/react-dom': 18.3.7(@types/react@18.3.27)
|
||||||
|
|
||||||
|
'@radix-ui/react-progress@1.1.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-context': 1.1.3(@types/react@18.3.27)(react@18.3.1)
|
||||||
|
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.27
|
||||||
|
'@types/react-dom': 18.3.7(@types/react@18.3.27)
|
||||||
|
|
||||||
'@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -4327,6 +4461,25 @@ snapshots:
|
|||||||
'@types/react': 18.3.27
|
'@types/react': 18.3.27
|
||||||
'@types/react-dom': 18.3.7(@types/react@18.3.27)
|
'@types/react-dom': 18.3.7(@types/react@18.3.27)
|
||||||
|
|
||||||
|
'@radix-ui/react-slider@1.3.6(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/number': 1.1.1
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1)
|
||||||
|
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.27)(react@18.3.1)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.27)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.27)(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-size': 1.1.1(@types/react@18.3.27)(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.27
|
||||||
|
'@types/react-dom': 18.3.7(@types/react@18.3.27)
|
||||||
|
|
||||||
'@radix-ui/react-slot@1.2.3(@types/react@18.3.27)(react@18.3.1)':
|
'@radix-ui/react-slot@1.2.3(@types/react@18.3.27)(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.27)(react@18.3.1)
|
||||||
@@ -4341,6 +4494,22 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.27
|
'@types/react': 18.3.27
|
||||||
|
|
||||||
|
'@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@18.3.27)(react@18.3.1)
|
||||||
|
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.27)(react@18.3.1)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@18.3.27)(react@18.3.1)
|
||||||
|
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.27)(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.27
|
||||||
|
'@types/react-dom': 18.3.7(@types/react@18.3.27)
|
||||||
|
|
||||||
'@radix-ui/react-toast@1.2.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@radix-ui/react-toast@1.2.15(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.3
|
'@radix-ui/primitive': 1.1.3
|
||||||
@@ -4395,6 +4564,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.27
|
'@types/react': 18.3.27
|
||||||
|
|
||||||
|
'@radix-ui/react-use-previous@1.1.1(@types/react@18.3.27)(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.27
|
||||||
|
|
||||||
'@radix-ui/react-use-rect@1.1.1(@types/react@18.3.27)(react@18.3.1)':
|
'@radix-ui/react-use-rect@1.1.1(@types/react@18.3.27)(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/rect': 1.1.1
|
'@radix-ui/rect': 1.1.1
|
||||||
@@ -5580,8 +5755,8 @@ snapshots:
|
|||||||
'@typescript-eslint/parser': 8.47.0(eslint@8.57.1)(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.47.0(eslint@8.57.1)(typescript@5.9.3)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||||
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
|
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
|
||||||
@@ -5600,7 +5775,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
|
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@@ -5611,22 +5786,22 @@ snapshots:
|
|||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
unrs-resolver: 1.11.1
|
unrs-resolver: 1.11.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 8.47.0(eslint@8.57.1)(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.47.0(eslint@8.57.1)(typescript@5.9.3)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
@@ -5637,7 +5812,7 @@ snapshots:
|
|||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
|
|||||||
Reference in New Issue
Block a user