Files
triqura-ecd/components/swift/shared/linked-evidence.tsx
colinislit adf6c3fb96 feat(swift): implementeer linked evidence UI voor overdracht (E5.S2)
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 <noreply@anthropic.com>
2025-12-27 18:38:45 +01:00

161 lines
5.3 KiB
TypeScript

'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 (
<span className={cn('inline-flex items-center gap-1 text-xs text-slate-500', className)}>
{bron.label} {bron.datum}
</span>
);
}
const getBronIcon = () => {
switch (bron.type) {
case 'observatie':
return <Activity className="h-3.5 w-3.5" />;
case 'rapportage':
case 'verpleegkundig':
return <FileText className="h-3.5 w-3.5" />;
case 'risico':
return <AlertTriangle className="h-3.5 w-3.5" />;
}
};
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 (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
className={cn(
'inline-flex items-center gap-1 text-xs text-slate-600 hover:text-slate-900',
'underline decoration-dotted underline-offset-2 cursor-help transition-colors',
className
)}
>
{getBronIcon()}
<span>{bron.label}</span>
<ExternalLink className="h-3 w-3 opacity-50" />
</button>
</PopoverTrigger>
<PopoverContent className="w-96 p-4" side="top" align="start">
<div className="space-y-3">
{/* Header */}
<div className="border-b border-slate-200 pb-2">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2">
{getBronIcon()}
<div>
<div className="text-sm font-medium text-slate-900">{bron.label}</div>
<div className="text-xs text-slate-500">{bron.datum}</div>
</div>
</div>
</div>
</div>
{/* Content based on type */}
{bron.type === 'observatie' && sourceData.value && (
<div className="space-y-2">
<div className="flex items-baseline gap-2">
<span className="text-2xl font-semibold text-slate-900">
{sourceData.value}
</span>
{sourceData.unit && (
<span className="text-sm text-slate-600">{sourceData.unit}</span>
)}
</div>
{sourceData.interpretation && (
<div className={cn('text-sm', getInterpretationColor(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)'}
</div>
)}
</div>
)}
{(bron.type === 'rapportage' || bron.type === 'verpleegkundig') && sourceData.content && (
<div className="space-y-2">
<div className="text-sm text-slate-700 bg-slate-50 rounded p-3 border border-slate-200">
{sourceData.content}
</div>
{sourceData.createdBy && (
<div className="text-xs text-slate-500">
Door: {sourceData.createdBy}
</div>
)}
</div>
)}
{bron.type === 'risico' && (
<div className="space-y-2">
{sourceData.riskLevel && (
<div className={cn('text-sm font-medium', getRiskLevelColor(sourceData.riskLevel))}>
Risiconiveau: {sourceData.riskLevel.replace('_', ' ')}
</div>
)}
{sourceData.rationale && (
<div className="text-sm text-slate-700 bg-slate-50 rounded p-3 border border-slate-200">
{sourceData.rationale}
</div>
)}
</div>
)}
</div>
</PopoverContent>
</Popover>
);
}