Files
triqura-ecd/components/cortex/blocks/diagnose-block.tsx
colinislit b095b1e492 feat(cortex): add Intake Blocks MVP for voice/text commands
Adds 4 new intents for intake workflow:
- intake_status: shows completion percentage and section checklist
- intake_navigeer: navigation to specific intake tabs
- risico_query: displays risk assessments with severity indicators
- diagnose_query: shows primary/secondary diagnoses

New components:
- IntakeStatusBlock, RisicoBlock, DiagnoseBlock
- Shared block components (BlockLoading, BlockError, BlockEmpty, BlockSection, BlockItem, BlockFooter)
- useBlockData and useIntakeContext hooks

New API routes:
- GET /api/cortex/intake/status
- GET /api/cortex/intake/risico
- GET /api/cortex/intake/diagnose

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 23:21:18 +01:00

304 lines
8.7 KiB
TypeScript

'use client';
/**
* DiagnoseBlock
*
* Shows diagnoses for an intake: primary/secondary distinction, ICD-10 codes.
* Uses Cortex shared components and hooks.
*
* Epic: E2.S3 - Intake Blocks
*/
import { useCortexStore } from '@/stores/cortex-store';
import { BlockContainer } from './block-container';
import { BLOCK_CONFIGS } from '@/lib/cortex/types';
import {
Stethoscope,
User,
ExternalLink,
Star,
Activity,
} from 'lucide-react';
// Use extracted hooks and components
import { useBlockData, useIntakeContext, type IntakePrefillData } from '@/lib/cortex/hooks';
import {
BlockLoading,
BlockError,
BlockEmpty,
BlockSection,
BlockItem,
BlockFooter,
type BadgeVariant,
} from '@/components/cortex/shared';
// Import response type from API
import type { IntakeDiagnoseResponse, DiagnosisItem } from '@/app/api/cortex/intake/diagnose/route';
// ============================================================================
// Types
// ============================================================================
interface DiagnoseBlockProps {
prefill?: IntakePrefillData;
}
// ============================================================================
// Sub-components
// ============================================================================
function PrimaryDiagnosisCard({ diagnosis }: { diagnosis: DiagnosisItem }) {
return (
<div className="p-4 bg-indigo-50 rounded-lg border border-indigo-200">
<div className="flex items-start gap-3">
<div className="p-2 bg-indigo-100 rounded-lg">
<Star className="h-5 w-5 text-indigo-600" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-medium text-indigo-600 uppercase tracking-wide">
Hoofddiagnose
</span>
{diagnosis.code && (
<span className="text-xs text-indigo-500">
{diagnosis.codeSystem}: {diagnosis.code}
</span>
)}
</div>
<h4 className="font-medium text-slate-900">{diagnosis.description}</h4>
{diagnosis.severity && (
<p className="text-sm text-slate-600 mt-1">
Ernst: {diagnosis.severity}
</p>
)}
</div>
</div>
</div>
);
}
function getStatusBadgeVariant(status: string): BadgeVariant {
switch (status) {
case 'active':
case 'recurrence':
case 'relapse':
return 'warning';
case 'resolved':
case 'remission':
return 'success';
case 'inactive':
return 'default';
default:
return 'default';
}
}
function getStatusLabel(status: string): string {
const labels: Record<string, string> = {
active: 'Actief',
recurrence: 'Recidief',
relapse: 'Terugval',
inactive: 'Inactief',
remission: 'Remissie',
resolved: 'Hersteld',
};
return labels[status] || status;
}
function DiagnosisItemRow({ diagnosis }: { diagnosis: DiagnosisItem }) {
const formatDate = (dateStr: string) => {
if (!dateStr) return '';
return new Date(dateStr).toLocaleDateString('nl-NL', {
day: 'numeric',
month: 'short',
year: 'numeric',
});
};
const subtitle = [
diagnosis.code ? `${diagnosis.codeSystem}: ${diagnosis.code}` : null,
formatDate(diagnosis.recordedDate),
]
.filter(Boolean)
.join(' • ');
return (
<BlockItem
title={diagnosis.description}
subtitle={subtitle}
badge={{
label: getStatusLabel(diagnosis.clinicalStatus),
variant: getStatusBadgeVariant(diagnosis.clinicalStatus),
}}
/>
);
}
function DiagnosisSummary({ summary }: { summary: IntakeDiagnoseResponse['summary'] }) {
return (
<div className="flex items-center gap-6 p-3 bg-slate-50 rounded-lg border border-slate-200">
<div className="flex items-center gap-2">
<Activity className="h-4 w-4 text-slate-500" />
<span className="text-sm text-slate-600">
<span className="font-medium">{summary.total}</span> diagnose{summary.total !== 1 ? 's' : ''}
</span>
</div>
{summary.hasActiveConditions && (
<span className="inline-flex items-center px-2 py-0.5 bg-amber-50 text-amber-700 text-xs font-medium rounded-full border border-amber-200">
Actieve condities
</span>
)}
</div>
);
}
// ============================================================================
// Main Component
// ============================================================================
export function DiagnoseBlock({ prefill }: DiagnoseBlockProps) {
const config = BLOCK_CONFIGS.diagnose_query;
const { closeBlock } = useCortexStore();
// Get patient context (from prefill or activePatient)
const { patientId, patientName, hasPatientContext } = useIntakeContext(prefill);
// Fetch diagnosis data
const { data, isLoading, error, refetch } = useBlockData<IntakeDiagnoseResponse>({
endpoint: '/api/cortex/intake/diagnose',
params: {
patientId: patientId || undefined,
intakeId: prefill?.intakeId,
},
enabled: hasPatientContext,
operationName: 'Diagnoses laden',
});
// No patient context
if (!hasPatientContext) {
return (
<BlockContainer title={config.title} size={config.size}>
<BlockEmpty
icon={User}
message="Selecteer eerst een patiënt"
action={{
label: 'Patiënt zoeken',
onClick: () => {
closeBlock();
// TODO: Open zoeken block
},
}}
/>
</BlockContainer>
);
}
// Loading state
if (isLoading) {
return (
<BlockContainer title={config.title} size={config.size}>
<BlockLoading message="Diagnoses laden..." />
</BlockContainer>
);
}
// Error state
if (error) {
return (
<BlockContainer title={config.title} size={config.size}>
<BlockError message={error} onRetry={refetch} />
</BlockContainer>
);
}
// No data
if (!data) {
return (
<BlockContainer title={config.title} size={config.size}>
<BlockEmpty
icon={Stethoscope}
message="Geen diagnoses gevonden"
/>
</BlockContainer>
);
}
// Get secondary diagnoses (non-primary)
const secondaryDiagnoses = data.diagnoses.filter((d) => !d.isPrimary);
return (
<BlockContainer title={config.title} size={config.size}>
<div className="space-y-4">
{/* Patient name header */}
{patientName && (
<div className="text-sm text-slate-600 font-medium">
Patiënt: {patientName}
</div>
)}
{/* Summary stats */}
<DiagnosisSummary summary={data.summary} />
{/* Primary diagnosis */}
{data.summary.primaryDiagnosis ? (
<PrimaryDiagnosisCard diagnosis={data.summary.primaryDiagnosis} />
) : (
<div className="p-4 bg-slate-50 rounded-lg border border-slate-200 border-dashed">
<div className="flex items-center gap-2 text-slate-500">
<Star className="h-4 w-4" />
<span className="text-sm">Geen hoofddiagnose geregistreerd</span>
</div>
</div>
)}
{/* Secondary diagnoses */}
{secondaryDiagnoses.length > 0 ? (
<BlockSection
icon={Stethoscope}
iconColor="text-rose-600"
title="Nevendiagnoses"
count={secondaryDiagnoses.length}
>
<div className="space-y-2">
{secondaryDiagnoses.map((diagnosis) => (
<DiagnosisItemRow key={diagnosis.id} diagnosis={diagnosis} />
))}
</div>
</BlockSection>
) : data.diagnoses.length > 0 ? (
<div className="text-sm text-slate-500 text-center py-4">
Geen nevendiagnoses geregistreerd
</div>
) : null}
{/* No diagnoses at all */}
{data.diagnoses.length === 0 && (
<div className="py-8 text-center">
<Stethoscope className="h-8 w-8 text-slate-300 mx-auto mb-2" />
<p className="text-sm text-slate-500">
Nog geen diagnoses geregistreerd
</p>
</div>
)}
{/* Footer with actions */}
<BlockFooter
secondaryAction={{
label: 'Sluiten',
onClick: closeBlock,
}}
primaryAction={{
label: 'Naar diagnoses',
icon: ExternalLink,
onClick: () => {
// Navigate to diagnosis page
const url = `/epd/patients/${data.patientId}/intakes/${data.intakeId}/diagnosis`;
window.location.href = url;
},
}}
/>
</div>
</BlockContainer>
);
}