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>
270 lines
8.0 KiB
TypeScript
270 lines
8.0 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* RisicoBlock
|
|
*
|
|
* Shows risk assessments for an intake: level indicators, categories.
|
|
* Uses Cortex shared components and hooks.
|
|
*
|
|
* Epic: E2.S2 - Intake Blocks
|
|
*/
|
|
|
|
import { useCortexStore } from '@/stores/cortex-store';
|
|
import { BlockContainer } from './block-container';
|
|
import { BLOCK_CONFIGS } from '@/lib/cortex/types';
|
|
import {
|
|
AlertTriangle,
|
|
User,
|
|
ExternalLink,
|
|
Shield,
|
|
AlertCircle,
|
|
} from 'lucide-react';
|
|
|
|
// Use extracted hooks and components
|
|
import { useBlockData, useIntakeContext, type IntakePrefillData } from '@/lib/cortex/hooks';
|
|
import {
|
|
BlockLoading,
|
|
BlockError,
|
|
BlockEmpty,
|
|
BlockSection,
|
|
BlockItem,
|
|
BlockFooter,
|
|
getRiskBadgeVariant,
|
|
} from '@/components/cortex/shared';
|
|
|
|
// Import response type from API
|
|
import type { IntakeRisicoResponse, RiskAssessmentItem } from '@/app/api/cortex/intake/risico/route';
|
|
|
|
// ============================================================================
|
|
// Types
|
|
// ============================================================================
|
|
|
|
interface RisicoBlockProps {
|
|
prefill?: IntakePrefillData;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Sub-components
|
|
// ============================================================================
|
|
|
|
function RiskSummaryCard({ summary }: { summary: IntakeRisicoResponse['summary'] }) {
|
|
const getLevelColor = (level: string | null) => {
|
|
if (!level) return 'bg-slate-100 border-slate-200';
|
|
switch (level) {
|
|
case 'acuut':
|
|
return 'bg-red-100 border-red-300';
|
|
case 'hoog':
|
|
return 'bg-red-50 border-red-200';
|
|
case 'matig':
|
|
return 'bg-amber-50 border-amber-200';
|
|
case 'laag':
|
|
return 'bg-green-50 border-green-200';
|
|
default:
|
|
return 'bg-slate-100 border-slate-200';
|
|
}
|
|
};
|
|
|
|
const getLevelLabel = (level: string | null) => {
|
|
if (!level) return 'Geen';
|
|
return level.charAt(0).toUpperCase() + level.slice(1);
|
|
};
|
|
|
|
return (
|
|
<div className={`p-4 rounded-lg border ${getLevelColor(summary.highestLevel)}`}>
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<Shield className="h-5 w-5 text-slate-600" />
|
|
<span className="font-medium text-slate-800">Risico-overzicht</span>
|
|
</div>
|
|
<span className={`px-2.5 py-1 rounded-full text-sm font-medium ${
|
|
summary.highestLevel === 'acuut' || summary.highestLevel === 'hoog'
|
|
? 'bg-red-100 text-red-700'
|
|
: summary.highestLevel === 'matig'
|
|
? 'bg-amber-100 text-amber-700'
|
|
: 'bg-green-100 text-green-700'
|
|
}`}>
|
|
Hoogste: {getLevelLabel(summary.highestLevel)}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Risk indicators */}
|
|
<div className="flex flex-wrap gap-2">
|
|
{summary.hasSuicideRisk && (
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-red-100 text-red-700 rounded-full text-xs font-medium">
|
|
<AlertCircle className="h-3.5 w-3.5" />
|
|
Suïcide
|
|
</span>
|
|
)}
|
|
{summary.hasSelfHarmRisk && (
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-amber-100 text-amber-700 rounded-full text-xs font-medium">
|
|
<AlertCircle className="h-3.5 w-3.5" />
|
|
Zelfbeschadiging
|
|
</span>
|
|
)}
|
|
{summary.hasAggressionRisk && (
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-orange-100 text-orange-700 rounded-full text-xs font-medium">
|
|
<AlertCircle className="h-3.5 w-3.5" />
|
|
Agressie
|
|
</span>
|
|
)}
|
|
{!summary.hasSuicideRisk && !summary.hasSelfHarmRisk && !summary.hasAggressionRisk && summary.total === 0 && (
|
|
<span className="text-sm text-slate-500">Geen specifieke risico's geregistreerd</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-3 text-xs text-slate-500">
|
|
{summary.total} risicotaxatie{summary.total !== 1 ? 's' : ''} geregistreerd
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function RiskItem({ risk }: { risk: RiskAssessmentItem }) {
|
|
const formatDate = (dateStr: string) => {
|
|
return new Date(dateStr).toLocaleDateString('nl-NL', {
|
|
day: 'numeric',
|
|
month: 'short',
|
|
year: 'numeric',
|
|
});
|
|
};
|
|
|
|
return (
|
|
<BlockItem
|
|
title={risk.type}
|
|
subtitle={`${formatDate(risk.assessmentDate)}${risk.measures ? ' • Maatregelen vastgelegd' : ''}`}
|
|
badge={{
|
|
label: risk.level.charAt(0).toUpperCase() + risk.level.slice(1),
|
|
variant: getRiskBadgeVariant(risk.level),
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Main Component
|
|
// ============================================================================
|
|
|
|
export function RisicoBlock({ prefill }: RisicoBlockProps) {
|
|
const config = BLOCK_CONFIGS.risico_query;
|
|
const { closeBlock } = useCortexStore();
|
|
|
|
// Get patient context (from prefill or activePatient)
|
|
const { patientId, patientName, hasPatientContext } = useIntakeContext(prefill);
|
|
|
|
// Fetch risk data
|
|
const { data, isLoading, error, refetch } = useBlockData<IntakeRisicoResponse>({
|
|
endpoint: '/api/cortex/intake/risico',
|
|
params: {
|
|
patientId: patientId || undefined,
|
|
intakeId: prefill?.intakeId,
|
|
},
|
|
enabled: hasPatientContext,
|
|
operationName: 'Risicotaxaties 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="Risicotaxaties 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={AlertTriangle}
|
|
message="Geen risicotaxaties gevonden"
|
|
/>
|
|
</BlockContainer>
|
|
);
|
|
}
|
|
|
|
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>
|
|
)}
|
|
|
|
{/* Risk summary */}
|
|
<RiskSummaryCard summary={data.summary} />
|
|
|
|
{/* Risk assessments list */}
|
|
{data.risks.length > 0 ? (
|
|
<BlockSection
|
|
icon={AlertTriangle}
|
|
iconColor="text-orange-600"
|
|
title="Risicotaxaties"
|
|
count={data.risks.length}
|
|
>
|
|
<div className="space-y-2">
|
|
{data.risks.map((risk) => (
|
|
<RiskItem key={risk.id} risk={risk} />
|
|
))}
|
|
</div>
|
|
</BlockSection>
|
|
) : (
|
|
<div className="py-8 text-center">
|
|
<AlertTriangle className="h-8 w-8 text-slate-300 mx-auto mb-2" />
|
|
<p className="text-sm text-slate-500">
|
|
Nog geen risicotaxaties geregistreerd
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Footer with actions */}
|
|
<BlockFooter
|
|
secondaryAction={{
|
|
label: 'Sluiten',
|
|
onClick: closeBlock,
|
|
}}
|
|
primaryAction={{
|
|
label: 'Naar risicotaxaties',
|
|
icon: ExternalLink,
|
|
onClick: () => {
|
|
// Navigate to risk page
|
|
const url = `/epd/patients/${data.patientId}/intakes/${data.intakeId}/risk`;
|
|
window.location.href = url;
|
|
},
|
|
}}
|
|
/>
|
|
</div>
|
|
</BlockContainer>
|
|
);
|
|
}
|