feat(diagnose+agenda): Diagnose module met ICD-10 en agenda uitbreidingen

Diagnose Module:
- Diagnose overzicht pagina met alle patiënt diagnoses
- Diagnosis manager met ICD-10 combobox zoekfunctie
- Diagnose kaarten met hoofddiagnose markering
- Modal voor nieuwe/bewerkte diagnoses
- ICD-10 GGZ codes dataset (lib/data/)
- Zod schemas voor diagnose validatie
- TypeScript types voor ICD-10 (lib/types/icd10.ts)
- Complete documentatie (PRD, FO, TO, Bouwplan)

Agenda Uitbreidingen:
- Patient context card in afspraak modal
- Rapportage composer direct in afspraak modal
- Rapportage bewerken vanuit gekoppelde rapportages
- Verbeterde focus styling voor inputs

Behandelplan:
- Flat componenten structuur (behandeldoel-card, form, planning)
- Context header component
- Uitgebreide types (lib/types/behandelplan.ts)
- Actions voor behandelplan beheer

UI Componenten:
- Command component (shadcn/ui) voor combobox
- Popover component (shadcn/ui)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
colinislit
2025-12-12 13:29:59 +01:00
parent 593bef4dcf
commit 265d3a971f
36 changed files with 6663 additions and 236 deletions

View File

@@ -0,0 +1,160 @@
'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">
&ldquo;{doel.clientVersion}&rdquo;
</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>
);
}

View File

@@ -0,0 +1,357 @@
'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>
);
}

View File

@@ -0,0 +1,290 @@
'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 &quot;Nieuw doel&quot; 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>
);
}

View File

@@ -0,0 +1,130 @@
'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">&ldquo;{hulpvraag}&rdquo;</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>
);
}

View File

@@ -0,0 +1,5 @@
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';

View File

@@ -0,0 +1,251 @@
'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>
);
}