diff --git a/app/epd/patients/[id]/components/client-header.tsx b/app/epd/patients/[id]/components/client-header.tsx index 84ac1f7..fede5b1 100644 --- a/app/epd/patients/[id]/components/client-header.tsx +++ b/app/epd/patients/[id]/components/client-header.tsx @@ -2,13 +2,16 @@ /** * Client Header Component - * E2.S3: Context-aware header showing client name, status, and last modified + * E2.S3: Context-aware header showing client name, status, last modified en acties */ +import { Mic } from 'lucide-react'; import type { FHIRPatient } from '@/lib/fhir'; +import { Button } from '@/components/ui/button'; interface ClientHeaderProps { patient: FHIRPatient; + onNewReport?: () => void; } // Status badge component @@ -35,7 +38,7 @@ function StatusBadge({ status }: { status?: string }) { ); } -export function ClientHeader({ patient }: ClientHeaderProps) { +export function ClientHeader({ patient, onNewReport }: ClientHeaderProps) { // Extract name const name = patient.name?.[0]; const fullName = [ @@ -97,9 +100,15 @@ export function ClientHeader({ patient }: ClientHeaderProps) { - {/* Patient ID (subtle) */} -
- ID: {patient.id} +
+
+ ID: {patient.id} +
+ {onNewReport && ( + + )}
diff --git a/app/epd/patients/[id]/components/patient-layout-client.tsx b/app/epd/patients/[id]/components/patient-layout-client.tsx new file mode 100644 index 0000000..1a5de67 --- /dev/null +++ b/app/epd/patients/[id]/components/patient-layout-client.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import type { ReactNode } from 'react'; +import type { FHIRPatient } from '@/lib/fhir'; +import { ClientHeader } from './client-header'; +import { RapportageModal } from '../rapportage/components/rapportage-modal'; + +interface PatientLayoutClientProps { + patient: FHIRPatient; + patientId: string; + children: ReactNode; +} + +export function PatientLayoutClient({ patient, patientId, children }: PatientLayoutClientProps) { + const [isModalOpen, setModalOpen] = useState(false); + + const patientName = useMemo(() => { + const name = patient.name?.[0]; + if (!name) return 'deze patiënt'; + return [ + ...(name.prefix || []), + ...(name.given || []), + name.family, + ] + .filter(Boolean) + .join(' '); + }, [patient]); + + return ( +
+ setModalOpen(true)} /> +
{children}
+ setModalOpen(false)} + patientId={patientId} + patientName={patientName} + /> +
+ ); +} diff --git a/app/epd/patients/[id]/layout.tsx b/app/epd/patients/[id]/layout.tsx index 8e110d0..56600c2 100644 --- a/app/epd/patients/[id]/layout.tsx +++ b/app/epd/patients/[id]/layout.tsx @@ -1,11 +1,20 @@ +import type { ReactNode } from 'react'; +import { getPatient } from '../actions'; +import { PatientLayoutClient } from './components/patient-layout-client'; + export default async function PatientDetailLayout({ children, params, }: { - children: React.ReactNode; + children: ReactNode; params: Promise<{ id: string }>; }) { const { id } = await params; + const patient = await getPatient(id); - return <>{children}; + return ( + + {children} + + ); } diff --git a/app/epd/patients/[id]/rapportage/components/rapportage-modal.tsx b/app/epd/patients/[id]/rapportage/components/rapportage-modal.tsx new file mode 100644 index 0000000..92ea62a --- /dev/null +++ b/app/epd/patients/[id]/rapportage/components/rapportage-modal.tsx @@ -0,0 +1,196 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { SpeechRecorder } from '@/components/speech-recorder'; +import type { ClassificationResult } from '@/lib/types/report'; +import { createReport } from '../actions'; +import { toast } from '@/hooks/use-toast'; + +interface RapportageModalProps { + isOpen: boolean; + onClose: () => void; + patientId: string; + patientName: string; +} + +export function RapportageModal({ isOpen, onClose, patientId, patientName }: RapportageModalProps) { + const [content, setContent] = useState(''); + const [classification, setClassification] = useState(null); + const [selectedType, setSelectedType] = useState<'behandeladvies' | 'vrije_notitie'>('vrije_notitie'); + const [isAnalyzing, setIsAnalyzing] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + const router = useRouter(); + + useEffect(() => { + if (!isOpen) { + setContent(''); + setClassification(null); + setSelectedType('vrije_notitie'); + setError(null); + setIsAnalyzing(false); + setIsSaving(false); + } + }, [isOpen]); + + const handleAnalyze = async () => { + if (contentInvalid) return; + setIsAnalyzing(true); + setError(null); + try { + const response = await fetch('/api/reports/classify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ content }), + }); + + if (!response.ok) { + throw new Error('AI-analyse mislukt'); + } + + const result: ClassificationResult = await response.json(); + setClassification(result); + setSelectedType(result.type); + } catch (err) { + setClassification(null); + setSelectedType('vrije_notitie'); + const message = err instanceof Error ? err.message : 'AI-analyse mislukt'; + setError(message); + toast({ + variant: 'destructive', + title: 'AI-analyse mislukt', + description: message, + }); + } finally { + setIsAnalyzing(false); + } + }; + + const handleSave = async () => { + setIsSaving(true); + setError(null); + try { + await createReport(patientId, { + type: selectedType, + content, + ai_confidence: classification?.confidence, + ai_reasoning: classification?.reasoning, + }); + toast({ + title: 'Rapportage opgeslagen', + description: `${patientName} heeft nu een nieuwe notitie in de tijdlijn.`, + }); + router.refresh(); + onClose(); + } catch (err) { + const message = err instanceof Error ? err.message : 'Opslaan mislukt'; + setError(message); + toast({ + variant: 'destructive', + title: 'Opslaan mislukt', + description: message, + }); + } finally { + setIsSaving(false); + } + }; + + const characterCount = content.length; + const contentInvalid = characterCount < 20 || characterCount > 5000; + + return ( + { + if (!open) { + onClose(); + } + }} + > + + + Nieuwe rapportage + + Leg een rapportage vast voor {patientName} + + + +
+
+