From adf6c3fb965a09601c48b05fbb3ddf5d282a7dea Mon Sep 17 00:00:00 2001 From: colinislit Date: Sat, 27 Dec 2025 18:38:45 +0100 Subject: [PATCH] feat(swift): implementeer linked evidence UI voor overdracht (E5.S2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E5.S2 compleet: Bronnotitie links met hover preview in OverdrachtBlock. Voortgang: 66 SP / 82 SP (80%) - Epic 5 40% compleet! Implementatie: - Bronverwijzingen nu klikbaar met hover popover voor volledige content - Source data enrichment in API response - Type-specific content display (observaties, rapportages, risico's) - Visual feedback met icons en kleuren per bron type Components & Types: - LinkedEvidence component (160 regels) - Reusable popover voor alle bron types - Aandachtspunt type extended met optionele sourceData field - enrichWithSourceData() functie in API route (48 regels) LinkedEvidence Features: - Hover popover met volledige bron content - Type-specific icons: Activity (observatie), FileText (rapportage), AlertTriangle (risico) - Click/hover trigger met underline-dotted styling - ExternalLink icon voor visual affordance - Popover positioning: top-start voor beste UX Source Data per Type: - Observaties: value + unit + interpretation (HH/H/N/L/LL) met kleuren - Rapportages/Verpleegkundig: content + createdBy - Risico's: riskLevel + rationale met severity kleuren API Enrichment Logic: - enrichWithSourceData() lookup in context (vitals, reports, risks) - Source data toegevoegd aan aandachtspunten vóór response - Type-safe matching op bron.id en bron.type - Backwards compatible: werkt met/zonder sourceData UI/UX Improvements: - Color-coded interpretations: Red (HH/LL kritiek), Amber (H/L), Teal (N) - Color-coded risk levels: Red (zeer hoog/hoog), Amber (gemiddeld), Teal (laag) - Formatted text display met bg-slate-50 border - Responsive popover met max-width 96 (384px) - Fallback: geen sourceData = plain text zonder popover Integration in OverdrachtBlock: - AandachtspuntItem gebruikt LinkedEvidence component - Replaced: "bron.label • bron.datum" plain text - Now: LinkedEvidence met hover preview Files Changed: - lib/types/overdracht.ts: +12 lines (sourceData interface) - app/api/overdracht/generate/route.ts: +58 lines (enrichWithSourceData) - components/swift/blocks/overdracht-block.tsx: +1/-3 lines (LinkedEvidence import/usage) - components/swift/shared/linked-evidence.tsx: +160 lines (NEW) Technical Details: - Popover from shadcn/ui (@/components/ui/popover) - Icons from lucide-react (Activity, FileText, AlertTriangle, ExternalLink) - Type guards voor safe property access (sourceData?.value) - Underline decoration-dotted cursor-help voor accessibility Testing: - Build succesvol (pnpm build) - /epd/swift: 143 kB (+24 kB door LinkedEvidence component) - Geen nieuwe type errors - Popover trigger werkt met keyboard (accessible) Voortgang: 66 SP / 82 SP (80%) - E5.S2 compleet (3 SP) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/api/overdracht/generate/route.ts | 58 ++++++- components/swift/blocks/overdracht-block.tsx | 5 +- components/swift/shared/linked-evidence.tsx | 160 +++++++++++++++++++ lib/types/overdracht.ts | 12 ++ 4 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 components/swift/shared/linked-evidence.tsx diff --git a/app/api/overdracht/generate/route.ts b/app/api/overdracht/generate/route.ts index 08baa44..4c19d3c 100644 --- a/app/api/overdracht/generate/route.ts +++ b/app/api/overdracht/generate/route.ts @@ -219,6 +219,59 @@ async function callClaudeAPI( return validated; } +/** + * Enrich aandachtspunten with full source data + */ +function enrichWithSourceData( + aandachtspunten: Aandachtspunt[], + context: OverdrachtContext +): Aandachtspunt[] { + return aandachtspunten.map((punt) => { + const { bron } = punt; + let sourceData: Aandachtspunt['sourceData']; + + switch (bron.type) { + case 'observatie': { + const vital = context.vitals.find((v) => v.id === bron.id); + if (vital) { + sourceData = { + value: vital.value_quantity_value?.toString() || undefined, + unit: vital.value_quantity_unit || undefined, + interpretation: vital.interpretation_code || undefined, + }; + } + break; + } + case 'rapportage': + case 'verpleegkundig': { + const report = context.reports.find((r) => r.id === bron.id); + if (report) { + sourceData = { + content: report.content, + createdBy: report.created_by || undefined, + }; + } + break; + } + case 'risico': { + const risk = context.risks.find((r) => r.id === bron.id); + if (risk) { + sourceData = { + riskLevel: risk.risk_level, + rationale: risk.rationale || undefined, + }; + } + break; + } + } + + return { + ...punt, + sourceData, + }; + }); +} + /** * Log AI event to database */ @@ -298,13 +351,16 @@ export async function POST(request: NextRequest) { const durationMs = Date.now() - startTime; + // Enrich aandachtspunten with full source data for linked evidence + const enrichedAandachtspunten = enrichWithSourceData(aiResult.aandachtspunten, context); + // Log AI event await logAIEvent(supabase, patientId, context, aiResult, durationMs); // Build response const response: AISamenvatting = { samenvatting: aiResult.samenvatting, - aandachtspunten: aiResult.aandachtspunten, + aandachtspunten: enrichedAandachtspunten, actiepunten: aiResult.actiepunten, generatedAt: new Date().toISOString(), durationMs, diff --git a/components/swift/blocks/overdracht-block.tsx b/components/swift/blocks/overdracht-block.tsx index e824b27..3c38ff6 100644 --- a/components/swift/blocks/overdracht-block.tsx +++ b/components/swift/blocks/overdracht-block.tsx @@ -29,6 +29,7 @@ import { nl } from 'date-fns/locale/nl'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { safeFetch, getErrorInfo, retryFetch } from '@/lib/swift/error-handler'; +import { LinkedEvidence } from '@/components/swift/shared/linked-evidence'; interface OverdrachtBlockProps { prefill?: BlockPrefillData; @@ -487,9 +488,7 @@ function AandachtspuntItem({ punt }: { punt: AISamenvatting['aandachtspunten'][0 > {getBronTypeLabel(punt.bron.type)} - - {punt.bron.label} • {punt.bron.datum} - + ); diff --git a/components/swift/shared/linked-evidence.tsx b/components/swift/shared/linked-evidence.tsx new file mode 100644 index 0000000..2baf75c --- /dev/null +++ b/components/swift/shared/linked-evidence.tsx @@ -0,0 +1,160 @@ +'use client'; + +/** + * Linked Evidence Component + * + * Shows source data in a hover popover for overdracht aandachtspunten. + * E5.S2: Provides quick access to original source without leaving context. + */ + +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import type { Aandachtspunt } from '@/lib/types/overdracht'; +import { ExternalLink, Activity, FileText, AlertTriangle } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface LinkedEvidenceProps { + bron: Aandachtspunt['bron']; + sourceData?: Aandachtspunt['sourceData']; + className?: string; +} + +export function LinkedEvidence({ bron, sourceData, className }: LinkedEvidenceProps) { + // If no source data, just show label without popover + if (!sourceData) { + return ( + + {bron.label} • {bron.datum} + + ); + } + + const getBronIcon = () => { + switch (bron.type) { + case 'observatie': + return ; + case 'rapportage': + case 'verpleegkundig': + return ; + case 'risico': + return ; + } + }; + + const getInterpretationColor = (code: string | undefined) => { + if (!code) return 'text-slate-600'; + switch (code) { + case 'HH': + case 'LL': + return 'text-red-700 font-semibold'; + case 'H': + case 'L': + return 'text-amber-700 font-medium'; + case 'N': + return 'text-teal-700'; + default: + return 'text-slate-600'; + } + }; + + const getRiskLevelColor = (level: string | undefined) => { + if (!level) return 'text-slate-600'; + switch (level.toLowerCase()) { + case 'zeer_hoog': + case 'hoog': + return 'text-red-700 font-semibold'; + case 'gemiddeld': + return 'text-amber-700'; + case 'laag': + return 'text-teal-700'; + default: + return 'text-slate-600'; + } + }; + + return ( + + + + + +
+ {/* Header */} +
+
+
+ {getBronIcon()} +
+
{bron.label}
+
{bron.datum}
+
+
+
+
+ + {/* Content based on type */} + {bron.type === 'observatie' && sourceData.value && ( +
+
+ + {sourceData.value} + + {sourceData.unit && ( + {sourceData.unit} + )} +
+ {sourceData.interpretation && ( +
+ Interpretatie: {sourceData.interpretation} + {sourceData.interpretation === 'HH' && ' (Kritiek hoog)'} + {sourceData.interpretation === 'H' && ' (Hoog)'} + {sourceData.interpretation === 'LL' && ' (Kritiek laag)'} + {sourceData.interpretation === 'L' && ' (Laag)'} + {sourceData.interpretation === 'N' && ' (Normaal)'} +
+ )} +
+ )} + + {(bron.type === 'rapportage' || bron.type === 'verpleegkundig') && sourceData.content && ( +
+
+ {sourceData.content} +
+ {sourceData.createdBy && ( +
+ Door: {sourceData.createdBy} +
+ )} +
+ )} + + {bron.type === 'risico' && ( +
+ {sourceData.riskLevel && ( +
+ Risiconiveau: {sourceData.riskLevel.replace('_', ' ')} +
+ )} + {sourceData.rationale && ( +
+ {sourceData.rationale} +
+ )} +
+ )} +
+
+
+ ); +} diff --git a/lib/types/overdracht.ts b/lib/types/overdracht.ts index 5a1e144..ea1a530 100644 --- a/lib/types/overdracht.ts +++ b/lib/types/overdracht.ts @@ -96,6 +96,18 @@ export interface Aandachtspunt { datum: string; label: string; }; + sourceData?: { + // Voor observaties (vitals) + value?: string; + unit?: string; + interpretation?: string; + // Voor reports (rapportage, verpleegkundig) + content?: string; + createdBy?: string; + // Voor risico's + riskLevel?: string; + rationale?: string; + }; } // Zod schema for generate request