chore(strip): verwijder behandelplan-module
AI-generatie negeerde de geregistreerde diagnose en ernst — elk plan was generiek (reviewbevinding). Module komt terug bij de rebuild, gekoppeld aan het nieuwe datamodel. - app/epd/patients/[id]/behandelplan/ en /api/behandelplan/ verwijderd - components/behandelplan/ (view, list, forms) verwijderd - lib/ai/behandelplan-prompt.ts, lib/ai/intervention-mapping.ts, lib/types/behandelplan.ts verwijderd - behandelplan-sectie uit patientdashboard, dashboard-API en Cortex patient-dashboard-block - 'Doorzetten naar behandelplan' uit behandeladvies-formulier Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,125 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Plus, FileText, CheckCircle2 } from 'lucide-react';
|
||||
import { FHIR_STATUS_LABELS, type FhirCarePlanStatus } from '@/lib/types/behandelplan';
|
||||
|
||||
interface CarePlanSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
version: number | null;
|
||||
created_at: string | null;
|
||||
published_at: string | null;
|
||||
}
|
||||
|
||||
interface BehandelplanListProps {
|
||||
plans: CarePlanSummary[];
|
||||
selectedPlanId: string | null;
|
||||
onSelectPlan: (planId: string) => void;
|
||||
onCreateNew: () => void;
|
||||
isCreating?: boolean;
|
||||
}
|
||||
|
||||
export function BehandelplanList({
|
||||
plans,
|
||||
selectedPlanId,
|
||||
onSelectPlan,
|
||||
onCreateNew,
|
||||
isCreating = false,
|
||||
}: BehandelplanListProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<FileText className="h-4 w-4" />
|
||||
Behandelplannen ({plans.length})
|
||||
</CardTitle>
|
||||
<Button
|
||||
onClick={onCreateNew}
|
||||
disabled={isCreating}
|
||||
size="sm"
|
||||
className="bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Nieuw
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{plans.length === 0 ? (
|
||||
<p className="text-sm text-slate-500 text-center py-4">
|
||||
Nog geen behandelplannen aangemaakt
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{plans.map((plan) => {
|
||||
const isSelected = plan.id === selectedPlanId;
|
||||
const statusInfo = FHIR_STATUS_LABELS[plan.status as FhirCarePlanStatus] || {
|
||||
label: plan.status,
|
||||
color: '#6b7280',
|
||||
};
|
||||
const isActive = plan.status === 'active';
|
||||
|
||||
return (
|
||||
<button
|
||||
key={plan.id}
|
||||
onClick={() => onSelectPlan(plan.id)}
|
||||
className={`w-full text-left p-3 rounded-lg border transition-all ${
|
||||
isSelected
|
||||
? 'border-indigo-500 bg-indigo-50 ring-1 ring-indigo-500'
|
||||
: 'border-slate-200 hover:border-slate-300 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{isActive && (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
)}
|
||||
<span className={`font-medium text-sm ${isSelected ? 'text-indigo-900' : 'text-slate-900'}`}>
|
||||
{plan.title}
|
||||
</span>
|
||||
</div>
|
||||
<Badge
|
||||
style={{
|
||||
backgroundColor: statusInfo.color,
|
||||
color: 'white',
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
{statusInfo.label}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1 text-xs text-slate-500">
|
||||
{plan.version && <span>v{plan.version}</span>}
|
||||
{plan.created_at && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>
|
||||
{new Date(plan.created_at).toLocaleDateString('nl-NL', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{plan.published_at && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="text-green-600">Gepubliceerd</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,162 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, ReactNode } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Pencil, X, Check, Loader2 } from 'lucide-react';
|
||||
|
||||
interface EditableSectionProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: ReactNode;
|
||||
children: ReactNode;
|
||||
editForm?: ReactNode;
|
||||
onSave?: () => Promise<void>;
|
||||
onCancel?: () => void;
|
||||
isEditing?: boolean;
|
||||
onEditChange?: (isEditing: boolean) => void;
|
||||
canEdit?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function EditableSection({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
children,
|
||||
editForm,
|
||||
onSave,
|
||||
onCancel,
|
||||
isEditing: externalIsEditing,
|
||||
onEditChange,
|
||||
canEdit = true,
|
||||
className = '',
|
||||
}: EditableSectionProps) {
|
||||
const [internalIsEditing, setInternalIsEditing] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Use external or internal state
|
||||
const isEditing = externalIsEditing ?? internalIsEditing;
|
||||
const setIsEditing = onEditChange ?? setInternalIsEditing;
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!onSave) return;
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await onSave();
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
console.error('Error saving:', error);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
onCancel?.();
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
{icon}
|
||||
{title}
|
||||
</CardTitle>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</div>
|
||||
{canEdit && !isEditing && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isEditing && editForm ? (
|
||||
<div className="space-y-4">
|
||||
{editForm}
|
||||
<div className="flex justify-end gap-2 pt-2 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCancel}
|
||||
disabled={isSaving}
|
||||
>
|
||||
<X className="h-4 w-4 mr-1" />
|
||||
Annuleren
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
||||
) : (
|
||||
<Check className="h-4 w-4 mr-1" />
|
||||
)}
|
||||
Opslaan
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Simple inline edit buttons for list items
|
||||
interface ItemActionsProps {
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
isDeleting?: boolean;
|
||||
}
|
||||
|
||||
export function ItemActions({ onEdit, onDelete, isDeleting }: ItemActionsProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{onEdit && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
{onDelete && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-red-500 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<X className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { type Behandeldoel, GOAL_STATUS_LABELS } from '@/lib/types/behandelplan';
|
||||
import { LIFE_DOMAIN_META } from '@/lib/types/leefgebieden';
|
||||
import { Target, Pencil, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { BehandeldoelForm } from './behandeldoel-form';
|
||||
|
||||
interface BehandeldoelCardProps {
|
||||
doel: Behandeldoel;
|
||||
isEditing: boolean;
|
||||
onEdit: () => void;
|
||||
onSave: (doel: Behandeldoel) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
onDelete?: () => Promise<void>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Behandeldoel Card
|
||||
* View mode: Compact card met doel + interventies
|
||||
* Edit mode: Inline form met alle velden
|
||||
*/
|
||||
export function BehandeldoelCard({
|
||||
doel,
|
||||
isEditing,
|
||||
onEdit,
|
||||
onSave,
|
||||
onCancel,
|
||||
onDelete,
|
||||
className,
|
||||
}: BehandeldoelCardProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<BehandeldoelForm
|
||||
doel={doel}
|
||||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
onDelete={onDelete}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = LIFE_DOMAIN_META[doel.lifeDomain];
|
||||
const statusInfo = GOAL_STATUS_LABELS[doel.status];
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
'transition-all hover:border-indigo-300 hover:shadow-sm',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Target className="h-4 w-4 text-indigo-600 shrink-0" />
|
||||
<h3 className="font-medium text-slate-900 truncate">{doel.title}</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs border-0"
|
||||
style={{ backgroundColor: meta.color, color: 'white' }}
|
||||
>
|
||||
{meta.shortLabel}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
style={{ borderColor: statusInfo.color, color: statusInfo.color }}
|
||||
>
|
||||
{statusInfo.label}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-4 pt-0 space-y-3">
|
||||
{/* Client version (B1 tekst) - altijd zichtbaar */}
|
||||
<div className="bg-blue-50 border border-blue-100 rounded-md p-2.5">
|
||||
<p className="text-sm text-blue-800 italic">
|
||||
“{doel.clientVersion}”
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Interventies */}
|
||||
{doel.interventies.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs font-medium text-slate-500 uppercase tracking-wide">
|
||||
Aanpak
|
||||
</span>
|
||||
<ul className="space-y-1">
|
||||
{doel.interventies.map((int) => (
|
||||
<li key={int.id} className="flex items-start gap-2 text-sm">
|
||||
<span className="text-slate-400">•</span>
|
||||
<span>
|
||||
<span className="font-medium text-slate-700">{int.name}</span>
|
||||
{int.description && (
|
||||
<span className="text-slate-500"> - {int.description}</span>
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress & timeline */}
|
||||
<div className="flex items-center justify-between gap-4 pt-2">
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center justify-between text-xs text-slate-500">
|
||||
<span>Week {doel.startWeek}-{doel.endWeek}</span>
|
||||
<span>{doel.progress}%</span>
|
||||
</div>
|
||||
<Progress value={doel.progress} className="h-2" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-slate-500 h-8 w-8 p-0"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onEdit}
|
||||
className="text-slate-500 h-8 w-8 p-0 hover:text-indigo-600"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded details (optional) */}
|
||||
{isExpanded && (
|
||||
<div className="pt-2 border-t border-slate-100 text-xs text-slate-500 space-y-1">
|
||||
<p>Leefgebied: {meta.label}</p>
|
||||
<p>Status: {statusInfo.label}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,357 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
type Behandeldoel,
|
||||
type EmbeddedInterventie,
|
||||
type GoalStatus,
|
||||
GOAL_STATUSES,
|
||||
GOAL_STATUS_LABELS,
|
||||
createEmptyEmbeddedInterventie,
|
||||
} from '@/lib/types/behandelplan';
|
||||
import { type LifeDomain, LIFE_DOMAINS, LIFE_DOMAIN_META } from '@/lib/types/leefgebieden';
|
||||
import { Plus, X, Sparkles, Trash2, Save } from 'lucide-react';
|
||||
|
||||
interface BehandeldoelFormProps {
|
||||
doel: Behandeldoel;
|
||||
onSave: (doel: Behandeldoel) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
onDelete?: () => Promise<void>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline edit form voor Behandeldoel
|
||||
* Alle velden in één uitklapbare card
|
||||
*/
|
||||
export function BehandeldoelForm({
|
||||
doel,
|
||||
onSave,
|
||||
onCancel,
|
||||
onDelete,
|
||||
className,
|
||||
}: BehandeldoelFormProps) {
|
||||
const [formData, setFormData] = useState<Behandeldoel>(doel);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await onSave(formData);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!onDelete) return;
|
||||
if (!confirm('Weet je zeker dat je dit behandeldoel wilt verwijderen?')) return;
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await onDelete();
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateField = <K extends keyof Behandeldoel>(
|
||||
field: K,
|
||||
value: Behandeldoel[K]
|
||||
) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const addInterventie = () => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
interventies: [...prev.interventies, createEmptyEmbeddedInterventie()],
|
||||
}));
|
||||
};
|
||||
|
||||
const updateInterventie = (
|
||||
index: number,
|
||||
field: keyof EmbeddedInterventie,
|
||||
value: string
|
||||
) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
interventies: prev.interventies.map((int, i) =>
|
||||
i === index ? { ...int, [field]: value } : int
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
const removeInterventie = (index: number) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
interventies: prev.interventies.filter((_, i) => i !== index),
|
||||
}));
|
||||
};
|
||||
|
||||
const isValid =
|
||||
formData.title.trim().length >= 5 &&
|
||||
formData.clientVersion.trim().length >= 5;
|
||||
|
||||
return (
|
||||
<Card className={cn('border-indigo-300 shadow-md', className)}>
|
||||
<CardHeader className="p-4 pb-2 border-b bg-indigo-50/50">
|
||||
<CardTitle className="text-base font-medium text-indigo-900">
|
||||
Behandeldoel bewerken
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-4 space-y-4">
|
||||
{/* Doel titel */}
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="title" className="text-sm font-medium">
|
||||
Doel <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="title"
|
||||
value={formData.title}
|
||||
onChange={(e) => updateField('title', e.target.value)}
|
||||
placeholder="Bijv. Weer 4 dagen per week stabiel kunnen werken"
|
||||
className="text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Client versie (B1) */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="clientVersion" className="text-sm font-medium">
|
||||
Cliënt-versie (B1) <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs text-indigo-600 hover:text-indigo-700"
|
||||
disabled // TODO: Implementeer AI generatie
|
||||
>
|
||||
<Sparkles className="h-3 w-3 mr-1" />
|
||||
Genereer met AI
|
||||
</Button>
|
||||
</div>
|
||||
<Textarea
|
||||
id="clientVersion"
|
||||
value={formData.clientVersion}
|
||||
onChange={(e) => updateField('clientVersion', e.target.value)}
|
||||
placeholder="Bijv. Ik kan weer 4 dagen werken zonder veel stress"
|
||||
className="text-sm min-h-[60px] resize-none"
|
||||
/>
|
||||
<p className="text-xs text-slate-500">
|
||||
Formuleer in eenvoudige taal (B1-niveau) zodat de cliënt het begrijpt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Leefgebied & Periode - inline */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm font-medium">Leefgebied</Label>
|
||||
<Select
|
||||
value={formData.lifeDomain}
|
||||
onValueChange={(v) => updateField('lifeDomain', v as LifeDomain)}
|
||||
>
|
||||
<SelectTrigger className="text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LIFE_DOMAINS.map((domain) => {
|
||||
const meta = LIFE_DOMAIN_META[domain];
|
||||
return (
|
||||
<SelectItem key={domain} value={domain}>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{meta.emoji}</span>
|
||||
<span>{meta.shortLabel}</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm font-medium">Periode</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={52}
|
||||
value={formData.startWeek}
|
||||
onChange={(e) =>
|
||||
updateField('startWeek', parseInt(e.target.value) || 1)
|
||||
}
|
||||
className="w-16 text-sm text-center"
|
||||
/>
|
||||
<span className="text-slate-500 text-sm">t/m</span>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={52}
|
||||
value={formData.endWeek}
|
||||
onChange={(e) =>
|
||||
updateField('endWeek', parseInt(e.target.value) || 8)
|
||||
}
|
||||
className="w-16 text-sm text-center"
|
||||
/>
|
||||
<span className="text-slate-500 text-sm">weken</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interventies */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">Aanpak (interventies)</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={addInterventie}
|
||||
className="h-7 text-xs text-indigo-600 hover:text-indigo-700"
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Toevoegen
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{formData.interventies.length === 0 ? (
|
||||
<p className="text-sm text-slate-500 italic py-2">
|
||||
Nog geen interventies toegevoegd
|
||||
</p>
|
||||
) : (
|
||||
formData.interventies.map((int, index) => (
|
||||
<div
|
||||
key={int.id}
|
||||
className="flex items-start gap-2 p-2 bg-slate-50 rounded-md"
|
||||
>
|
||||
<div className="flex-1 grid grid-cols-3 gap-2">
|
||||
<Input
|
||||
value={int.name}
|
||||
onChange={(e) =>
|
||||
updateInterventie(index, 'name', e.target.value)
|
||||
}
|
||||
placeholder="CGT"
|
||||
className="text-sm"
|
||||
/>
|
||||
<Input
|
||||
value={int.description}
|
||||
onChange={(e) =>
|
||||
updateInterventie(index, 'description', e.target.value)
|
||||
}
|
||||
placeholder="Korte beschrijving"
|
||||
className="text-sm col-span-2"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeInterventie(index)}
|
||||
className="h-8 w-8 p-0 text-slate-400 hover:text-red-500"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status & Voortgang */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm font-medium">Status</Label>
|
||||
<Select
|
||||
value={formData.status}
|
||||
onValueChange={(v) => updateField('status', v as GoalStatus)}
|
||||
>
|
||||
<SelectTrigger className="text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{GOAL_STATUSES.map((status) => {
|
||||
const info = GOAL_STATUS_LABELS[status];
|
||||
return (
|
||||
<SelectItem key={status} value={status}>
|
||||
{info.label}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm font-medium">
|
||||
Voortgang: {formData.progress}%
|
||||
</Label>
|
||||
<Slider
|
||||
value={[formData.progress]}
|
||||
onValueChange={([v]) => updateField('progress', v)}
|
||||
min={0}
|
||||
max={100}
|
||||
step={5}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-between pt-2 border-t">
|
||||
{onDelete && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
{isDeleting ? 'Verwijderen...' : 'Verwijderen'}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onCancel}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Annuleren
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={!isValid || isSaving}
|
||||
className="bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
<Save className="h-4 w-4 mr-1" />
|
||||
{isSaving ? 'Opslaan...' : 'Opslaan'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
type Behandeldoel,
|
||||
type Behandelstructuur,
|
||||
type Evaluatiemoment,
|
||||
type Veiligheidsplan,
|
||||
type SmartGoal,
|
||||
type Intervention,
|
||||
type FhirCarePlanStatus,
|
||||
FHIR_STATUS_LABELS,
|
||||
transformToFlat,
|
||||
createEmptyBehandeldoel,
|
||||
calculateBehandeldoelenProgress,
|
||||
} from '@/lib/types/behandelplan';
|
||||
import { type LifeDomainScore } from '@/lib/types/leefgebieden';
|
||||
import { ContextHeader } from './context-header';
|
||||
import { BehandeldoelCard } from './behandeldoel-card';
|
||||
import { PlanningSection } from './planning-section';
|
||||
import { Plus, Sparkles, FileText, CheckCircle2 } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
|
||||
interface Condition {
|
||||
id: string;
|
||||
category: string;
|
||||
code_display: string;
|
||||
severity_code: string | null;
|
||||
severity_display: string | null;
|
||||
}
|
||||
|
||||
interface CarePlan {
|
||||
id: string;
|
||||
title: string;
|
||||
status: FhirCarePlanStatus;
|
||||
version: number | null;
|
||||
goals: SmartGoal[] | null;
|
||||
activities: Intervention[] | null;
|
||||
behandelstructuur: Behandelstructuur | null;
|
||||
sessie_planning: unknown[] | null;
|
||||
evaluatiemomenten: Evaluatiemoment[] | null;
|
||||
veiligheidsplan: Veiligheidsplan | null;
|
||||
created_at: string | null;
|
||||
published_at: string | null;
|
||||
period_start: string | null;
|
||||
}
|
||||
|
||||
interface BehandelplanFlatProps {
|
||||
patientId: string;
|
||||
carePlan: CarePlan | null;
|
||||
condition: Condition | null;
|
||||
hulpvraag: string | null;
|
||||
lifeDomainScores: LifeDomainScore[] | null;
|
||||
// Callbacks
|
||||
onGenerate?: () => Promise<void>;
|
||||
onCreateManual?: () => Promise<void>;
|
||||
onStatusChange?: (status: FhirCarePlanStatus) => Promise<void>;
|
||||
onSaveBehandeldoel?: (doel: Behandeldoel) => Promise<void>;
|
||||
onDeleteBehandeldoel?: (doelId: string) => Promise<void>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* BehandelplanFlat - Hoofdcomponent voor plat behandelplan
|
||||
*
|
||||
* 3 blokken:
|
||||
* 1. Context Header (read-only): Diagnose, hulpvraag, leefgebieden
|
||||
* 2. Behandeldoelen (editable): Cards met inline interventies
|
||||
* 3. Planning & Evaluatie (collapsed): Evaluaties, sessies, veiligheidsplan
|
||||
*/
|
||||
export function BehandelplanFlat({
|
||||
patientId,
|
||||
carePlan,
|
||||
condition,
|
||||
hulpvraag,
|
||||
lifeDomainScores,
|
||||
onGenerate,
|
||||
onCreateManual,
|
||||
onStatusChange,
|
||||
onSaveBehandeldoel,
|
||||
onDeleteBehandeldoel,
|
||||
className,
|
||||
}: BehandelplanFlatProps) {
|
||||
const [editingDoelId, setEditingDoelId] = useState<string | null>(null);
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
// Transform old structure to flat
|
||||
const behandeldoelen: Behandeldoel[] = carePlan?.goals && carePlan?.activities
|
||||
? transformToFlat(carePlan.goals, carePlan.activities)
|
||||
: [];
|
||||
|
||||
const totalProgress = calculateBehandeldoelenProgress(behandeldoelen);
|
||||
const statusInfo = carePlan?.status ? FHIR_STATUS_LABELS[carePlan.status] : null;
|
||||
|
||||
// Handlers
|
||||
const handleGenerate = async () => {
|
||||
if (!onGenerate) return;
|
||||
setIsGenerating(true);
|
||||
try {
|
||||
await onGenerate();
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateManual = async () => {
|
||||
if (!onCreateManual) return;
|
||||
setIsCreating(true);
|
||||
try {
|
||||
await onCreateManual();
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveDoel = async (doel: Behandeldoel) => {
|
||||
if (!onSaveBehandeldoel) return;
|
||||
await onSaveBehandeldoel(doel);
|
||||
setEditingDoelId(null);
|
||||
};
|
||||
|
||||
const handleDeleteDoel = async (doelId: string) => {
|
||||
if (!onDeleteBehandeldoel) return;
|
||||
await onDeleteBehandeldoel(doelId);
|
||||
setEditingDoelId(null);
|
||||
};
|
||||
|
||||
const handleAddDoel = () => {
|
||||
const newDoel = createEmptyBehandeldoel();
|
||||
// Start editing immediately
|
||||
setEditingDoelId(newDoel.id);
|
||||
// We need to save this empty doel first, then edit
|
||||
// For now, we'll handle this in the parent component
|
||||
};
|
||||
|
||||
// No plan yet - show creation options
|
||||
if (!carePlan) {
|
||||
return (
|
||||
<div className={cn('space-y-4', className)}>
|
||||
{/* Context header */}
|
||||
<ContextHeader
|
||||
condition={condition}
|
||||
hulpvraag={hulpvraag}
|
||||
lifeDomainScores={lifeDomainScores}
|
||||
/>
|
||||
|
||||
{/* Creation options */}
|
||||
<Card className="border-dashed border-2 border-slate-300">
|
||||
<CardContent className="p-6 text-center space-y-4">
|
||||
<div className="mx-auto w-12 h-12 rounded-full bg-indigo-100 flex items-center justify-center">
|
||||
<FileText className="h-6 w-6 text-indigo-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-slate-900">
|
||||
Nog geen behandelplan
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
Maak een nieuw behandelplan aan
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<Button
|
||||
onClick={handleGenerate}
|
||||
disabled={isGenerating}
|
||||
className="bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
{isGenerating ? 'Genereren...' : 'Genereer met AI'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCreateManual}
|
||||
disabled={isCreating}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{isCreating ? 'Aanmaken...' : 'Handmatig aanmaken'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-4', className)}>
|
||||
{/* Plan header with status */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-lg font-semibold text-slate-900">
|
||||
{carePlan.title || 'Behandelplan'}
|
||||
</h2>
|
||||
{statusInfo && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
style={{ borderColor: statusInfo.color, color: statusInfo.color }}
|
||||
>
|
||||
{statusInfo.label}
|
||||
</Badge>
|
||||
)}
|
||||
{carePlan.version && (
|
||||
<span className="text-sm text-slate-500">v{carePlan.version}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Overall progress */}
|
||||
<div className="flex items-center gap-2 text-sm text-slate-600">
|
||||
<span>Voortgang:</span>
|
||||
<div className="w-24">
|
||||
<Progress value={totalProgress} className="h-2" />
|
||||
</div>
|
||||
<span className="font-medium">{totalProgress}%</span>
|
||||
</div>
|
||||
{/* Status actions */}
|
||||
{carePlan.status === 'draft' && onStatusChange && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onStatusChange('active')}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4 mr-1" />
|
||||
Activeren
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Block 1: Context Header */}
|
||||
<ContextHeader
|
||||
condition={condition}
|
||||
hulpvraag={hulpvraag}
|
||||
lifeDomainScores={lifeDomainScores}
|
||||
/>
|
||||
|
||||
{/* Block 2: Behandeldoelen */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-slate-700 uppercase tracking-wide">
|
||||
Behandeldoelen ({behandeldoelen.length})
|
||||
</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleAddDoel}
|
||||
className="text-indigo-600 hover:text-indigo-700"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Nieuw doel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{behandeldoelen.length === 0 ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="p-6 text-center">
|
||||
<p className="text-sm text-slate-500">
|
||||
Nog geen behandeldoelen. Klik op "Nieuw doel" om te beginnen.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{behandeldoelen.map((doel) => (
|
||||
<BehandeldoelCard
|
||||
key={doel.id}
|
||||
doel={doel}
|
||||
isEditing={editingDoelId === doel.id}
|
||||
onEdit={() => setEditingDoelId(doel.id)}
|
||||
onSave={handleSaveDoel}
|
||||
onCancel={() => setEditingDoelId(null)}
|
||||
onDelete={() => handleDeleteDoel(doel.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Block 3: Planning & Evaluatie */}
|
||||
<PlanningSection
|
||||
behandelstructuur={carePlan.behandelstructuur}
|
||||
evaluatiemomenten={carePlan.evaluatiemomenten}
|
||||
veiligheidsplan={carePlan.veiligheidsplan}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { type LifeDomain, type LifeDomainScore, LIFE_DOMAIN_META } from '@/lib/types/leefgebieden';
|
||||
import { Stethoscope, MessageSquareQuote } from 'lucide-react';
|
||||
|
||||
interface Condition {
|
||||
id: string;
|
||||
category: string;
|
||||
code_display: string;
|
||||
severity_code: string | null;
|
||||
severity_display: string | null;
|
||||
}
|
||||
|
||||
interface ContextHeaderProps {
|
||||
condition: Condition | null;
|
||||
hulpvraag: string | null;
|
||||
lifeDomainScores: LifeDomainScore[] | null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blok 1: Context Header
|
||||
* Read-only samenvatting van diagnose, hulpvraag en leefgebieden
|
||||
*/
|
||||
export function ContextHeader({
|
||||
condition,
|
||||
hulpvraag,
|
||||
lifeDomainScores,
|
||||
className,
|
||||
}: ContextHeaderProps) {
|
||||
// Filter op leefgebieden met hoge prioriteit of lage scores
|
||||
const priorityDomains = lifeDomainScores?.filter(
|
||||
(s) => s.priority === 'hoog' || s.baseline <= 2
|
||||
) || [];
|
||||
|
||||
return (
|
||||
<Card className={cn('bg-slate-50 border-slate-200', className)}>
|
||||
<CardContent className="p-4 space-y-3">
|
||||
{/* Diagnose */}
|
||||
<div className="flex items-start gap-2">
|
||||
<Stethoscope className="h-4 w-4 text-slate-500 mt-0.5 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<span className="text-xs font-medium text-slate-500 uppercase tracking-wide">
|
||||
Diagnose
|
||||
</span>
|
||||
{condition ? (
|
||||
<p className="text-sm font-medium text-slate-900">
|
||||
{condition.code_display}
|
||||
{condition.severity_display && (
|
||||
<span className="text-slate-500 font-normal ml-1">
|
||||
({condition.severity_display})
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-slate-500 italic">Geen diagnose vastgesteld</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hulpvraag */}
|
||||
{hulpvraag && (
|
||||
<div className="flex items-start gap-2">
|
||||
<MessageSquareQuote className="h-4 w-4 text-slate-500 mt-0.5 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<span className="text-xs font-medium text-slate-500 uppercase tracking-wide">
|
||||
Hulpvraag
|
||||
</span>
|
||||
<p className="text-sm text-slate-700 italic">“{hulpvraag}”</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Leefgebieden bars */}
|
||||
{priorityDomains.length > 0 && (
|
||||
<div className="pt-2 border-t border-slate-200">
|
||||
<span className="text-xs font-medium text-slate-500 uppercase tracking-wide block mb-2">
|
||||
Prioritaire leefgebieden
|
||||
</span>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
|
||||
{priorityDomains.map((score) => (
|
||||
<LifeDomainBar key={score.domain} score={score} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
interface LifeDomainBarProps {
|
||||
score: LifeDomainScore;
|
||||
}
|
||||
|
||||
function LifeDomainBar({ score }: LifeDomainBarProps) {
|
||||
const meta = LIFE_DOMAIN_META[score.domain];
|
||||
const progressPercent = (score.baseline / 5) * 100;
|
||||
const targetPercent = (score.target / 5) * 100;
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="font-medium text-slate-700">
|
||||
{meta.emoji} {meta.shortLabel}
|
||||
</span>
|
||||
<span className="text-slate-500">
|
||||
{score.baseline} → {score.target}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-slate-200 rounded-full relative overflow-hidden">
|
||||
{/* Target indicator */}
|
||||
<div
|
||||
className="absolute h-full w-0.5 bg-slate-400 z-10"
|
||||
style={{ left: `${targetPercent}%` }}
|
||||
/>
|
||||
{/* Current progress */}
|
||||
<div
|
||||
className="h-full rounded-full transition-all"
|
||||
style={{
|
||||
width: `${progressPercent}%`,
|
||||
backgroundColor: meta.color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export { BehandelplanFlat } from './behandelplan-flat';
|
||||
export { ContextHeader } from './context-header';
|
||||
export { BehandeldoelCard } from './behandeldoel-card';
|
||||
export { BehandeldoelForm } from './behandeldoel-form';
|
||||
export { PlanningSection } from './planning-section';
|
||||
@@ -1,251 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
type Behandelstructuur,
|
||||
type Evaluatiemoment,
|
||||
type Veiligheidsplan,
|
||||
EVALUATION_STATUSES,
|
||||
} from '@/lib/types/behandelplan';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Calendar,
|
||||
Clock,
|
||||
Shield,
|
||||
AlertTriangle,
|
||||
Phone,
|
||||
CheckCircle2,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface PlanningSectionProps {
|
||||
behandelstructuur: Behandelstructuur | null;
|
||||
evaluatiemomenten: Evaluatiemoment[] | null;
|
||||
veiligheidsplan: Veiligheidsplan | null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blok 3: Planning & Evaluatie
|
||||
* Collapsed by default, bevat:
|
||||
* - Evaluatiemomenten
|
||||
* - Behandelstructuur
|
||||
* - Veiligheidsplan (indien aanwezig)
|
||||
*/
|
||||
export function PlanningSection({
|
||||
behandelstructuur,
|
||||
evaluatiemomenten,
|
||||
veiligheidsplan,
|
||||
className,
|
||||
}: PlanningSectionProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const evaluatiesCount = evaluatiemomenten?.length || 0;
|
||||
const hasVeiligheidsplan = !!veiligheidsplan;
|
||||
|
||||
// Count pending evaluations
|
||||
const pendingEvaluaties =
|
||||
evaluatiemomenten?.filter((e) => e.status === 'gepland').length || 0;
|
||||
|
||||
return (
|
||||
<Card className={cn('', className)}>
|
||||
{/* Collapsed header */}
|
||||
<CardHeader
|
||||
className="p-3 cursor-pointer hover:bg-slate-50 transition-colors"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4 text-slate-500" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-slate-500" />
|
||||
)}
|
||||
<Calendar className="h-4 w-4 text-slate-500" />
|
||||
<span className="font-medium text-slate-700">
|
||||
Planning & Evaluatie
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{pendingEvaluaties > 0 && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{pendingEvaluaties} gepland
|
||||
</Badge>
|
||||
)}
|
||||
{hasVeiligheidsplan && (
|
||||
<Badge variant="outline" className="text-xs text-orange-600 border-orange-300">
|
||||
<Shield className="h-3 w-3 mr-1" />
|
||||
Veiligheidsplan
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{/* Expanded content */}
|
||||
{isExpanded && (
|
||||
<CardContent className="p-4 pt-0 space-y-4 border-t">
|
||||
{/* Behandelstructuur */}
|
||||
{behandelstructuur && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-medium text-slate-500 uppercase tracking-wide">
|
||||
Behandelstructuur
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-3 text-sm">
|
||||
<div className="flex items-center gap-1.5 text-slate-700">
|
||||
<Clock className="h-4 w-4 text-slate-400" />
|
||||
<span>{behandelstructuur.duur}</span>
|
||||
</div>
|
||||
<span className="text-slate-300">•</span>
|
||||
<span className="text-slate-700">{behandelstructuur.frequentie}</span>
|
||||
<span className="text-slate-300">•</span>
|
||||
<span className="text-slate-700">
|
||||
{behandelstructuur.aantalSessies} sessies
|
||||
</span>
|
||||
<span className="text-slate-300">•</span>
|
||||
<span className="text-slate-700">{behandelstructuur.vorm}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Evaluatiemomenten */}
|
||||
{evaluatiemomenten && evaluatiemomenten.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-medium text-slate-500 uppercase tracking-wide">
|
||||
Evaluatiemomenten
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{evaluatiemomenten.map((eval_) => (
|
||||
<EvaluatieItem key={eval_.id} evaluatie={eval_} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Veiligheidsplan */}
|
||||
{veiligheidsplan && (
|
||||
<VeiligheidsplanSection veiligheidsplan={veiligheidsplan} />
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
interface EvaluatieItemProps {
|
||||
evaluatie: Evaluatiemoment;
|
||||
}
|
||||
|
||||
function EvaluatieItem({ evaluatie }: EvaluatieItemProps) {
|
||||
const isCompleted = evaluatie.status === 'afgerond';
|
||||
const typeLabel =
|
||||
evaluatie.type === 'tussentijds'
|
||||
? 'Tussentijds'
|
||||
: evaluatie.type === 'eind'
|
||||
? 'Eind'
|
||||
: 'Crisis';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 p-2 rounded-md text-sm',
|
||||
isCompleted ? 'bg-green-50' : 'bg-slate-50'
|
||||
)}
|
||||
>
|
||||
{isCompleted ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 shrink-0" />
|
||||
) : (
|
||||
<div className="h-4 w-4 rounded-full border-2 border-slate-300 shrink-0" />
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-slate-700 truncate">
|
||||
Week {evaluatie.weekNumber}: {typeLabel}
|
||||
</p>
|
||||
{evaluatie.plannedDate && (
|
||||
<p className="text-xs text-slate-500">
|
||||
{new Date(evaluatie.plannedDate).toLocaleDateString('nl-NL', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface VeiligheidsplanSectionProps {
|
||||
veiligheidsplan: Veiligheidsplan;
|
||||
}
|
||||
|
||||
function VeiligheidsplanSection({ veiligheidsplan }: VeiligheidsplanSectionProps) {
|
||||
return (
|
||||
<div className="space-y-3 p-3 bg-orange-50 border border-orange-200 rounded-md">
|
||||
<div className="flex items-center gap-2 text-orange-700">
|
||||
<Shield className="h-4 w-4" />
|
||||
<h4 className="font-medium text-sm">Veiligheidsplan</h4>
|
||||
</div>
|
||||
|
||||
{/* Waarschuwingssignalen */}
|
||||
{veiligheidsplan.waarschuwingssignalen.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-medium text-orange-700 flex items-center gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
Waarschuwingssignalen
|
||||
</p>
|
||||
<ul className="text-sm text-orange-900 space-y-0.5">
|
||||
{veiligheidsplan.waarschuwingssignalen.map((signal, i) => (
|
||||
<li key={i} className="flex items-start gap-1.5">
|
||||
<span className="text-orange-400">•</span>
|
||||
<span>{signal}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Coping strategieën */}
|
||||
{veiligheidsplan.copingStrategieen.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-medium text-orange-700">
|
||||
Coping strategieën
|
||||
</p>
|
||||
<ul className="text-sm text-orange-900 space-y-0.5">
|
||||
{veiligheidsplan.copingStrategieen.map((strategy, i) => (
|
||||
<li key={i} className="flex items-start gap-1.5">
|
||||
<span className="text-orange-400">•</span>
|
||||
<span>{strategy}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Contacten */}
|
||||
{veiligheidsplan.contacten.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-medium text-orange-700 flex items-center gap-1">
|
||||
<Phone className="h-3 w-3" />
|
||||
Noodcontacten
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{veiligheidsplan.contacten.map((contact, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-sm bg-white/50 rounded p-1.5 text-orange-900"
|
||||
>
|
||||
<p className="font-medium">{contact.naam}</p>
|
||||
<p className="text-xs text-orange-700">{contact.rol}</p>
|
||||
<p className="text-xs">{contact.telefoon}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
// Behandelplan Views
|
||||
export { BehandelplanView } from './behandelplan-view';
|
||||
export { BehandelplanList } from './behandelplan-list';
|
||||
|
||||
// Editable Components
|
||||
export { EditableSection, ItemActions } from './editable-section';
|
||||
|
||||
// Section Forms
|
||||
export { BehandelstructuurForm } from './sections/behandelstructuur-form';
|
||||
export { GoalForm } from './sections/goal-form';
|
||||
export { InterventionForm } from './sections/intervention-form';
|
||||
@@ -1,77 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import type { Behandelstructuur } from '@/lib/types/behandelplan';
|
||||
|
||||
interface BehandelstructuurFormProps {
|
||||
initialData?: Behandelstructuur | null;
|
||||
onChange: (data: Behandelstructuur) => void;
|
||||
}
|
||||
|
||||
const DUUR_OPTIONS = ['4 weken', '6 weken', '8 weken', '10 weken', '12 weken', '16 weken', '24 weken'];
|
||||
const FREQUENTIE_OPTIONS = ['Wekelijks', 'Tweewekelijks', 'Maandelijks', '2x per week'];
|
||||
const VORM_OPTIONS = ['Individueel', 'Groep', 'Gezin', 'Paar', 'Online', 'Hybride'];
|
||||
|
||||
export function BehandelstructuurForm({ initialData, onChange }: BehandelstructuurFormProps) {
|
||||
const [data, setData] = useState<Behandelstructuur>(
|
||||
initialData || {
|
||||
duur: '8 weken',
|
||||
frequentie: 'Wekelijks',
|
||||
aantalSessies: 8,
|
||||
vorm: 'Individueel',
|
||||
}
|
||||
);
|
||||
|
||||
const handleChange = (field: keyof Behandelstructuur, value: string | number) => {
|
||||
const updated = { ...data, [field]: value };
|
||||
setData(updated);
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="duur">Duur</Label>
|
||||
<Select value={data.duur} onValueChange={(v) => handleChange('duur', v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecteer duur" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DUUR_OPTIONS.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="frequentie">Frequentie</Label>
|
||||
<Select value={data.frequentie} onValueChange={(v) => handleChange('frequentie', v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecteer frequentie" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FREQUENTIE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sessies">Aantal sessies</Label>
|
||||
<Input
|
||||
id="sessies"
|
||||
type="number"
|
||||
min={1}
|
||||
max={52}
|
||||
value={data.aantalSessies}
|
||||
onChange={(e) => handleChange('aantalSessies', parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="vorm">Vorm</Label>
|
||||
<Select value={data.vorm} onValueChange={(v) => handleChange('vorm', v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecteer vorm" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{VORM_OPTIONS.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import type { SmartGoal, GoalStatus } from '@/lib/types/behandelplan';
|
||||
import { LIFE_DOMAINS, LIFE_DOMAIN_META, type LifeDomain } from '@/lib/types/leefgebieden';
|
||||
|
||||
interface GoalFormProps {
|
||||
initialData?: SmartGoal | null;
|
||||
onChange: (data: SmartGoal) => void;
|
||||
}
|
||||
|
||||
const PRIORITY_OPTIONS = [
|
||||
{ value: 'hoog', label: 'Hoog' },
|
||||
{ value: 'middel', label: 'Middel' },
|
||||
{ value: 'laag', label: 'Laag' },
|
||||
];
|
||||
|
||||
const STATUS_OPTIONS: { value: GoalStatus; label: string }[] = [
|
||||
{ value: 'niet_gestart', label: 'Niet gestart' },
|
||||
{ value: 'bezig', label: 'Bezig' },
|
||||
{ value: 'gehaald', label: 'Gehaald' },
|
||||
{ value: 'bijgesteld', label: 'Bijgesteld' },
|
||||
];
|
||||
|
||||
export function GoalForm({ initialData, onChange }: GoalFormProps) {
|
||||
const [data, setData] = useState<SmartGoal>(
|
||||
initialData || {
|
||||
id: crypto.randomUUID(),
|
||||
title: '',
|
||||
description: '',
|
||||
clientVersion: '',
|
||||
lifeDomain: 'dlv',
|
||||
priority: 'middel',
|
||||
measurability: '',
|
||||
timelineWeeks: 8,
|
||||
status: 'niet_gestart',
|
||||
progress: 0,
|
||||
}
|
||||
);
|
||||
|
||||
const handleChange = <K extends keyof SmartGoal>(field: K, value: SmartGoal[K]) => {
|
||||
const updated = { ...data, [field]: value };
|
||||
setData(updated);
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Titel</Label>
|
||||
<Input
|
||||
id="title"
|
||||
value={data.title}
|
||||
onChange={(e) => handleChange('title', e.target.value)}
|
||||
placeholder="Korte beschrijving van het doel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lifeDomain">Leefgebied</Label>
|
||||
<Select
|
||||
value={data.lifeDomain}
|
||||
onValueChange={(v) => handleChange('lifeDomain', v as LifeDomain)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecteer leefgebied" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LIFE_DOMAINS.map((domain) => (
|
||||
<SelectItem key={domain} value={domain}>
|
||||
{LIFE_DOMAIN_META[domain].emoji} {LIFE_DOMAIN_META[domain].label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">SMART Beschrijving</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={data.description}
|
||||
onChange={(e) => handleChange('description', e.target.value)}
|
||||
placeholder="Specifiek, Meetbaar, Acceptabel, Realistisch, Tijdgebonden"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="clientVersion">Cliënt versie (B1-taal)</Label>
|
||||
<Textarea
|
||||
id="clientVersion"
|
||||
value={data.clientVersion}
|
||||
onChange={(e) => handleChange('clientVersion', e.target.value)}
|
||||
placeholder="Eenvoudige uitleg voor de cliënt"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="priority">Prioriteit</Label>
|
||||
<Select
|
||||
value={data.priority}
|
||||
onValueChange={(v) => handleChange('priority', v as 'hoog' | 'middel' | 'laag')}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PRIORITY_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="status">Status</Label>
|
||||
<Select
|
||||
value={data.status}
|
||||
onValueChange={(v) => handleChange('status', v as GoalStatus)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{STATUS_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="timelineWeeks">Tijdlijn (weken)</Label>
|
||||
<Input
|
||||
id="timelineWeeks"
|
||||
type="number"
|
||||
min={1}
|
||||
max={52}
|
||||
value={data.timelineWeeks}
|
||||
onChange={(e) => handleChange('timelineWeeks', parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="measurability">Meetbaarheid</Label>
|
||||
<Input
|
||||
id="measurability"
|
||||
value={data.measurability}
|
||||
onChange={(e) => handleChange('measurability', e.target.value)}
|
||||
placeholder="Hoe meten we vooruitgang?"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<Label>Voortgang</Label>
|
||||
<span className="text-sm text-slate-500">{data.progress}%</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[data.progress]}
|
||||
onValueChange={(v) => handleChange('progress', v[0])}
|
||||
max={100}
|
||||
step={5}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import type { Intervention, SmartGoal } from '@/lib/types/behandelplan';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
|
||||
interface InterventionFormProps {
|
||||
initialData?: Intervention | null;
|
||||
goals?: SmartGoal[];
|
||||
onChange: (data: Intervention) => void;
|
||||
}
|
||||
|
||||
const COMMON_INTERVENTIONS = [
|
||||
'CGT (Cognitieve Gedragstherapie)',
|
||||
'EMDR',
|
||||
'ACT (Acceptance and Commitment Therapy)',
|
||||
'Schematherapie',
|
||||
'Psycho-educatie',
|
||||
'Mindfulness',
|
||||
'Exposure therapie',
|
||||
'Systeemtherapie',
|
||||
];
|
||||
|
||||
export function InterventionForm({ initialData, goals = [], onChange }: InterventionFormProps) {
|
||||
const [data, setData] = useState<Intervention>(
|
||||
initialData || {
|
||||
id: crypto.randomUUID(),
|
||||
name: '',
|
||||
description: '',
|
||||
rationale: '',
|
||||
linkedGoalIds: [],
|
||||
}
|
||||
);
|
||||
|
||||
const handleChange = <K extends keyof Intervention>(field: K, value: Intervention[K]) => {
|
||||
const updated = { ...data, [field]: value };
|
||||
setData(updated);
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
const toggleGoalLink = (goalId: string) => {
|
||||
const linkedGoalIds = data.linkedGoalIds.includes(goalId)
|
||||
? data.linkedGoalIds.filter((id) => id !== goalId)
|
||||
: [...data.linkedGoalIds, goalId];
|
||||
handleChange('linkedGoalIds', linkedGoalIds);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Naam interventie</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={data.name}
|
||||
onChange={(e) => handleChange('name', e.target.value)}
|
||||
placeholder="bijv. CGT, EMDR, ACT"
|
||||
list="interventions"
|
||||
/>
|
||||
<datalist id="interventions">
|
||||
{COMMON_INTERVENTIONS.map((name) => (
|
||||
<option key={name} value={name} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Beschrijving</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={data.description}
|
||||
onChange={(e) => handleChange('description', e.target.value)}
|
||||
placeholder="Uitleg van de interventie"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="rationale">Rationale</Label>
|
||||
<Textarea
|
||||
id="rationale"
|
||||
value={data.rationale}
|
||||
onChange={(e) => handleChange('rationale', e.target.value)}
|
||||
placeholder="Waarom past deze interventie bij deze cliënt?"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{goals.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label>Gekoppelde doelen</Label>
|
||||
<div className="space-y-2 p-3 bg-slate-50 rounded-lg">
|
||||
{goals.map((goal) => (
|
||||
<div key={goal.id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`goal-${goal.id}`}
|
||||
checked={data.linkedGoalIds.includes(goal.id)}
|
||||
onCheckedChange={() => toggleGoalLink(goal.id)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`goal-${goal.id}`}
|
||||
className="text-sm cursor-pointer"
|
||||
>
|
||||
{goal.title || 'Doel zonder titel'}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -41,23 +41,10 @@ interface EncounterSummary {
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface CarePlanSummary {
|
||||
id?: string;
|
||||
title?: string | null;
|
||||
status?: string | null;
|
||||
based_on_intake_id?: string | null;
|
||||
behandelstructuur?: unknown;
|
||||
goals?: unknown;
|
||||
activities?: unknown;
|
||||
evaluatiemomenten?: unknown;
|
||||
}
|
||||
|
||||
interface PatientDashboardResponse {
|
||||
patient: FHIRPatient;
|
||||
intakes: Intake[];
|
||||
encounters: EncounterSummary[];
|
||||
carePlan: CarePlanSummary | null;
|
||||
hulpvraag?: string | null;
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
@@ -179,11 +166,6 @@ export function PatientDashboardBlock({ prefill }: PatientDashboardBlockProps) {
|
||||
return displayEncounters;
|
||||
}, [encounters]);
|
||||
|
||||
const goalsCount = Array.isArray(data?.carePlan?.goals) ? data?.carePlan?.goals.length : 0;
|
||||
const interventionsCount = Array.isArray(data?.carePlan?.activities)
|
||||
? data?.carePlan?.activities.length
|
||||
: 0;
|
||||
|
||||
// E3.S2: Use patientNameFromPrefill for title
|
||||
const title = patientNameFromPrefill
|
||||
? `${config.title} - ${patientNameFromPrefill}`
|
||||
@@ -337,36 +319,6 @@ export function PatientDashboardBlock({ prefill }: PatientDashboardBlockProps) {
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Behandelplan */}
|
||||
<section className="bg-white rounded-lg border border-slate-200 p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<ClipboardList className="h-4 w-4 text-purple-600" />
|
||||
<h3 className="text-sm font-medium text-slate-700">Actief behandelplan</h3>
|
||||
</div>
|
||||
{data.carePlan ? (
|
||||
<div className="space-y-3">
|
||||
{data.hulpvraag && (
|
||||
<div className="bg-slate-50 rounded-lg p-3 text-sm text-slate-700">
|
||||
<p className="text-xs font-medium text-slate-500 mb-1">Hulpvraag</p>
|
||||
<p className="italic">“{data.hulpvraag}”</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div className="bg-teal-50 border border-teal-200 rounded-lg p-3">
|
||||
<p className="text-xs text-teal-700 mb-1">Doelen</p>
|
||||
<p className="text-base font-semibold text-teal-900">{goalsCount}</p>
|
||||
</div>
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-lg p-3">
|
||||
<p className="text-xs text-purple-700 mb-1">Interventies</p>
|
||||
<p className="text-base font-semibold text-purple-900">{interventionsCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-slate-500">Geen actief behandelplan</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-slate-500">Geen gegevens beschikbaar</div>
|
||||
|
||||
Reference in New Issue
Block a user