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:
@@ -1,13 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
import { createDiagnosis, deleteDiagnosis, type Condition } from '../../actions';
|
||||
import { Loader2, Trash2 } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { nl } from 'date-fns/locale';
|
||||
/**
|
||||
* Diagnosis Manager Component
|
||||
*
|
||||
* Beheert de lijst van diagnoses met modal voor toevoegen/bewerken en cards voor weergave.
|
||||
*/
|
||||
|
||||
const severities = ['licht', 'matig', 'ernstig'];
|
||||
const statuses = ['active', 'resolved', 'entered-in-error'];
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useTransition } from 'react';
|
||||
import { Plus, Loader2, AlertTriangle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { DiagnosisCard } from './diagnosis-card';
|
||||
import { DiagnosisModal } from './diagnosis-modal';
|
||||
import { deleteDiagnosis, type Condition } from '../../actions';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
|
||||
interface DiagnosisManagerProps {
|
||||
patientId: string;
|
||||
@@ -16,136 +30,180 @@ interface DiagnosisManagerProps {
|
||||
}
|
||||
|
||||
export function DiagnosisManager({ patientId, intakeId, diagnoses }: DiagnosisManagerProps) {
|
||||
const [form, setForm] = useState({ code: '', description: '', severity: severities[0], status: statuses[0], notes: '' });
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingDiagnosis, setEditingDiagnosis] = useState<Condition | undefined>();
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!form.code || !form.description) {
|
||||
setError('Code en omschrijving zijn verplicht.');
|
||||
return;
|
||||
}
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await createDiagnosis({
|
||||
patientId,
|
||||
intakeId,
|
||||
code: form.code,
|
||||
description: form.description,
|
||||
severity: form.severity,
|
||||
status: form.status,
|
||||
notes: form.notes,
|
||||
});
|
||||
setForm({ ...form, notes: '' });
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Opslaan mislukt');
|
||||
}
|
||||
// Sorteer diagnoses: hoofddiagnoses eerst, dan op datum
|
||||
const sortedDiagnoses = useMemo(() => {
|
||||
return [...diagnoses].sort((a, b) => {
|
||||
// Hoofddiagnoses eerst
|
||||
const aIsPrimary = a.category === 'primary-diagnosis';
|
||||
const bIsPrimary = b.category === 'primary-diagnosis';
|
||||
if (aIsPrimary && !bIsPrimary) return -1;
|
||||
if (!aIsPrimary && bIsPrimary) return 1;
|
||||
|
||||
// Dan op datum (nieuwste eerst)
|
||||
const aDate = a.recorded_date ? new Date(a.recorded_date).getTime() : 0;
|
||||
const bDate = b.recorded_date ? new Date(b.recorded_date).getTime() : 0;
|
||||
return bDate - aDate;
|
||||
});
|
||||
}, [diagnoses]);
|
||||
|
||||
const handleNewDiagnosis = () => {
|
||||
setEditingDiagnosis(undefined);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setDeletingId(id);
|
||||
const handleEditDiagnosis = (diagnosis: Condition) => {
|
||||
setEditingDiagnosis(diagnosis);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (diagnosisId: string) => {
|
||||
setDeletingId(diagnosisId);
|
||||
setShowDeleteConfirm(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = () => {
|
||||
if (!deletingId) return;
|
||||
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await deleteDiagnosis(patientId, intakeId, id);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Verwijderen mislukt');
|
||||
} finally {
|
||||
await deleteDiagnosis(patientId, intakeId, deletingId);
|
||||
toast({
|
||||
title: 'Diagnose verwijderd',
|
||||
description: 'De diagnose is succesvol verwijderd.',
|
||||
});
|
||||
setShowDeleteConfirm(false);
|
||||
setDeletingId(null);
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Verwijderen mislukt',
|
||||
description: error instanceof Error ? error.message : 'Er ging iets mis bij het verwijderen.',
|
||||
});
|
||||
setDeletingId(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleModalSuccess = () => {
|
||||
setModalOpen(false);
|
||||
setEditingDiagnosis(undefined);
|
||||
// Toast wordt al getoond door de modal
|
||||
};
|
||||
|
||||
const diagnosisToDelete = deletingId ? diagnoses.find((d) => d.id === deletingId) : null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{diagnoses.length === 0 && <p className="text-sm text-slate-500">Nog geen diagnoses.</p>}
|
||||
{diagnoses.map((diag) => (
|
||||
<div key={diag.id} className="rounded-lg border border-slate-200 p-4 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-slate-900">{diag.code_code} — {diag.code_display}</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
{diag.recorded_date
|
||||
? format(new Date(diag.recorded_date), 'd MMM yyyy', { locale: nl })
|
||||
: 'Onbekende datum'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDelete(diag.id)}
|
||||
disabled={deletingId === diag.id}
|
||||
className="inline-flex items-center gap-1 rounded-md border border-red-200 px-3 py-1.5 text-xs text-red-600"
|
||||
>
|
||||
{deletingId === diag.id ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />} Verwijder
|
||||
</button>
|
||||
</div>
|
||||
{diag.note && <p className="text-sm text-slate-700">{diag.note}</p>}
|
||||
</div>
|
||||
))}
|
||||
{/* Header met button */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-slate-900">
|
||||
Diagnoses ({diagnoses.length})
|
||||
</h3>
|
||||
</div>
|
||||
<Button onClick={handleNewDiagnosis} size="sm">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Nieuwe diagnose
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-slate-200 p-4 space-y-3">
|
||||
<h3 className="text-sm font-semibold text-slate-900">Nieuwe diagnose</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="DSM code"
|
||||
value={form.code}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, code: e.target.value }))}
|
||||
className="h-10 rounded-md border border-slate-300 px-3 text-sm"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Omschrijving"
|
||||
value={form.description}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, description: e.target.value }))}
|
||||
className="h-10 rounded-md border border-slate-300 px-3 text-sm"
|
||||
/>
|
||||
{/* Diagnoses lijst */}
|
||||
{sortedDiagnoses.length === 0 ? (
|
||||
<div className="text-center py-8 text-slate-500">
|
||||
<p className="text-sm">Nog geen diagnoses geregistreerd.</p>
|
||||
<p className="text-xs mt-1">Klik op "Nieuwe diagnose" om er een toe te voegen.</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<select
|
||||
value={form.severity}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, severity: e.target.value }))}
|
||||
className="h-10 rounded-md border border-slate-300 px-3 text-sm"
|
||||
>
|
||||
{severities.map((sev) => (
|
||||
<option key={sev} value={sev}>
|
||||
{sev}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={form.status}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, status: e.target.value }))}
|
||||
className="h-10 rounded-md border border-slate-300 px-3 text-sm"
|
||||
>
|
||||
{statuses.map((status) => (
|
||||
<option key={status} value={status}>
|
||||
{status}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{sortedDiagnoses.map((diagnosis) => {
|
||||
const isPrimary = diagnosis.category === 'primary-diagnosis';
|
||||
return (
|
||||
<DiagnosisCard
|
||||
key={diagnosis.id}
|
||||
diagnosis={diagnosis}
|
||||
isPrimary={isPrimary}
|
||||
onEdit={() => handleEditDiagnosis(diagnosis)}
|
||||
onDelete={() => handleDeleteClick(diagnosis.id)}
|
||||
isDeleting={deletingId === diagnosis.id && isPending}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<textarea
|
||||
value={form.notes}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, notes: e.target.value }))}
|
||||
placeholder="Notities"
|
||||
className="w-full rounded-md border border-slate-300 px-3 py-2 text-sm"
|
||||
/>
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={isPending}
|
||||
className="inline-flex items-center gap-2 rounded-md bg-teal-600 px-4 py-2 text-sm font-medium text-white"
|
||||
>
|
||||
{isPending && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
Opslaan
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Diagnosis Modal */}
|
||||
<DiagnosisModal
|
||||
open={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
patientId={patientId}
|
||||
intakeId={intakeId}
|
||||
diagnosis={editingDiagnosis}
|
||||
onSuccess={handleModalSuccess}
|
||||
/>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-red-600">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
Diagnose verwijderen?
|
||||
</DialogTitle>
|
||||
<DialogDescription asChild>
|
||||
<div className="space-y-4 pt-2">
|
||||
<p>
|
||||
Weet je zeker dat je deze diagnose wilt verwijderen? Deze actie kan
|
||||
niet ongedaan worden gemaakt.
|
||||
</p>
|
||||
{diagnosisToDelete && (
|
||||
<div className="bg-red-50 rounded-lg p-4 border border-red-100">
|
||||
<div className="font-medium text-slate-900">
|
||||
{diagnosisToDelete.code_code} — {diagnosisToDelete.code_display}
|
||||
</div>
|
||||
{diagnosisToDelete.severity_display && (
|
||||
<div className="text-sm text-slate-600 mt-1">
|
||||
Ernst: {diagnosisToDelete.severity_display}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowDeleteConfirm(false);
|
||||
setDeletingId(null);
|
||||
}}
|
||||
disabled={isPending}
|
||||
>
|
||||
Annuleren
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDeleteConfirm}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Verwijderen...
|
||||
</>
|
||||
) : (
|
||||
'Verwijderen'
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user