'use client'; /** * Patient Dashboard Block * * Swift artifact that shows patient properties and dashboard summary. */ import { useEffect, useMemo, useState } from 'react'; import { format } from 'date-fns'; import { nl } from 'date-fns/locale'; import { AlertCircle, Calendar, ClipboardList, Clock, FileText, Loader2, User, } from 'lucide-react'; import { BlockContainer } from './block-container'; import { safeFetch, getErrorInfo } from '@/lib/cortex/error-handler'; import { useToast } from '@/hooks/use-toast'; import { BLOCK_CONFIGS } from '@/lib/cortex/types'; import type { BlockPrefillData } from '@/stores/cortex-store'; import { useCortexStore } from '@/stores/cortex-store'; import type { FHIRPatient } from '@/lib/fhir'; import type { Intake } from '@/lib/types/intake'; import { cn } from '@/lib/utils'; import { formatPatientName } from '@/lib/fhir/patient-mapper'; interface PatientDashboardBlockProps { prefill?: BlockPrefillData; } interface EncounterSummary { id: string; period_start: string; period_end?: string | null; type_display?: string | null; status: string; } interface PatientDashboardResponse { patient: FHIRPatient; intakes: Intake[]; encounters: EncounterSummary[]; } const STATUS_LABELS: Record = { planned: 'Screening', active: 'Actief', finished: 'Afgerond', cancelled: 'Afgemeld', }; const GENDER_LABELS: Record = { male: 'Man', female: 'Vrouw', other: 'Anders', unknown: 'Onbekend', }; function extractEpisodeStatus(patient?: FHIRPatient): string | null { if (!patient) return null; const statusExtension = (patient as any)?.extension?.find( (ext: any) => ext.url === 'http://mini-epd.local/fhir/StructureDefinition/episode-status' ); return statusExtension?.valueCode || null; } function getPatientName(patient?: FHIRPatient): string { const name = patient?.name?.[0]; if (!name) return 'Onbekende patiënt'; return [ ...(name.prefix || []), ...(name.given || []), name.family, ] .filter(Boolean) .join(' '); } function getPatientBsn(patient?: FHIRPatient): string | null { if (!patient?.identifier) return null; return ( patient.identifier.find( (id) => id.system === 'http://fhir.nl/fhir/NamingSystem/bsn' )?.value || null ); } export function PatientDashboardBlock({ prefill }: PatientDashboardBlockProps) { const config = BLOCK_CONFIGS['patient-dashboard']; const { activePatient } = useCortexStore(); const { toast } = useToast(); // E3.S2: Use activePatient as fallback for patientId const patientId = prefill?.patientId || activePatient?.id; const patientNameFromPrefill = prefill?.patientName || (activePatient ? formatPatientName(activePatient) : undefined); const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(Boolean(patientId)); const [error, setError] = useState(null); useEffect(() => { if (!patientId) { setError('Geen patiënt geselecteerd'); setIsLoading(false); return; } const fetchDashboard = async () => { setIsLoading(true); setError(null); try { const response = await safeFetch( `/api/patients/${patientId}/dashboard`, undefined, { operation: 'Patiëntdashboard laden' } ); const result = (await response.json()) as PatientDashboardResponse; setData(result); } catch (err) { const statusCode = (err as any)?.statusCode; const errorInfo = getErrorInfo(err, { operation: 'Patiëntdashboard laden', statusCode, }); setError(errorInfo.description); toast({ variant: 'destructive', title: errorInfo.title, description: errorInfo.description, }); } finally { setIsLoading(false); } }; fetchDashboard(); }, [patientId, toast]); const patient = data?.patient; const patientName = useMemo(() => getPatientName(patient), [patient]); const patientStatus = extractEpisodeStatus(patient); const patientStatusLabel = patientStatus ? STATUS_LABELS[patientStatus] || patientStatus : null; const patientBirthDate = patient?.birthDate ? format(new Date(patient.birthDate), 'd MMM yyyy', { locale: nl }) : 'Onbekend'; const patientGender = patient?.gender ? GENDER_LABELS[patient.gender] || patient.gender : 'Onbekend'; const patientBsn = getPatientBsn(patient) || 'Onbekend'; const recentIntakes = data?.intakes?.slice(0, 3) || []; const encounters = data?.encounters || []; const encounterGroups = useMemo(() => { const now = new Date(); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const upcoming = encounters.filter((e) => new Date(e.period_start) >= todayStart); const recent = encounters.filter((e) => new Date(e.period_start) < todayStart); const displayEncounters = [...upcoming, ...recent].slice(0, 5); return displayEncounters; }, [encounters]); // E3.S2: Use patientNameFromPrefill for title const title = patientNameFromPrefill ? `${config.title} - ${patientNameFromPrefill}` : config.title; return ( {isLoading ? (
Dashboard laden...
) : error ? (

{error}

) : data ? (
{/* Basisgegevens */}

Basisgegevens

{patientStatusLabel && ( {patientStatusLabel} )}

Naam

{patientName}

Geboortedatum

{patientBirthDate}

BSN

{patientBsn}

Geslacht

{patientGender}

{/* Recente intakes */}

Recente intakes

({data.intakes.length})
{recentIntakes.length === 0 ? (

Geen intakes gevonden

) : (
{recentIntakes.map((intake) => (

{intake.title}

{intake.department} {format(new Date(intake.start_date), 'd MMM yyyy', { locale: nl })}
{intake.status}
))}
)}
{/* Agenda afspraken */}

Agenda afspraken

({encounters.length})
{encounterGroups.length === 0 ? (

Geen afspraken gevonden

) : (
{encounterGroups.map((encounter) => { const encounterDate = new Date(encounter.period_start); const isPast = encounterDate < new Date(); return (

{encounter.type_display || 'Afspraak'}

{format(encounterDate, 'd MMM yyyy HH:mm', { locale: nl })} {encounter.period_end && ( <> {format(new Date(encounter.period_end), 'HH:mm', { locale: nl })} )}
{encounter.status === 'planned' ? 'Gepland' : encounter.status === 'arrived' ? 'Aangekomen' : encounter.status === 'finished' ? 'Afgerond' : encounter.status}
); })}
)}
) : (
Geen gegevens beschikbaar
)}
); }